--- title: '@nocobase/resourcer' order: 2 # toc: menu --- # @nocobase/resourcer ## 介绍 提供数据资源操作方法,可单独使用。 基于资源(resource)和操作方法(action)设计,将 REST 和 RPC 思想融合起来,为资源提供操作方法,执行方法时提供相关参数(params)。 特点: - 可注册的 Middlewares 和 Actions - 提供灵活的多来源 Action Params 合并方案 - 不局限于 Koa 框架 - 不局限于 HTTP 以 filter 为例,某数据表的过滤条件可能来自: - 客户端请求参数 - 视图限定了筛选范围 - 权限也可能限定了筛选范围 - 通过中间件注入定制化的过滤条件 resourcer 提供了非常方便的接口用于处理参数的合并(包括自定义合并或替换规则) 资源有五种类型: - single 独立资源 - hasOne 一对一的关系资源 - hasMany 一对多的关系资源 - belongsTo 一对一的关系资源 - belongsToMany 多对多的关系资源 举例说明: - users:独立资源; - posts:独立资源; - users.profile:关系资源,一对一关系 User.hasOne(Profile),资源为 profile,属于 users; - posts.user:关系资源,多对一关系 Post.belongsTo(User),资源为 user,属于 posts; - users.posts:关系资源,一对多关系 User.hasMany(Post),资源为 posts,属于 users; - posts.tags:关系资源,多对多关系 Post.belongsToMany(Tag),资源为 tags,属于 posts。 资源名称的格式: - resourceName:独立资源 - associatedName.resourceName:关系资源 操作名称的格式: - create:全局操作 - resourceName:create:隶属某个资源的操作 - associatedName.resourceName:create:隶属某个关系资源的操作 这种资源和操作的格式,在 SDK 的协助下,看起来会更加直观,如: ```ts api.resource('users').login({ // 省略具体参数 }); ``` Resourcer 是基于资源设计的,可以看做是 REST 的扩展(兼容的同时,但有些许不同),提供了类 REST 的 Resource Action API([点此查看细节](#resourcerkoarestapimiddleware)),非常灵活,同时弥补了 REST 的一些缺陷。不用 GraphQL 的原因是因为还不够完善,尤其为了追求 GraphQL 需要写很多代码,并不适合作为常规 API 开放给大众。后续如果反应强烈会考虑适配 GraphQL,其他感兴趣的开发也可以自由发挥,来完善 GraphQL API。 ## 安装 ```bash yarn add @nocobase/resourcer ``` ## Usage ```ts import Resourcer from '@nocobase/resourcer'; const resourcer = new Resourcer(); resourcer.registerActions({ async list(ctx, next) { ctx.arr.push(3); await next(); ctx.arr.push(4); }, async create(ctx, next) { ctx.arr.push(5); await next(); ctx.arr.push(6); }, }); resourcer.define({ name: 'users', }); const context = { arr: [], }; await resourcer.execute({ resource: 'users', action: 'list', }, context); console.log(context.arr); // [1,3,4,2] ``` ## API ### context.action.params action.params 分为三类: - 用于定位资源和操作的参数(这些暂时也放在 action.params 里了) - actionName - resourceName - associatedName - 定位资源 ID 相关参数 - resourceKey - associatedKey - request 的 query 和 body 相关参数 - filter - fields - sort - page - perPage - values 对应为 request.body - 其他 query params #### actionName 资源操作名称 #### resourceName 资源名称 #### associatedName 所属资源名称 #### resourceKey 资源 ID #### associatedKey 所属资源 ID #### filter 过滤条件 #### fields 字段 #### sort 排序 #### page 分页 #### perPage 每页显示条数 #### values body 数据 #### 其他自定义参数 待补充... ### context.action.mergeParams 为 action.params 提供的多来源参数合并的方法 ```ts action.mergeParams({ filter: {col1: 'val1'}, }); ``` 后续可能会使用 [deepmerge](https://www.npmjs.com/package/deepmerge) 重构,以便处理更灵活的自定义合并规则。 ### resourcer.define 配置资源 用例: ```ts resourcer.define({ name: 'posts', middlewares: [ async (ctx, next) => { await next(); }, { only: [], except: [], handler: async (ctx, next) => { await next(); }, }, ], actions: { async get(ctx, next) { await next(); }, list: { fields, filter, sort, page, perPage, middlewares: [], handler: async (ctx, next) => { await next(); }, }, }, }); ``` ### resourcer.execute 实验性 注入 context ### resourcer.import 批量导入已配置的资源 ### resourcer.isDefined 判断资源是否已定义 ### resourcer.koaRestApiMiddleware 待完善 原 resourcer.middleware 为 Koa 提供的类 REST API 中间件。提供了标准的 REST API 映射,Resource Action 与 Request Method 的对应关系如下: | Resource Action | Request Method | | :-------------- | :---------------------- | | list | GET \ | | get | GET \ | | create | POST \ | | update | PUT \ | | destroy | DELETE \ | 标准的 REST API 映射下,actionName 可以缺失,但也可以显式声明,当指定 actionName 时,将不受 Request Method 影响。相关 HTTP 格式如下: ```bash # 独立资源 /api/:? /api/:/? # 关系资源 /api///:? /api///:/? # 非 GET 请求时,可以提供 body 数据,一般为 JSON 格式 ``` 为了让大家更理解 Resource Action API 设计,我们接下来举几个具体的例子: ##### 查看文章列表 ```bash GET /api/posts?filter={"col1": "val1"}&fields=col1,col2&sort=-created_at ``` 对应的 context.action.params 为: ```ts { actionName: 'list', resourceName: 'posts', filter: {'col1': 'val1'}, fields: ['col1', 'col2'], sort: ['-created_at'], } ``` ##### 新增文章 ```bash POST /api/posts {"title": "title1"} ``` 对应的 context.action.params 为: ```ts { resourceName: 'posts', actionName: 'create', values: { title: 'title1', }, } ``` ##### 查看文章详情 ```bash GET /api/posts/1?fields=col1,col2 ``` 对应的 context.action.params 为: ```ts { resourceName: 'posts', resourceKey: 1, actionName: 'get', fields: ['col1', 'col2'], } ``` ##### 更新文章 ```bash PUT /api/posts/1 {"title": "title1"} ``` 对应的 context.action.params 为: ```ts { resourceName: 'posts', resourceKey: 1, actionName: 'update', values: { title: 'title1', }, } ``` ##### 删除文章 ```bash DELETE /api/posts/1 ``` 对应的 context.action.params 为: ```ts { resourceName: 'posts', resourceKey: 1, actionName: 'destroy', } ``` ##### 文章评论列表 ```bash GET /api/posts/1/comments?filter={"col1": "val1"}&fields=col1,col2&sort=-created_at ``` 对应的 context.action.params 为: ```ts { associatedName: 'posts', associatedKey: 1, resourceName: 'comments', actionName: 'list', filter: {'col1': 'val1'}, fields: ['col1', 'col2'], sort: ['-created_at'], } ``` ##### 文章评论详情 ```bash GET /api/posts/1/comments/2 ``` 对应的 context.action.params 为: ```ts { associatedName: 'posts', associatedKey: 1, resourceName: 'comments', resourceKey: 2, actionName: 'get', } ``` ##### 显式声明 actionName 的例子 ```bash POST /api/users:login {"username": "admin", "password": "password"} ``` 对应的 context.action.params 为: ```ts { resourceName: 'users', actionName: 'login', values: { username: 'admin', password: 'password', }, } ``` 当不指定 actionName 时,默认为 create,对应的是「新建用户」操作,但是指定 actionName=login 时,就变为了「用户登录」操作了。 在 Resource Action API 的设计理念里,即使类似 login、register、logout 等非标准的 REST API 也可以非常方便的扩展。大家可以更专注于 action 本身,而不必纠结于 request method 和 route 应该如何设计或派发,也不需要考虑 routes 优先级等问题。 ### resourcer.registerAction(name: ActionName, options: ActionOptions) 待完善 原 resourcer.registerActionHandler - name:操作名称 - options:操作配置 可用于注册全局的或某资源特有的 action。 与 resourcer.define 不同,registerAction 的 actionName 支持三种格式: - `` 全局操作 - `:` 某资源特有操作 - `.:` 某关系资源特有操作 更复杂判断条件,需要结合 `resourcer.use` 方法一起处理 示例 ```ts resourcer.registerAction('actionName', async (ctx, next) => { await next(); }); // 带配置 resourcer.registerAction('actionName', { filter, fields, middlewares: [], handler: async (ctx, next) => { await next(); }, }); resourcer.registerAction('resourceName:actionName', async (ctx, next) => { await next(); }); resourcer.registerAction('associatedName.resourceName:actionName', async (ctx, next) => { await next(); }); ``` resourcer 使用 koa-compress 来处理中间件,是一种洋葱圈模型,因此在 action handler 里也不要忘了 `next()`,不然会影响后置逻辑的处理。 更复杂的情况: ```ts resourcer.use(async (ctx, next) => { const { actionName, resourceName } = ctx.action.params; if (actionName === 'foo') { // 其他判断条件 // 不符合条件的 404 处理 ctx.throw(404); } await next(); }); ``` ### resourcer.registerActions(actions) 待完善 原 resourcer.registerActionHandlers 批量注册 actions,用法同 [resourcer.registerAction](#resourcerregisterActionname-actionname-handler-handlertype) 示例: ```ts resourcer.registerActions({ async foo(ctx, next) { await next(); }, async bar(ctx, next) { await next(); }, }); ``` ### resourcer.registerActionMiddleware(actionName, handler) 未实现 为某操作(action)注册特有的 middleware ### resourcer.registerResourceMiddleware(resourceName, options) 未实现 为某资源(resource)注册特有的 middleware ### resourcer.use 注册 resourcer 全局 middleware ```ts resourcer.use(async (ctx, next) => { // code... await next(); }) ``` 虽然大部分框架都提供了中间件,但是中间件的执行顺序(优先级)依赖于编码顺序,这种方式非常不利于插件化管理。因此,在 Resourcer 设计思想里,将中间件做了分层,不同层级的 middlewares 不依赖于编码顺序,而是如下顺序: 1. 首先,koa 层:`koa.use` 2. 其次,resourcer 层:`resourcer.use` 3. 再次,resource 层(每个资源独立):`resourcer.registerActionMiddleware` 4. 最后,action 层:`resourcer.registerResourceMiddleware` 不过,每层的中间件执行顺序还依赖于编码顺序,如有需要再进行更细微的改进。