@e22m4u/js-trie-router 0.6.3 → 0.6.4
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 +334 -40
- package/dist/cjs/index.cjs +339 -291
- package/package.json +1 -1
- package/src/hooks/router-hook-invoker.d.ts +14 -13
- package/src/hooks/router-hook-invoker.js +175 -79
- package/src/hooks/router-hook-invoker.spec.js +658 -159
- package/src/trie-router.js +20 -29
- package/src/trie-router.spec.js +321 -178
package/README.md
CHANGED
|
@@ -21,8 +21,6 @@ HTTP маршрутизатор для Node.js на основе
|
|
|
21
21
|
- [Контекст запроса](#контекст-запроса)
|
|
22
22
|
- [Отправка ответа](#отправка-ответа)
|
|
23
23
|
- [Хуки маршрута](#хуки-маршрута)
|
|
24
|
-
- [preHandler](#prehandler)
|
|
25
|
-
- [postHandler](#posthandler)
|
|
26
24
|
- [Глобальные хуки](#глобальные-хуки)
|
|
27
25
|
- [Метаданные маршрута](#метаданные-маршрута)
|
|
28
26
|
- [Состояние запроса](#состояние-запроса)
|
|
@@ -186,10 +184,10 @@ router.defineRoute({
|
|
|
186
184
|
для отслеживания и перехвата входящего запроса и ответа
|
|
187
185
|
конкретного маршрута.
|
|
188
186
|
|
|
189
|
-
- `preHandler` выполняется перед вызовом
|
|
190
|
-
- `postHandler` выполняется после вызова
|
|
187
|
+
- [`preHandler`](#prehandler-для-маршрута) выполняется перед вызовом обработчика;
|
|
188
|
+
- [`postHandler`](#posthandler-для-маршрута) выполняется после вызова обработчика;
|
|
191
189
|
|
|
192
|
-
#### preHandler
|
|
190
|
+
#### preHandler (для маршрута)
|
|
193
191
|
|
|
194
192
|
Перед вызовом обработчика маршрута может потребоваться выполнение
|
|
195
193
|
таких операции как авторизация и проверка параметров запроса. Для
|
|
@@ -201,7 +199,7 @@ router.defineRoute({ // регистрация маршрута
|
|
|
201
199
|
preHandler(ctx) {
|
|
202
200
|
// перед обработчиком маршрута
|
|
203
201
|
console.log(`Incoming request ${ctx.method} ${ctx.path}`);
|
|
204
|
-
// >
|
|
202
|
+
// > Incoming request GET /myPath
|
|
205
203
|
},
|
|
206
204
|
handler(ctx) {
|
|
207
205
|
return 'Hello world!';
|
|
@@ -209,9 +207,9 @@ router.defineRoute({ // регистрация маршрута
|
|
|
209
207
|
});
|
|
210
208
|
```
|
|
211
209
|
|
|
212
|
-
Если хук `preHandler` возвращает значение отличное от `undefined
|
|
213
|
-
|
|
214
|
-
обработчика маршрута будет
|
|
210
|
+
Если хук `preHandler` возвращает значение отличное от `undefined`, то такое
|
|
211
|
+
значение будет использовано в качестве ответа сервера, а вызов следующих хуков
|
|
212
|
+
и основного обработчика маршрута будет прерван.
|
|
215
213
|
|
|
216
214
|
```js
|
|
217
215
|
router.defineRoute({ // регистрация маршрута
|
|
@@ -223,66 +221,362 @@ router.defineRoute({ // регистрация маршрута
|
|
|
223
221
|
handler(ctx) {
|
|
224
222
|
// данный обработчик не будет вызван, так как
|
|
225
223
|
// хук "preHandler" уже отправил ответ
|
|
224
|
+
throw new Error('Should not be called!');
|
|
226
225
|
},
|
|
227
226
|
});
|
|
228
227
|
```
|
|
229
228
|
|
|
230
|
-
|
|
229
|
+
Допускается определение множества хуков `preHandler`, которые вызываются
|
|
230
|
+
последовательно перед основным обработчиком. В примере ниже используются
|
|
231
|
+
синхронные хуки, но маршрутизатор поддерживает и асинхронное выполнение,
|
|
232
|
+
при котором также сохраняется порядок вызова.
|
|
231
233
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
```js
|
|
235
|
+
router.defineRoute({ // регистрация маршрута
|
|
236
|
+
// ...
|
|
237
|
+
preHandler: [
|
|
238
|
+
(ctx) => console.log('First hook invoked!'),
|
|
239
|
+
(ctx) => console.log('Second hook invoked!'),
|
|
240
|
+
],
|
|
241
|
+
handler(ctx) {
|
|
242
|
+
// > First hook invoked!
|
|
243
|
+
// > Second hook invoked!
|
|
244
|
+
return 'OK';
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Кроме возвращаемого значения, маршрутизатор отслеживает состояние отправки
|
|
250
|
+
ответа через экземпляр `ServerResponse`. Если сервер уже отправил ответ,
|
|
251
|
+
то вызов следующих хуков и основного обработчика прерывается.
|
|
237
252
|
|
|
238
253
|
```js
|
|
239
|
-
router.defineRoute({
|
|
254
|
+
router.defineRoute({ // регистрация маршрута
|
|
240
255
|
// ...
|
|
256
|
+
preHandler: [
|
|
257
|
+
(ctx) => {
|
|
258
|
+
// отправка ответа через ServerResponse
|
|
259
|
+
ctx.response.statusCode = 200;
|
|
260
|
+
ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
261
|
+
ctx.response.end('OK');
|
|
262
|
+
},
|
|
263
|
+
(ctx) => {
|
|
264
|
+
// данный хук не будет вызван, так как
|
|
265
|
+
// предыдущий уже отправил ответ 200 "OK"
|
|
266
|
+
throw new Error('Should not be called!');
|
|
267
|
+
}
|
|
268
|
+
],
|
|
241
269
|
handler(ctx) {
|
|
242
|
-
|
|
270
|
+
// основной обработчик не будет вызван, так как
|
|
271
|
+
// хук "preHandler" уже отправил ответ 200 "OK"
|
|
272
|
+
throw new Error('Should not be called!');
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### postHandler (для маршрута)
|
|
278
|
+
|
|
279
|
+
Данный хук выполняется после вызова основного обработчика маршрута
|
|
280
|
+
(или после `preHandler`, если тот завершил запрос досрочно). Его главной
|
|
281
|
+
задачей является перехват и трансформация данных перед отправкой клиенту.
|
|
282
|
+
Хук принимает контекст запроса первым аргументом, а вторым данные для отправки.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
router.defineRoute({
|
|
286
|
+
method: HttpMethod.GET,
|
|
287
|
+
path: '/hello',
|
|
288
|
+
handler(ctx) {
|
|
289
|
+
return 'Hello World!';
|
|
243
290
|
},
|
|
244
291
|
postHandler(ctx, data) {
|
|
245
|
-
//
|
|
246
|
-
return data.toUpperCase(); // HELLO WORLD!
|
|
292
|
+
console.log(data); // > Hello World!
|
|
247
293
|
},
|
|
248
294
|
});
|
|
249
295
|
```
|
|
250
296
|
|
|
251
|
-
|
|
297
|
+
В отличие от `preHandler`, хуки `postHandler` работают по принципу конвейера.
|
|
298
|
+
Значение, возвращаемое хуком (если оно отлично от `undefined`), автоматически
|
|
299
|
+
заменяет собой текущие данные. Обновленный результат передается следующему
|
|
300
|
+
зарегистрированному хуку.
|
|
252
301
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
302
|
+
```js
|
|
303
|
+
router.defineRoute({
|
|
304
|
+
method: HttpMethod.GET,
|
|
305
|
+
path: '/users/:id',
|
|
306
|
+
handler(ctx) {
|
|
307
|
+
// основной обработчик ничего не знает о формате ответа,
|
|
308
|
+
// он просто возвращает "сырые" данные из базы
|
|
309
|
+
return {id: 1, name: 'John Doe', passwordHash: 'secret'};
|
|
310
|
+
},
|
|
311
|
+
postHandler: [
|
|
312
|
+
// удаление чувствительных данных
|
|
313
|
+
(ctx, data) => {
|
|
314
|
+
const {passwordHash, ...safeUser} = data;
|
|
315
|
+
// возврат безопасного объекта, который заменит собой
|
|
316
|
+
// исходные данные и будет передан в следующий хук
|
|
317
|
+
// (или отправлен клиенту, если следующего хука нет)
|
|
318
|
+
return safeUser;
|
|
319
|
+
},
|
|
320
|
+
// стандартизация ответа
|
|
321
|
+
(ctx, data) => {
|
|
322
|
+
return {success: true, payload: data};
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
});
|
|
256
326
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
327
|
+
// итоговый JSON, который уйдет клиенту:
|
|
328
|
+
// {
|
|
329
|
+
// "success": true,
|
|
330
|
+
// "payload": {"id": 1, "name": "John Doe"}
|
|
331
|
+
// }
|
|
332
|
+
```
|
|
260
333
|
|
|
261
|
-
|
|
334
|
+
Единственным условием для досрочного прерывания вызова `postHandler` хуков
|
|
335
|
+
является принудительная отправка HTTP-ответа внутри самого хука с использованием
|
|
336
|
+
нативного объекта `ctx.response`. В таком случае выполнение оставшихся хуков
|
|
337
|
+
прерывается.
|
|
262
338
|
|
|
263
339
|
```js
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
340
|
+
router.defineRoute({
|
|
341
|
+
method: HttpMethod.GET,
|
|
342
|
+
path: '/report',
|
|
343
|
+
handler() {
|
|
344
|
+
// основной обработчик возвращает сырые данные
|
|
345
|
+
return {status: 'pending', id: 123};
|
|
346
|
+
},
|
|
347
|
+
postHandler: [
|
|
348
|
+
(ctx, data) => {
|
|
349
|
+
// принудительная отправка ответа напрямую
|
|
350
|
+
// через нативный объект ServerResponse
|
|
351
|
+
ctx.response.statusCode = 202;
|
|
352
|
+
ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
353
|
+
ctx.response.end('Отчет еще формируется, попробуйте позже.');
|
|
354
|
+
},
|
|
355
|
+
(ctx, data) => {
|
|
356
|
+
// данный хук никогда не будет выполнен, так как ответ
|
|
357
|
+
// уже был отправлен предыдущим хуком
|
|
358
|
+
return {...data, formatted: true};
|
|
359
|
+
},
|
|
360
|
+
],
|
|
268
361
|
});
|
|
362
|
+
```
|
|
269
363
|
|
|
270
|
-
|
|
271
|
-
|
|
364
|
+
### Глобальные хуки
|
|
365
|
+
|
|
366
|
+
Экземпляр маршрутизатора `TrieRouter` позволяет задавать глобальные хуки,
|
|
367
|
+
которые выполняются на различных этапах жизненного цикла.
|
|
368
|
+
|
|
369
|
+
- [`onDefineRoute`](#ondefineroute) выполняется перед регистрацией маршрута;
|
|
370
|
+
- [`preHandler`](#prehandler-глобальный) выполняется перед вызовом обработчика каждого маршрута;
|
|
371
|
+
- [`postHandler`](#posthandler-глобальный) выполняется после вызова обработчика каждого маршрута;
|
|
372
|
+
|
|
373
|
+
Добавить глобальные хуки можно методом маршрутизатора `addHook`.
|
|
374
|
+
|
|
375
|
+
#### onDefineRoute
|
|
376
|
+
|
|
377
|
+
Перед регистрацией каждого маршрута выполняются хуки `onDefineRoute`. Данный
|
|
378
|
+
хук может быть только синхронным. В первый аргумент вызова передается копия
|
|
379
|
+
определения маршрута, а во второй экземпляр сервис-контейнера.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef, container) => {
|
|
383
|
+
// выполняется перед добавлением маршрута
|
|
384
|
+
console.log(routeDef);
|
|
385
|
+
// {
|
|
386
|
+
// method: 'GET',
|
|
387
|
+
// path: '/users',
|
|
388
|
+
// handler() {...}
|
|
389
|
+
// ...
|
|
390
|
+
// }
|
|
272
391
|
});
|
|
273
392
|
|
|
274
|
-
router.
|
|
393
|
+
// router.defineRoute(...)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Возвращаемым значением данного хука может быть модифицированное определение
|
|
397
|
+
маршрута, либо `undefined`. Чтобы изменения параметров маршрута были учтены
|
|
398
|
+
маршрутизатором, требуется передать новое определение в качестве результата.
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef, container) => {
|
|
275
402
|
// позволяет модифицировать определение
|
|
276
|
-
// маршрута в момент регистрации
|
|
403
|
+
// маршрута в момент его регистрации
|
|
277
404
|
routeDef.method = HttpMethod.POST;
|
|
278
405
|
routeDef.path = '/myPath';
|
|
279
406
|
routeDef.handler = () => 'OK';
|
|
407
|
+
// так как аргументом "routeDef" является копия
|
|
408
|
+
// оригинального определения, требуется передать
|
|
409
|
+
// модифицированный аргумент в результат вызова
|
|
410
|
+
return routeDef;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// router.defineRoute(...)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### preHandler (глобальный)
|
|
417
|
+
|
|
418
|
+
Глобальный хук `preHandler` вызывается перед каждым обработчиком маршрута,
|
|
419
|
+
и может быть полезен для аутентификации или других проверок доступа. Хук
|
|
420
|
+
будет вызван только в том случае, если для данного запроса найден
|
|
421
|
+
соответствующий маршрут.
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
425
|
+
// вызывается перед каждым обработчиком маршрута
|
|
426
|
+
const token = ctx.headers['Authorization'];
|
|
427
|
+
if (token === 'secret-key') {
|
|
428
|
+
ctx.state.authenticated = true;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Если глобальный хук `preHandler` возвращает значение отличное от `undefined`,
|
|
434
|
+
то такое значение будет использовано как ответ сервера. При этом, вызов
|
|
435
|
+
следующих хуков и основного обработчика маршрута будет прерван.
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
439
|
+
// вызывается перед каждым обработчиком маршрута
|
|
440
|
+
return 'Hello World!';
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
444
|
+
// данный хук не будет вызван, так как
|
|
445
|
+
// предыдущий уже отправил ответ "Hello World!"
|
|
446
|
+
throw new Error('Should not be called!');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// регистрация маршрута
|
|
450
|
+
router.defineRoute({
|
|
451
|
+
method: HttpMethod.GET,
|
|
452
|
+
path: '/',
|
|
453
|
+
handler() {
|
|
454
|
+
// данный обработчик не будет вызван, так как
|
|
455
|
+
// глобальный хук уже отправил ответ "Hello World!"
|
|
456
|
+
throw new Error('Should not be called!');
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Кроме возвращаемого значения, маршрутизатор отслеживает состояние отправки
|
|
462
|
+
ответа через экземпляр `ServerResponse`. Если сервер уже отправил ответ,
|
|
463
|
+
то вызов следующих хуков и основного обработчика маршрута будет прерван.
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
467
|
+
// отправка ответа через ServerResponse
|
|
468
|
+
ctx.response.statusCode = 200;
|
|
469
|
+
ctx.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
470
|
+
ctx.response.end('OK');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
474
|
+
// данный хук не будет вызван, так как
|
|
475
|
+
// предыдущий уже отправил ответ 200 "OK"
|
|
476
|
+
throw new Error('Should not be called!');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// регистрация маршрута
|
|
480
|
+
router.defineRoute({
|
|
481
|
+
method: HttpMethod.GET,
|
|
482
|
+
path: '/',
|
|
483
|
+
handler() {
|
|
484
|
+
// данный обработчик не будет вызван, так как
|
|
485
|
+
// глобальный хук уже отправил ответ 200 "OK"
|
|
486
|
+
throw new Error('Should not be called!');
|
|
487
|
+
},
|
|
280
488
|
});
|
|
281
489
|
```
|
|
282
490
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
как
|
|
491
|
+
#### postHandler (глобальный)
|
|
492
|
+
|
|
493
|
+
Глобальный хук `postHandler` работает по такому же принципу, как и одноименный
|
|
494
|
+
хук на уровне маршрута, но применяется абсолютно ко всем обработанным запросам.
|
|
495
|
+
Хук принимает контекст запроса первым аргументом, а вторым данные для отправки.
|
|
496
|
+
|
|
497
|
+
```js
|
|
498
|
+
router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
|
|
499
|
+
// GET /hello
|
|
500
|
+
console.log(data); // > Hello World!
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// регистрация маршрута
|
|
504
|
+
router.defineRoute({
|
|
505
|
+
method: HttpMethod.GET,
|
|
506
|
+
path: '/hello',
|
|
507
|
+
handler() {
|
|
508
|
+
return 'Hello World!';
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Глобальные хуки `postHandler` позволяют применять трансформацию ко всем ответам
|
|
514
|
+
маршрутизатора. Это может быть использовано для приведения ответов к единому
|
|
515
|
+
формату, когда результат работы любого маршрута автоматически оборачивается
|
|
516
|
+
в стандартизированную структуру с добавлением метаинформации.
|
|
517
|
+
|
|
518
|
+
```js
|
|
519
|
+
router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
|
|
520
|
+
// обертка ответа в единую структуру
|
|
521
|
+
return {
|
|
522
|
+
meta: {
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
path: ctx.pathname
|
|
525
|
+
},
|
|
526
|
+
data: data
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// регистрация маршрута
|
|
531
|
+
router.defineRoute({
|
|
532
|
+
method: HttpMethod.GET,
|
|
533
|
+
path: '/hello',
|
|
534
|
+
handler() {
|
|
535
|
+
return 'Hello World!';
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// запрос: GET /hello
|
|
540
|
+
// ответ сервера:
|
|
541
|
+
// {
|
|
542
|
+
// "meta": {"timestamp": 1672531200000, "path": "/hello"},
|
|
543
|
+
// "data": "Hello World!"
|
|
544
|
+
// }
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Если внутри хука выполнена отправка HTTP-ответа через методы нативного
|
|
548
|
+
объекта `ctx.response` (например, для перенаправления), то выполнение
|
|
549
|
+
цепочки хуков немедленно прерывается, и все последующие хуки игнорируются.
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
|
|
553
|
+
// если обработчик вернул команду на редирект
|
|
554
|
+
if (data === 'REDIRECT_TO_LOGIN') {
|
|
555
|
+
// принудительная отправка ответа через ServerResponse
|
|
556
|
+
ctx.response.statusCode = 302;
|
|
557
|
+
ctx.response.setHeader('Location', '/login');
|
|
558
|
+
ctx.response.end();
|
|
559
|
+
// на данном этапе выполнение цепочки хуков прекращается
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
return data;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
router.addHook(RouterHookType.POST_HANDLER, (ctx, data) => {
|
|
566
|
+
// этот код не будет выполнен, если предыдущий хук
|
|
567
|
+
// уже отправил ответ клиенту (вызвал ctx.response.end)
|
|
568
|
+
return {result: data};
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// регистрация маршрута
|
|
572
|
+
router.defineRoute({
|
|
573
|
+
method: HttpMethod.GET,
|
|
574
|
+
path: '/dashboard',
|
|
575
|
+
handler() {
|
|
576
|
+
return 'REDIRECT_TO_LOGIN';
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
```
|
|
286
580
|
|
|
287
581
|
### Метаданные маршрута
|
|
288
582
|
|
|
@@ -311,7 +605,7 @@ const router = new TrieRouter();
|
|
|
311
605
|
// перед основным обработчиком каждого маршрута
|
|
312
606
|
router.addHook(RouterHookType.PRE_HANDLER, (ctx) => {
|
|
313
607
|
// доступ к метаданным текущего маршрута
|
|
314
|
-
console.log(ctx.meta); // {foo: 'bar'}
|
|
608
|
+
console.log(ctx.meta); // > {foo: 'bar'}
|
|
315
609
|
});
|
|
316
610
|
|
|
317
611
|
router.defineRoute({
|
|
@@ -410,7 +704,7 @@ adminBranch.defineRoute({
|
|
|
410
704
|
path: '/dashboard',
|
|
411
705
|
handler: (ctx) => {
|
|
412
706
|
// маршрут наследует префикс /admin и метаданные
|
|
413
|
-
console.log(ctx.meta); // {access: 'admin'}
|
|
707
|
+
console.log(ctx.meta); // > {access: 'admin'}
|
|
414
708
|
return 'Dashboard';
|
|
415
709
|
},
|
|
416
710
|
});
|