Роутер, контролери та сервіси
Що робить роутер
Роутер має мапінг між URL та відповідним обробником запиту. Наприклад, коли користувач вашого вебсайту з браузера запитує адресу /some-path
чи /other-path
, чи /path-with/:parameter
і т.д. - таким чином він повідомляє Ditsmod-застосунок, що хоче отримати певний ресурс, або хоче здійснити певну зміну на сайті. Щоб Ditsmod-застосунок знав, що саме треба робити в даних випадках, у його коді потрібно прописувати відповідні обробники запитів. Тоб то, якщо запитують /some-path
, значить треба виконати певну функцію; якщо запитують /other-path
, значить треба виконати іншу функцію і т.д. Коли прописують подібну відповідність між адресою та її обробником - це і є процес створення мапінгу між URL та відповідним обробником запиту.
Хоча вам не прийдеться вручну писати цей мапінг, але для загального уявлення роботи роутера, у дуже спрощеному вигляді його можна уявити так:
const routes = new Map<string, Function>();
routes.set('/some-path', function() { /** обробка запиту... **/ });
routes.set('/other-path', function() { /** обробка запиту... **/ });
routes.set('/path-with/:parameter', function() { /** обробка запиту... **/ });
// ...
Зразу після того, як Node.js отримує HTTP-запит і передає його в Ditsmod, URL запиту розбивається на дві частини, які розділяються знаком питання (якщо він є). Перша частина завжди містить так званий path, а друга частина - query-параметри, якщо URL містить знак питання.
Задача роутера полягає в тому, щоб знайти обробник HTTP-запиту по path. У дуже спрощеному вигляді цей процес можна уявити так:
const path = '/some-path';
const handle = routes.get(path);
// ...
// І потім цей обробник викликається у функції, що прослуховує HTTP-запити.
if (handle) {
handle();
}
У більшості випадків, обробник запиту викликає метод контролера.
Що являє собою контролер
Мапінг між URL та обробником запиту формується на основі метаданих, які закріпляються за методами контролерів. TypeScript клас стає контролером Ditsmod завдяки декоратору controller
:
import { controller } from '@ditsmod/core';
@controller()
export class SomeController {}
Файли контролерів рекомендується називати із закінченням *.controller.ts
, а імена їхніх класів - із закінченням *Controller
.
Починаючи з v2.50.0, Ditsmod дає можливість працювати з контролером у двох режимах:
- Контролер неодинак (по-дефолту). Його інстанс створюється за кожним HTTP-запитом.
- Контролер одинак. Його інстанс створюється один раз на рівні модуля під час ініціалізації застосунку.
Перший варіант більш безпечний, коли потрібно працювати в контексті поточного HTTP-запиту (клієнт надає певний ідентифікатор, який необхідно враховувати для формування відповіді). Другий варіант роботи помітно швидший (приблизно на 15%) і споживає менше пам'яті, але контекст запиту не можна зберігати у властивостях інстансу контролера, бо цей інстанс може одночасно використовуватись для інших клієнтів. В другому варіанті контекст запиту прийдеться передавати лише як аргумент для методів.
Щоб Ditsmod працював з контролером як з одинаком, в метаданих потрібно вказати { isSingleton: true }
:
import { controller } from '@ditsmod/core';
@controller({ isSingleton: true })
export class SomeController {}
Контролер неодинак
Як вже було сказано вище, після того, як роутер знайшов обробника HTTP-запиту, цей обробник може викликати метод контролера. Щоб це стало можливим, спочатку HTTP-запити прив'язуються до методів контролерів через систему маршрутизації, з використанням декоратора route
. В наступному прикладі створено єдиний маршрут, що приймає GET
запит за адресою /hello
:
import { controller, route, Res } from '@ditsmod/core';
@controller()
export class HelloWorldController {
@route('GET', 'hello')
method1(res: Res) {
res.send('Hello World!');
}
}
Що ми тут бачимо:
- Маршрут створюється за допомогою декоратора
route
, що ставиться перед методом класу, причому не важливо як саме називається цей метод. - В методі класу оголошується параметр
res
з типом данихRes
. Таким чином ми просимо Ditsmod щоб він створив інстанс класуRes
і передав його у відповідну змінну. До речі,res
- це скорочення від слова response. - Текстові відповіді на HTTP-запити відправляються через
res.send()
.
Хоча в попередньому прикладі інстанс класу Res
запитувався через method1
, але аналогічним чином ми можемо запитати цей інстанс і в конструкторі:
import { controller, route, Res } from '@ditsmod/core';
@controller()
export class HelloWorldController {
constructor(private res: Res) {}
@route('GET', 'hello')
method1() {
this.res.send('Hello World!');
}
}
Звичайно ж, у параметрах можна запитувати й інші інстанси класів, причому послідовність параметрів є неважливою.
Модифікатор доступу в конструкторі може бути будь-яким (private, protected або public), але взагалі без модифікатора - res
вже буде простим параметром з видимістю лише в конструкторі.
Щоб отримати pathParams
чи queryParams
, доведеться скористатись декоратором inject
та токенами PATH_PARAMS
і QUERY_PARAMS
:
import { controller, Res, route, inject, AnyObj, PATH_PARAMS, QUERY_PARAMS } from '@ditsmod/core';
@controller()
export class SomeController {
@route('GET', 'some-url/:param1/:param2')
method1(
@inject(PATH_PARAMS) pathParams: AnyObj,
@inject(QUERY_PARAMS) queryParams: AnyObj,
res: Res
) {
res.sendJson({ pathParams, queryParams });
}
}
Більше інформації про те, що таке токен та що саме робить декоратор inject
ви можете отримати з розділу Dependecy Injection.
Як бачите з попереднього прикладу, щоб відправляти відповіді з об'єктами, необхідно використовувати метод res.sendJson()
замість res.send()
(бо він відправляє тільки текст).
Рідні Node.js об'єкти запиту та відповіді можна отримати за токенами відповідно - NODE_REQ
та NODE_RES
:
import { controller, route, inject, NODE_REQ, NODE_RES, NodeRequest, NodeResponse } from '@ditsmod/core';
@controller()
export class HelloWorldController {
constructor(
@inject(NODE_REQ) private nodeReq: NodeRequest,
@inject(NODE_RES) private nodeRes: NodeResponse
) {}
@route('GET', 'hello')
method1() {
this.nodeRes.end('Hello World!');
}
}
Вас також може зацікавити як можна отримати тіло HTTP-запиту.
Контролер одинак
Через те, що інстанс контролера у цьому режимі створюється єдиний раз, ви не зможете запитувати у його конструкторі інстанси класів, які створюються за кожним запитом. Наприклад, якщо в конструкторі ви запросите інстанс класу Res
, Ditsmod кине помилку:
import { controller, route, RequestContext } from '@ditsmod/core';
@controller({ isSingleton: true })
export class HelloWorldController {
constructor(private res: Res) {}
@route('GET', 'hello')
method1() {
this.res.send('Hello, World!');
}
}
Робочий варіант буде таким:
import { controller, route, RequestContext } from '@ditsmod/core';
@controller({ isSingleton: true })
export class HelloWorldController {
@route('GET', 'hello')
method1(ctx: RequestContext) {
ctx.send('Hello, World!');
}
}
В режимі "контролер-одинак", методи контролерів, що прив'язані до певних роутів, отримують єдиний аргумент - контекст запиту. Тобто в цьому режимі ви вже не зможете запитати у Ditsmod, щоб він передавав у ці методи інстанси інших класів. Разом з тим, в конструкторі ви все ще можете запитувати інстанси певних класів, які створюються єдиний раз.