Оглавление

1. Введение
2. Разделите его на слои
3. Добавьте ООП
4. Под капотом
5. Пример

вступление

Что ж, мне нравится Express.js за его минимализм и удобство для начинающих - этот фреймворк действительно прост в использовании. Но когда код растет, вам нужен способ как-то его организовать. К сожалению, Express.js не предоставляет удобного способа сделать это, поэтому разработчики должны организовать это сами.

Разделите его на слои

Для удобства разделим наше серверное приложение на отдельные уровни.

  1. Контроллер - серверный модуль, который получает определенные данные от клиента и передает их на уровень сервиса.
  2. Сервис - бизнес-логика, то есть фрагменты кода, которые отвечают за обработку и манипулирование данными.
  3. Модель - данные из нашей базы данных, которая хорошо организована ORM.

Добавьте ООП

Представьте, что есть контроллер, который отвечает за аутентификацию пользователя. Он должен обеспечивать login логику и некоторые другие.

class AuthController extends Controller {
    path = '/auth'; // The path on which this.routes will be mapped
    routes = [
        {
            path: '/login', // Will become /auth/login
            method: Methods.POST,
            handler: this.handleLogin,
            localMiddleware: []
        },
        // Other routes...
    ];

    constructor() {
        super();
    };

    async handleLogin(req: Request, res: Response, next: NextFunction): Promise<void> {
        try {
            const { username, password } = req.body;    // Get credentials from client
            const userService = new UserService(username, password);
            const result = await userService.login();   // Use login service
            if (result.success) {
                // Send success response
            } else {
                // Send error response
            }
        } catch(e) {
            // Handle error
        }
    };
    // Other handlers...
}

Как видите, маршруты теперь выглядят как массив объектов со следующими свойствами:

  • path
  • method: метод HTTP
  • handler: конкретный обработчик для path
  • localMiddleware: массив промежуточного программного обеспечения, который сопоставлен с path каждого маршрута

Кроме того, логика входа в систему инкапсулирована в уровень сервиса, поэтому в обработчике мы просто передаем данные в экземпляр UserService, получаем результат и отправляем его обратно клиенту.

Под капотом

import { Response, Request, NextFunction, Router, RequestHandler } from 'express';

// HTTP methods
export enum Methods {
    GET = 'GET',
    POST = 'POST',
    PUT = 'PUT',
    DELETE = 'DELETE'
};

// Route interface for each route in `routes` field of `Controller` class.
interface IRoute {
    path: string;
    method: Methods;
    handler: (req: Request, res: Response, next: NextFunction) => void | Promise<void>;
    localMiddleware: ((req: Request, res: Response, next: NextFunction) => void)[]
};

export default abstract class Controller {
    // Router instance for mapping routes
    public router: Router = Router();
    // The path on which this.routes will be mapped
    public abstract path: string;
    // Array of objects which implement IRoutes interface
    protected abstract readonly routes: Array<IRoute> = [];

    public setRoutes = (): Router => {
    // Set HTTP method, middleware, and handler for each route
    // Returns Router object, which we will use in Server class
        for (const route of this.routes) {
            for (const mw of route.localMiddleware) {
                this.router.use(route.path, mw)
            };
            switch (route.method) {
                case 'GET':
                    this.router.get(route.path, route.handler);
                    break;
                case 'POST':
                    this.router.post(route.path, route.handler);
                    break;
                case 'PUT':
                    this.router.put(route.path, route.handler);
                    break;
                case 'DELETE':
                    this.router.delete(route.path, route.handler);
                    break;
                default:
                    // Throw exception
            };
        };
        // Return router instance (will be usable in Server class)
        return this.router;
    };
};

Что ж, все кажется довольно тривиальным. У нас есть экземпляр Router, который мы используем в качестве «движка» для каждого экземпляра класса, который будет унаследован от абстрактного класса Controller.

Еще одна хорошая идея - посмотреть, как реализован класс Server.

class Server {
    private app: Application;
    private readonly port: number;

    constructor(app: Application, database: Sequelize, port: number) {
        this.app = app;
        this.port = port;
    };

    public run(): http.Server {
        return this.app.listen(this.port, () => {
            console.log(`Up and running on port ${this.port}`)
        });
    };

    public loadGlobalMiddleware(middleware: Array<RequestHandler>): void {
        // global stuff like cors, body-parser, etc
        middleware.forEach(mw => {
            this.app.use(mw);
        });
    };

    public loadControllers(controllers: Array<Controller>): void {
        controllers.forEach(controller => {
            // use setRoutes method that maps routes and returns Router object
            this.app.use(controller.path, controller.setRoutes());
        });
    };

    public async initDatabase(): Promise<void> {
        // ...
    }
}

И в index.js:

const app = express();
const server = new Server(app, db, PORT);

const controllers: Array<Controller> = [
    new AuthController(),
    new TokenController(),
    new MatchmakingController(),
    new RoomController()
];

const globalMiddleware: Array<RequestHandler> = [
    urlencoded({ extended: false }),
    json(),
    cors({ credentials: true, origin: true }),
    // ...
];

Promise.resolve()
    .then(() => server.initDatabase())
    .then(() => {
        server.loadMiddleware(globalMiddleware);
        server.loadControllers(controllers);
        server.run();
    });

Пример

Я использовал эту практику организации в своем недавнем проекте, исходный код которого вы можете найти здесь: https://github.com/thedenisnikulin/chattitude-app-backend

Вот и все, спасибо, что прочитали эту статью :).

Первоначально опубликовано на https://dev.to 10 ноября 2020 г.