路由 (@halsp/router)

添加 @halsp/router 以支持路由功能

  • 支持 HTTP RESTful
  • 支持微服务路由
  • 根据文件系统映射访问路径,彻底解耦无关联功能
  • 按需加载,提升请求速度
  • 轻量化,高可扩展性
  • 移除 controller 层,灵活性更高

安装

npm install @halsp/router

开始使用

在入口文件中添加 startup.useRouter

import "@halsp/router";
startup.useRouter()

src/actions 下创建 test.get.ts 文件和 Action 派生类

import { Action } from "@halsp/router";

export default class extends Action{
  invoke(){
    this.ok();
  }
}

访问 GET /test 即可触发该 Action 中间件

TIP

startup.useRouter 实际上可能会注册多个中间件

路由文件夹

路由文件夹默认为 src/actionssrc/modules (二选一)

一般 src/actions 用于简单项目,src/modules 用于稍大的模块化项目,本教程仅用 src/actions,模块化请参考 模块化项目 教程

在其他文件夹的 actions 不会被正确编译和执行

配置参数

startup.useRouter 接收可选参数 RouterOptions,包含以下字段

  • prefix: 路由前缀,比如统一添加 /api/v3 前缀

路由匹配

路由匹配有两种方式

  1. 请求路径与文件系统路径匹配,通过后缀设置请求方法
  2. 通过装饰器指定请求路径和请求方法

在一个项目中,两种方式可以混用,但装饰器的优先级更高

WARNING

微服务项目只能使用装饰器的方式来匹配路由

用文件系统匹配路由

普通路径与文件系统路径完全相同

作为路由查询参数的文件或文件夹,命名要以 ^ 开头

action 文件应以 .post.ts.get.ts.get.delete.put .ts 等结尾(或其他自定义请求方法)

如果没有请求方法后缀,任意 httpMethod 都可以请求,与 .any.ts 效果相同

例 1

获取 todo list

指定 HttpMethod(建议)

目录结构如下:

+-- actions
|   +-- todo
|       +-- _.get.ts

+-- actions
|   +-- todo.get.ts

访问地址为 GET /todo

任意请求方法

目录结构如下:

+-- actions
|   +-- todo
|       +-- getTodoList.ts

访问地址为 GET /todo/getTodoListPOST /todo/getTodoListPUT /todo/getTodoList 等等,效果相同。

例 2

获取单个 todo item

指定 HttpMethod(建议)

目录结构如下:

+-- actions
|   +-- todo
|       +-- ^id
|           +-- _.get.ts

+-- actions
|   +-- todo
|       +-- ^id.get.ts

访问地址为 GET /todo/66

任意请求方法

目录结构如下:

+-- actions
|   +-- todo
|       +-- getTodoItem.ts

访问地址为 GET(POST 等) /todo/getTodoItem,需要在 bodyheaderquery 传入 todoId 参数

用装饰器匹配路由

通过装饰器指定请求路径和请求方法

和文件系统匹配相比,有更强的灵活性,但需要写更多代码,需要支持装饰器功能

装饰器

提供以下装饰器用于指定 Action 的请求方法和请求路径

  • HttpGet
  • HttpPost
  • HttpPatch
  • HttpDelete
  • HttpPut
  • HttpHead
  • HttpOptions
  • HttpConnect
  • HttpTrace
  • HttpCustom

使用方式

同时指定请求方法和路径

import { HttpGet } from '@halsp/router';

@HttpGet("test/^id")
export default class extends Action {
  async invoke(): Promise<void> {
    this.ok("method");
  }
}

只指定请求方法,路径按文件系统匹配

import { HttpGet } from '@halsp/router';

@HttpGet
export default class extends Action {
  async invoke(): Promise<void> {
    this.ok("method");
  }
}

自定义请求方法

使用 @HttpCustom(method, url) 支持自定义请求方法

关于如何添加自定义请求方法请参考后面的 自定义请求方法

该装饰器接收两个参数

  1. 请求方法
  2. 请求路径,如果不传此参数将按文件系统匹配

Action

Action 也是中间件,该类继承中间件类 Middleware,但 Action 中间件会在 useRouter 中自动注册,无需手动注册

如果有特殊需求,正常情况 Action 会作为最终中间件,不应调用 this.next()

每次收到请求,主要执行的是自定义 Action 类实例对象中的 invoke 函数

所有自定义 Action 都应派生自 Action 类,并重写 invoke 函数

创建一个 Action

根据下面步骤创建一个常规 Action

创建路由文件夹

创建文件夹 src/actions,用于存放所有 Action

路由文件夹也可以是 src/modules,参考前面 路由文件夹 部分

创建 action 文件

根据各业务,创建文件夹和 .ts 文件,名称自定,但名称和路径会映射为访问路径,每个文件对应一个 action

命名格式为 <actionName>.<httpMethod>.ts ,其中 httpMethod 可以多个,如 user.get.tsuser.delete.put.ts

<actionName> 可以是划线 _,即取自父级文件夹名称,如 /user/_.get.post.ts/user.get.post.ts 相同

如果命名没有 <httpMethod> 部分,则任意方法都能访问,与 .any.ts 作用相同,如 user.ts 等同于 user.any.ts

+-- actions
|   +-- type1
|       +-- _.post.ts
|       +-- user.get.ts
|       +-- ...
|   +-- type2
|       +-- _.patch.ts
|       +-- user.delete.ts
|       +-- ^id
|           +-- _.put.ts
|           +-- ...
|   +-- type3.get.post.ts

创建 action 类

在 action 文件 (.ts) 中创建继承 Action 的类,并重写 invoke 函数

import { Action } from "@halsp/router";

export default class extends Action {
  async invoke() {
    this.ok({
      result: 'success'
    });
  }
}

Request.params

Request.params 是 RESTful 路径中的参数,如

  • 路由/文件地址:/user/^userId/todo/^todoId.get.ts
  • 访问路径:/user/66/todo/88

那么 params 值为

{
  "userId": 66,
  "todoId": 88
}

@halsp/router 会在 ctx.req 中添加 params 属性

自定义请求方法

如果内置请求方法不满足需求,可以自定义请求方法,如 ['custom-get','custom-method']

startup
  .useHttp({
    customMethods: ["custom-get", "custom-method"],
  })
  .useRouter();

路由元数据

你可以通过装饰器 @SetActionMetadata(key,value) 装饰 Action,给 Action 添加元数据,添加的元数据可以在解析路由后获取

import { Action } from "@halsp/core"

@SetActionMetadata("roles", ["admin"])
export default class extends Action{}
import "@halsp/router";

startup
  .use(async (ctx, next)=>{
    const role = ctx.actionMetadata.role; // admin
    await next();
  })
  .useRouter();

也可以利用 setActionMetadata 创建自定义装饰器,更便捷的添加元数据

import { setActionMetadata } from "@halsp/router";

function Admin(target: any) {
  setActionMetadata(target, {
    role: 'admin',
  });
}
function Root(target: any) {
  setActionMetadata(target, {
    role: 'root',
  });
}
import { Action } from "@halsp/core"
@Admin
export default class extends Action{}
import { Action } from "@halsp/core"
@Root
export default class extends Action{}

也可以使用 getActionMetadata 获取元数据

import { Action } from "@halsp/core"
import { getActionMetadata } from "@halsp/router";

@Root
export default class extends Action{
  async invoke(){
    const metadata = getActionMetadata(this);
  }
}

WARNING

元数据会被转为 json,因此元数据的值不能是函数等内容,因为其不能被解析为 json

编译

使用 @halsp/cli 的编译命令 halsp build 时,@halsp/router 会扫描路由文件夹并创建映射表

用于快速匹配路由,提升程序启动速度,serverless 项目通过编译路由表,能极大的提升启动速度和响应速度

编译结果

编译会在目标文件夹下生成 halsp-router.config 文件

该文件记录了 @halsp/router 的配置和路由表,请不要手动修改

发布

发布时需要将 halsp-router.config 一同发布,否则程序首次启动会自动重新创建映射表

如果没有 halsp-router.config 文件,对于原生服务 @halsp/native 影响不是很大

但 serverless 项目可能每次请求都会启动一个新程序,即重新创建映射表,将失去编译路由表的优势