Оглавление
1. Введение
2. Разделите его на слои
3. Добавьте ООП
4. Под капотом
5. Пример
вступление
Что ж, мне нравится Express.js за его минимализм и удобство для начинающих - этот фреймворк действительно прост в использовании. Но когда код растет, вам нужен способ как-то его организовать. К сожалению, Express.js не предоставляет удобного способа сделать это, поэтому разработчики должны организовать это сами.
Разделите его на слои
Для удобства разделим наше серверное приложение на отдельные уровни.
- Контроллер - серверный модуль, который получает определенные данные от клиента и передает их на уровень сервиса.
- Сервис - бизнес-логика, то есть фрагменты кода, которые отвечают за обработку и манипулирование данными.
- Модель - данные из нашей базы данных, которая хорошо организована 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
: метод HTTPhandler
: конкретный обработчик для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 г.