@e22m4u/js-trie-router 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,13 +16,11 @@ HTTP маршрутизатор для Node.js на основе
16
16
  ## Содержание
17
17
 
18
18
  - [Установка](#установка)
19
- - [Расширения](#расширения)
20
19
  - [Использование](#использование)
21
20
  - [Контекст запроса](#контекст-запроса)
22
21
  - [Отправка ответа](#отправка-ответа)
22
+ - [Жизненный цикл](#жизненный-цикл)
23
23
  - [Хуки маршрута](#хуки-маршрута)
24
- - [preHandler](#prehandler)
25
- - [postHandler](#posthandler)
26
24
  - [Глобальные хуки](#глобальные-хуки)
27
25
  - [Метаданные маршрута](#метаданные-маршрута)
28
26
  - [Состояние запроса](#состояние-запроса)
@@ -180,16 +178,66 @@ router.defineRoute({
180
178
  });
181
179
  ```
182
180
 
181
+ ### Жизненный цикл
182
+
183
+ Для понимания того, как маршрутизатор обрабатывает входящий запрос, ниже
184
+ представлен порядок выполнения внутреннего конвейера и всех доступных хуков.
185
+
186
+ **На этапе запуска приложения**
187
+
188
+ 1. [Глобальные хуки `onDefineRoute`](#ondefineroute).
189
+ \- Вызываются при регистрации маршрута.
190
+
191
+ **При получении входящего HTTP-запроса**
192
+
193
+ 1. [Глобальные хуки `onRequest`](#onrequest)
194
+ \- Вызываются до поиска маршрута и создания контекста. Подходит для ранней
195
+ блокировки запроса или установки базовых заголовков.
196
+
197
+ 2. Поиск маршрута
198
+ \- Маршрутизатор ищет совпадение пути. Если маршрут не найден,
199
+ отправляется ответ `404`, а дальнейшая обработка прекращается.
200
+
201
+ 3. Создание `RequestContext` и парсинг
202
+ \- Создается экземпляр контекста, разбирается строка запроса, заголовки
203
+ и извлекается тело запроса.
204
+
205
+ 4. [Глобальные хуки `preHandler`](#prehandler-глобальный)
206
+ \- Вызываются последовательно. Если хук возвращает значение
207
+ или отправляет ответ, цикл прерывается.
208
+
209
+ 5. [Хуки маршрута `preHandler`](#prehandler-для-маршрута)
210
+ \- Вызываются последовательно для конкретного маршрута. Правила
211
+ прерывания такие же, как у одноименных глобальных хуков.
212
+
213
+ 6. Основной обработчик (функция `handler`)
214
+ \- Вызывается для формирования ответа сервера.
215
+
216
+ 7. [Хуки маршрута `postHandler`](#posthandler-для-маршрута)
217
+ \- Вызываются последовательно. Получают результат работы обработчика
218
+ (или из `preHandler`) и могут трансформировать его перед отправкой.
219
+
220
+ 8. [Глобальные хуки `postHandler`](#posthandler-глобальный)
221
+ \- Завершают цепочку трансформации ответа.
222
+
223
+ 9. Отправка ответа.
224
+ \- Маршрутизатор автоматически форматирует и отправляет итоговые данные
225
+ клиенту (если ответ не был отправлен ранее вручную).
226
+
227
+ Процесс обработки запроса обернут в глобальный `try/catch` блок. Любая ошибка,
228
+ выброшенная на любом этапе (в хуках, при разборе тела или в самом обработчике),
229
+ будет перехвачена и передана в `ErrorSender` для формирования ответа с ошибкой.
230
+
183
231
  ### Хуки маршрута
184
232
 
185
233
  Определение маршрута методом `defineRoute` позволяет задать хуки
186
234
  для отслеживания и перехвата входящего запроса и ответа
187
235
  конкретного маршрута.
188
236
 
189
- - `preHandler` выполняется перед вызовом обработчика
190
- - `postHandler` выполняется после вызова обработчика
237
+ - [`preHandler`](#prehandler-для-маршрута) выполняется перед вызовом обработчика;
238
+ - [`postHandler`](#posthandler-для-маршрута) выполняется после вызова обработчика;
191
239
 
192
- #### preHandler
240
+ #### preHandler (для маршрута)
193
241
 
194
242
  Перед вызовом обработчика маршрута может потребоваться выполнение
195
243
  таких операции как авторизация и проверка параметров запроса. Для
@@ -201,7 +249,7 @@ router.defineRoute({ // регистрация маршрута
201
249
  preHandler(ctx) {
202
250
  // перед обработчиком маршрута
203
251
  console.log(`Incoming request ${ctx.method} ${ctx.path}`);
204
- // > incoming request GET /myPath
252
+ // > Incoming request GET /myPath
205
253
  },
206
254
  handler(ctx) {
207
255
  return 'Hello world!';
@@ -209,9 +257,9 @@ router.defineRoute({ // регистрация маршрута
209
257
  });
210
258
  ```
211
259
 
212
- Если хук `preHandler` возвращает значение отличное от `undefined` и `null`,
213
- то такое значение будет использовано в качестве ответа сервера, а вызов
214
- обработчика маршрута будет пропущен.
260
+ Если хук `preHandler` возвращает значение отличное от `undefined`, то такое
261
+ значение будет использовано в качестве ответа сервера, а вызов следующих хуков
262
+ и основного обработчика маршрута будет прерван.
215
263
 
216
264
  ```js
217
265
  router.defineRoute({ // регистрация маршрута
@@ -223,66 +271,408 @@ router.defineRoute({ // регистрация маршрута
223
271
  handler(ctx) {
224
272
  // данный обработчик не будет вызван, так как
225
273
  // хук "preHandler" уже отправил ответ
274
+ throw new Error('Should not be called!');
275
+ },
276
+ });
277
+ ```
278
+
279
+ Допускается определение множества хуков `preHandler`, которые вызываются
280
+ последовательно перед основным обработчиком. В примере ниже используются
281
+ синхронные хуки, но маршрутизатор поддерживает и асинхронное выполнение,
282
+ при котором также сохраняется порядок вызова.
283
+
284
+ ```js
285
+ router.defineRoute({ // регистрация маршрута
286
+ // ...
287
+ preHandler: [
288
+ (ctx) => console.log('First hook invoked!'),
289
+ (ctx) => console.log('Second hook invoked!'),
290
+ ],
291
+ handler(ctx) {
292
+ // > First hook invoked!
293
+ // > Second hook invoked!
294
+ return 'OK';
226
295
  },
227
296
  });
228
297
  ```
229
298
 
230
- #### postHandler
299
+ Кроме возвращаемого значения, маршрутизатор отслеживает состояние отправки
300
+ ответа через экземпляр `ServerResponse`. Если сервер уже отправил ответ,
301
+ то вызов следующих хуков и основного обработчика прерывается.
231
302
 
232
- Возвращаемое значение обработчика маршрута передается вторым аргументом
233
- хука `postHandler`. По аналогии с `preHandler`, если возвращаемое
234
- значение отличается от `undefined` и `null`, то такое значение будет
235
- использовано в качестве ответа сервера. Это может быть полезно для
236
- модификации возвращаемого ответа.
303
+ ```js
304
+ router.defineRoute({ // регистрация маршрута
305
+ // ...
306
+ preHandler: [
307
+ (ctx) => {
308
+ // отправка ответа через ServerResponse
309
+ ctx.response.statusCode = 200;
310
+ ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
311
+ ctx.response.end('OK');
312
+ },
313
+ (ctx) => {
314
+ // данный хук не будет вызван, так как
315
+ // предыдущий уже отправил ответ 200 "OK"
316
+ throw new Error('Should not be called!');
317
+ }
318
+ ],
319
+ handler(ctx) {
320
+ // основной обработчик не будет вызван, так как
321
+ // хук "preHandler" уже отправил ответ 200 "OK"
322
+ throw new Error('Should not be called!');
323
+ },
324
+ });
325
+ ```
326
+
327
+ #### postHandler (для маршрута)
328
+
329
+ Данный хук выполняется после вызова основного обработчика маршрута
330
+ (или после `preHandler`, если тот завершил запрос досрочно). Его главной
331
+ задачей является перехват и трансформация данных перед отправкой клиенту.
332
+ Хук принимает контекст запроса первым аргументом, а вторым данные для отправки.
237
333
 
238
334
  ```js
239
335
  router.defineRoute({
240
- // ...
336
+ method: HttpMethod.GET,
337
+ path: '/hello',
241
338
  handler(ctx) {
242
- return 'Hello world!';
339
+ return 'Hello World!';
243
340
  },
244
341
  postHandler(ctx, data) {
245
- // после обработчика маршрута
246
- return data.toUpperCase(); // HELLO WORLD!
342
+ console.log(data); // > Hello World!
247
343
  },
248
344
  });
249
345
  ```
250
346
 
251
- ### Глобальные хуки
347
+ В отличие от `preHandler`, хуки `postHandler` работают по принципу конвейера.
348
+ Значение, возвращаемое хуком (если оно отлично от `undefined`), автоматически
349
+ заменяет собой текущие данные. Обновленный результат передается следующему
350
+ зарегистрированному хуку.
252
351
 
253
- Экземпляр роутера `TrieRouter` позволяет задать глобальные хуки, которые
254
- имеют более высокий приоритет перед хуками маршрута, и вызываются
255
- в первую очередь.
352
+ ```js
353
+ router.defineRoute({
354
+ method: HttpMethod.GET,
355
+ path: '/users/:id',
356
+ handler(ctx) {
357
+ // основной обработчик ничего не знает о формате ответа,
358
+ // он просто возвращает "сырые" данные из базы
359
+ return {id: 1, name: 'John Doe', passwordHash: 'secret'};
360
+ },
361
+ postHandler: [
362
+ // удаление чувствительных данных
363
+ (ctx, data) => {
364
+ const {passwordHash, ...safeUser} = data;
365
+ // возврат безопасного объекта, который заменит собой
366
+ // исходные данные и будет передан в следующий хук
367
+ // (или отправлен клиенту, если следующего хука нет)
368
+ return safeUser;
369
+ },
370
+ // стандартизация ответа
371
+ (ctx, data) => {
372
+ return {success: true, payload: data};
373
+ }
374
+ ],
375
+ });
256
376
 
257
- - `preHandler` выполняется перед вызовом обработчика каждого маршрута;
258
- - `postHandler` выполняется после вызова обработчика каждого маршрута;
259
- - `onDefineRoute` выполняется в момент регистрации маршрута;
377
+ // итоговый JSON, который уйдет клиенту:
378
+ // {
379
+ // "success": true,
380
+ // "payload": {"id": 1, "name": "John Doe"}
381
+ // }
382
+ ```
260
383
 
261
- Добавить глобальные хуки можно методами экземпляра `TrieRouter`.
384
+ Единственным условием для досрочного прерывания вызова `postHandler` хуков
385
+ является принудительная отправка HTTP-ответа внутри самого хука с использованием
386
+ нативного объекта `ctx.response`. В таком случае выполнение оставшихся хуков
387
+ прерывается.
262
388
 
263
389
  ```js
264
- import {RouterHookType} form '@e22m4u/js-trie-router';
265
-
266
- router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
267
- // перед обработчиком маршрута
390
+ router.defineRoute({
391
+ method: HttpMethod.GET,
392
+ path: '/report',
393
+ handler() {
394
+ // основной обработчик возвращает сырые данные
395
+ return {status: 'pending', id: 123};
396
+ },
397
+ postHandler: [
398
+ (ctx, data) => {
399
+ // принудительная отправка ответа напрямую
400
+ // через нативный объект ServerResponse
401
+ ctx.response.statusCode = 202;
402
+ ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
403
+ ctx.response.end('Отчет еще формируется, попробуйте позже.');
404
+ },
405
+ (ctx, data) => {
406
+ // данный хук никогда не будет выполнен, так как ответ
407
+ // уже был отправлен предыдущим хуком
408
+ return {...data, formatted: true};
409
+ },
410
+ ],
268
411
  });
412
+ ```
269
413
 
270
- router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
271
- // после обработчика маршрута
414
+ ### Глобальные хуки
415
+
416
+ Экземпляр маршрутизатора `TrieRouter` позволяет задавать глобальные хуки,
417
+ которые выполняются на различных этапах жизненного цикла.
418
+
419
+ - [`onDefineRoute`](#ondefineroute) выполняется перед регистрацией маршрута;
420
+ - [`onRequest`](#onrequest) выполняется при получении входящего HTTP-запроса;
421
+ - [`preHandler`](#prehandler-глобальный) выполняется перед вызовом обработчика каждого маршрута;
422
+ - [`postHandler`](#posthandler-глобальный) выполняется после вызова обработчика каждого маршрута;
423
+
424
+ Добавить глобальные хуки можно методом маршрутизатора `addHook`.
425
+
426
+ #### onDefineRoute
427
+
428
+ Перед регистрацией каждого маршрута выполняются хуки `onDefineRoute`. Данный
429
+ хук может быть только синхронным. В первый аргумент вызова передается копия
430
+ определения маршрута, а во второй экземпляр сервис-контейнера.
431
+
432
+ ```js
433
+ router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef, container) => {
434
+ // выполняется перед добавлением маршрута
435
+ console.log(routeDef);
436
+ // {
437
+ // method: 'GET',
438
+ // path: '/users',
439
+ // handler() {...}
440
+ // ...
441
+ // }
272
442
  });
273
443
 
274
- router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef) => {
444
+ // router.defineRoute(...)
445
+ ```
446
+
447
+ Возвращаемым значением данного хука может быть модифицированное определение
448
+ маршрута, либо `undefined`. Чтобы изменения параметров маршрута были учтены
449
+ маршрутизатором, требуется передать новое определение в качестве результата.
450
+
451
+ ```js
452
+ router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef, container) => {
275
453
  // позволяет модифицировать определение
276
- // маршрута в момент регистрации
454
+ // маршрута в момент его регистрации
277
455
  routeDef.method = HttpMethod.POST;
278
456
  routeDef.path = '/myPath';
279
457
  routeDef.handler = () => 'OK';
458
+ // так как аргументом "routeDef" является копия
459
+ // оригинального определения, требуется передать
460
+ // модифицированный аргумент в результат вызова
461
+ return routeDef;
462
+ });
463
+
464
+ // router.defineRoute(...)
465
+ ```
466
+
467
+ #### onRequest
468
+
469
+ Глобальный хук `onRequest` выполняется самым первым при получении входящего
470
+ запроса. В этот момент маршрутизатор еще не начал поиск подходящего маршрута,
471
+ не разобрал тело запроса и не создавал `RequestContext` (контекст запроса).
472
+
473
+ Хук принимает три аргумента:
474
+
475
+ - `request: IncomingMessage` нативный экземпляр запроса;
476
+ - `response: ServerResponse` нативный экземпляр ответа;
477
+ - `container: ServiceContainer` сервис-контейнер приложения;
478
+
479
+ Это идеальное место для установки общих CORS-заголовков, раннего логирования
480
+ или блокировки нежелательных запросов (например, по IP).
481
+
482
+ ```js
483
+ router.addHook(RouterHookType.ON_REQUEST, (req, res, container) => {
484
+ // логирование входящего запроса до начала любой обработки
485
+ console.log(`[Incoming]: ${req.method} ${req.url}`);
486
+ // установка глобальных заголовков
487
+ res.setHeader('X-Powered-By', 'TrieRouter');
488
+ });
489
+ ```
490
+
491
+ Если хук отправляет ответ клиенту (например, вызывает `res.end()`) или явно
492
+ возвращает логическое значение `true`, маршрутизатор немедленно прерывает
493
+ обработку запроса. Поиск маршрута, чтение тела и вызов остальных хуков
494
+ выполнены не будут.
495
+
496
+ ```js
497
+ router.addHook(RouterHookType.ON_REQUEST, (req, res) => {
498
+ const clientIp = req.socket.remoteAddress;
499
+ // пример блокировки запроса на самом раннем этапе
500
+ if (clientIp === '192.168.0.100') {
501
+ res.statusCode = 403;
502
+ res.end('Access denied');
503
+ return true; // прерывает дальнейшее выполнение
504
+ }
280
505
  });
281
506
  ```
282
507
 
283
- Аналогично хукам маршрута, если глобальный хук возвращает значение
284
- отличное от `undefined` и `null`, то такое значение будет использовано
285
- как ответ сервера.
508
+ Если хук `onRequest` возвращает значение, оно обязано быть логическим типом
509
+ или `undefined`. Также допускается `Promise`, разрешающийся этими значениями.
510
+ Попытка вернуть строку или объект приведет к выбросу ошибки.
511
+
512
+ #### preHandler (глобальный)
513
+
514
+ Глобальный хук `preHandler` вызывается перед каждым обработчиком маршрута,
515
+ и может быть полезен для аутентификации или других проверок доступа. Хук
516
+ будет вызван только в том случае, если для данного запроса найден
517
+ соответствующий маршрут.
518
+
519
+ ```js
520
+ router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
521
+ // вызывается перед каждым обработчиком маршрута
522
+ const token = ctx.headers['Authorization'];
523
+ if (token === 'secret-key') {
524
+ ctx.state.authenticated = true;
525
+ }
526
+ });
527
+ ```
528
+
529
+ Если глобальный хук `preHandler` возвращает значение отличное от `undefined`,
530
+ то такое значение будет использовано как ответ сервера. При этом, вызов
531
+ следующих хуков и основного обработчика маршрута будет прерван.
532
+
533
+ ```js
534
+ router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
535
+ // вызывается перед каждым обработчиком маршрута
536
+ return 'Hello World!';
537
+ });
538
+
539
+ router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
540
+ // данный хук не будет вызван, так как
541
+ // предыдущий уже отправил ответ "Hello World!"
542
+ throw new Error('Should not be called!');
543
+ });
544
+
545
+ // регистрация маршрута
546
+ router.defineRoute({
547
+ method: HttpMethod.GET,
548
+ path: '/',
549
+ handler() {
550
+ // данный обработчик не будет вызван, так как
551
+ // глобальный хук уже отправил ответ "Hello World!"
552
+ throw new Error('Should not be called!');
553
+ },
554
+ });
555
+ ```
556
+
557
+ Кроме возвращаемого значения, маршрутизатор отслеживает состояние отправки
558
+ ответа через экземпляр `ServerResponse`. Если сервер уже отправил ответ,
559
+ то вызов следующих хуков и основного обработчика маршрута будет прерван.
560
+
561
+ ```js
562
+ router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
563
+ // отправка ответа через ServerResponse
564
+ ctx.response.statusCode = 200;
565
+ ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
566
+ ctx.response.end('OK');
567
+ });
568
+
569
+ router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
570
+ // данный хук не будет вызван, так как
571
+ // предыдущий уже отправил ответ 200 "OK"
572
+ throw new Error('Should not be called!');
573
+ });
574
+
575
+ // регистрация маршрута
576
+ router.defineRoute({
577
+ method: HttpMethod.GET,
578
+ path: '/',
579
+ handler() {
580
+ // данный обработчик не будет вызван, так как
581
+ // глобальный хук уже отправил ответ 200 "OK"
582
+ throw new Error('Should not be called!');
583
+ },
584
+ });
585
+ ```
586
+
587
+ #### postHandler (глобальный)
588
+
589
+ Глобальный хук `postHandler` работает по такому же принципу, как и одноименный
590
+ хук на уровне маршрута, но применяется абсолютно ко всем обработанным запросам.
591
+ Хук принимает контекст запроса первым аргументом, а вторым данные для отправки.
592
+
593
+ ```js
594
+ router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
595
+ // GET /hello
596
+ console.log(data); // > Hello World!
597
+ });
598
+
599
+ // регистрация маршрута
600
+ router.defineRoute({
601
+ method: HttpMethod.GET,
602
+ path: '/hello',
603
+ handler() {
604
+ return 'Hello World!';
605
+ },
606
+ });
607
+ ```
608
+
609
+ Глобальные хуки `postHandler` позволяют применять трансформацию ко всем ответам
610
+ маршрутизатора. Это может быть использовано для приведения ответов к единому
611
+ формату, когда результат работы любого маршрута автоматически оборачивается
612
+ в стандартизированную структуру с добавлением метаинформации.
613
+
614
+ ```js
615
+ router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
616
+ // обертка ответа в единую структуру
617
+ return {
618
+ meta: {
619
+ timestamp: Date.now(),
620
+ path: ctx.pathname
621
+ },
622
+ data: data
623
+ };
624
+ });
625
+
626
+ // регистрация маршрута
627
+ router.defineRoute({
628
+ method: HttpMethod.GET,
629
+ path: '/hello',
630
+ handler() {
631
+ return 'Hello World!';
632
+ },
633
+ });
634
+
635
+ // запрос: GET /hello
636
+ // ответ сервера:
637
+ // {
638
+ // "meta": {"timestamp": 1672531200000, "path": "/hello"},
639
+ // "data": "Hello World!"
640
+ // }
641
+ ```
642
+
643
+ Если внутри хука выполнена отправка HTTP-ответа через методы нативного
644
+ объекта `ctx.response` (например, для перенаправления), то выполнение
645
+ цепочки хуков немедленно прерывается, и все последующие хуки игнорируются.
646
+
647
+ ```js
648
+ router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
649
+ // если обработчик вернул команду на редирект
650
+ if (data === 'REDIRECT_TO_LOGIN') {
651
+ // принудительная отправка ответа через ServerResponse
652
+ ctx.response.statusCode = 302;
653
+ ctx.response.setHeader('Location', '/login');
654
+ ctx.response.end();
655
+ // на данном этапе выполнение цепочки хуков прекращается
656
+ return;
657
+ }
658
+ return data;
659
+ });
660
+
661
+ router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
662
+ // этот код не будет выполнен, если предыдущий хук
663
+ // уже отправил ответ клиенту (вызвал ctx.response.end)
664
+ return {result: data};
665
+ });
666
+
667
+ // регистрация маршрута
668
+ router.defineRoute({
669
+ method: HttpMethod.GET,
670
+ path: '/dashboard',
671
+ handler() {
672
+ return 'REDIRECT_TO_LOGIN';
673
+ },
674
+ });
675
+ ```
286
676
 
287
677
  ### Метаданные маршрута
288
678
 
@@ -311,7 +701,7 @@ const router = new TrieRouter();
311
701
  // перед основным обработчиком каждого маршрута
312
702
  router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
313
703
  // доступ к метаданным текущего маршрута
314
- console.log(ctx.meta); // {foo: 'bar'}
704
+ console.log(ctx.meta); // > {foo: 'bar'}
315
705
  });
316
706
 
317
707
  router.defineRoute({
@@ -410,7 +800,7 @@ adminBranch.defineRoute({
410
800
  path: '/dashboard',
411
801
  handler: (ctx) => {
412
802
  // маршрут наследует префикс /admin и метаданные
413
- console.log(ctx.meta); // {access: 'admin'}
803
+ console.log(ctx.meta); // > {access: 'admin'}
414
804
  return 'Dashboard';
415
805
  },
416
806
  });