tachybase_todo/docs/cores/packages/resourcer.md

12 KiB
Raw Blame History

title order
@nocobase/resourcer 2

@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 的协助下,看起来会更加直观,如:

api.resource('users').login({
  // 省略具体参数
});

Resourcer 是基于资源设计的,可以看做是 REST 的扩展(兼容的同时,但有些许不同),提供了类 REST 的 Resource Action API点此查看细节),非常灵活,同时弥补了 REST 的一些缺陷。不用 GraphQL 的原因是因为还不够完善,尤其为了追求 GraphQL 需要写很多代码,并不适合作为常规 API 开放给大众。后续如果反应强烈会考虑适配 GraphQL其他感兴趣的开发也可以自由发挥来完善 GraphQL API。

安装

yarn add @nocobase/resourcer

Usage

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 提供的多来源参数合并的方法

action.mergeParams({
  filter: {col1: 'val1'},
});

后续可能会使用 deepmerge 重构,以便处理更灵活的自定义合并规则。

resourcer.define

配置资源

用例:

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 <collection URL>
get GET <resource URL>
create POST <collection URL>
update PUT <resource URL>
destroy DELETE <resource URL>

标准的 REST API 映射下actionName 可以缺失,但也可以显式声明,当指定 actionName 时,将不受 Request Method 影响。相关 HTTP 格式如下:

# 独立资源
<requestMethod> /api/<resourceName>:<actionName>?<queryString>
<requestMethod> /api/<resourceName>:<actionName>/<resourceKey>?<queryString>

# 关系资源
<requestMethod> /api/<associatedName>/<associatedKey>/<resourceName>:<actionName>?<queryString>
<requestMethod> /api/<associatedName>/<associatedKey>/<resourceName>:<actionName>/<resourceKey>?<queryString>

<body> # 非 GET 请求时,可以提供 body 数据,一般为 JSON 格式

为了让大家更理解 Resource Action API 设计,我们接下来举几个具体的例子:

查看文章列表
GET /api/posts?filter={"col1": "val1"}&fields=col1,col2&sort=-created_at

对应的 context.action.params 为:

{
  actionName: 'list',
  resourceName: 'posts',
  filter: {'col1': 'val1'},
  fields: ['col1', 'col2'],
  sort: ['-created_at'],
}
新增文章
POST /api/posts

{"title": "title1"}

对应的 context.action.params 为:

{
  resourceName: 'posts',
  actionName: 'create',
  values: {
    title: 'title1',
  },
}
查看文章详情
GET /api/posts/1?fields=col1,col2

对应的 context.action.params 为:

{
  resourceName: 'posts',
  resourceKey: 1,
  actionName: 'get',
  fields: ['col1', 'col2'],
}
更新文章
PUT /api/posts/1

{"title": "title1"}

对应的 context.action.params 为:

{
  resourceName: 'posts',
  resourceKey: 1,
  actionName: 'update',
  values: {
    title: 'title1',
  },
}
删除文章
DELETE /api/posts/1

对应的 context.action.params 为:

{
  resourceName: 'posts',
  resourceKey: 1,
  actionName: 'destroy',
}
文章评论列表
GET /api/posts/1/comments?filter={"col1": "val1"}&fields=col1,col2&sort=-created_at

对应的 context.action.params 为:

{
  associatedName: 'posts',
  associatedKey: 1,
  resourceName: 'comments',
  actionName: 'list',
  filter: {'col1': 'val1'},
  fields: ['col1', 'col2'],
  sort: ['-created_at'],
}
文章评论详情
GET /api/posts/1/comments/2

对应的 context.action.params 为:

{
  associatedName: 'posts',
  associatedKey: 1,
  resourceName: 'comments',
  resourceKey: 2,
  actionName: 'get',
}
显式声明 actionName 的例子
POST /api/users:login

{"username": "admin", "password": "password"}

对应的 context.action.params 为:

{
  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 支持三种格式:

  • <actionName> 全局操作
  • <resourceName>:<actionName> 某资源特有操作
  • <associatedName>.<resourceName>:<actionName> 某关系资源特有操作

更复杂判断条件,需要结合 resourcer.use 方法一起处理

示例

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(),不然会影响后置逻辑的处理。

更复杂的情况:

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

示例:

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

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

不过,每层的中间件执行顺序还依赖于编码顺序,如有需要再进行更细微的改进。