nocobase/docs/cores/packages/resourcer.md

570 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: '@nocobase/resourcer'
order: 2
# toc: menu
---
# @nocobase/resourcer
## 介绍
提供数据资源操作方法,可单独使用。
基于资源resource和操作方法action设计将 REST 和 RPC 思想融合起来为资源提供操作方法执行方法时提供相关参数params
特点:
- 可注册的 Middlewares 和 Actions
- 提供灵活的多来源 Action Params 合并方案
- 不局限于 Koa 框架
- 不局限于 HTTP
<Alert title="何为多来源 Action Params" type="info">
以 filter 为例,某数据表的过滤条件可能来自:
- 客户端请求参数
- 视图限定了筛选范围
- 权限也可能限定了筛选范围
- 通过中间件注入定制化的过滤条件
resourcer 提供了非常方便的接口用于处理参数的合并(包括自定义合并或替换规则)
</Alert>
资源有五种类型:
- 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({
// 省略具体参数
});
```
<Alert title="为什么不是 REST 或 GraphQL" type="info">
Resourcer 是基于资源设计的,可以看做是 REST 的扩展(兼容的同时,但有些许不同),提供了类 REST 的 Resource Action API[点此查看细节](#resourcerkoarestapimiddleware)),非常灵活,同时弥补了 REST 的一些缺陷。不用 GraphQL 的原因是因为还不够完善,尤其为了追求 GraphQL 需要写很多代码,并不适合作为常规 API 开放给大众。后续如果反应强烈会考虑适配 GraphQL其他感兴趣的开发也可以自由发挥来完善 GraphQL API。
</Alert>
## 安装
```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'},
});
```
<Alert title="注意" type="warning">
后续可能会使用 [deepmerge](https://www.npmjs.com/package/deepmerge) 重构,以便处理更灵活的自定义合并规则。
</Alert>
### 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 <Badge>实验性</Badge>
注入 context
### resourcer.import
批量导入已配置的资源
### resourcer.isDefined
判断资源是否已定义
### resourcer.koaRestApiMiddleware <Badge>待完善</Badge>
原 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 格式如下:
```bash
# 独立资源
<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 设计,我们接下来举几个具体的例子:
##### 查看文章列表
```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 时,就变为了「用户登录」操作了。
<Alert title="注意" type="warning">
在 Resource Action API 的设计理念里,即使类似 login、register、logout 等非标准的 REST API 也可以非常方便的扩展。大家可以更专注于 action 本身,而不必纠结于 request method 和 route 应该如何设计或派发,也不需要考虑 routes 优先级等问题。
</Alert>
### resourcer.registerAction(name: ActionName, options: ActionOptions) <Badge>待完善</Badge>
原 resourcer.registerActionHandler
- name操作名称
- options操作配置
可用于注册全局的或某资源特有的 action。
<Alert title="注意" type="warning">
与 resourcer.define 不同registerAction 的 actionName 支持三种格式:
- `<actionName>` 全局操作
- `<resourceName>:<actionName>` 某资源特有操作
- `<associatedName>.<resourceName>:<actionName>` 某关系资源特有操作
更复杂判断条件,需要结合 `resourcer.use` 方法一起处理
</Alert>
示例
```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();
});
```
<Alert title="注意" type="warning">
resourcer 使用 koa-compress 来处理中间件,是一种洋葱圈模型,因此在 action handler 里也不要忘了 `next()`,不然会影响后置逻辑的处理。
</Alert>
更复杂的情况:
```ts
resourcer.use(async (ctx, next) => {
const { actionName, resourceName } = ctx.action.params;
if (actionName === 'foo') {
// 其他判断条件
// 不符合条件的 404 处理
ctx.throw(404);
}
await next();
});
```
### resourcer.registerActions(actions) <Badge>待完善</Badge>
原 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) <Badge>未实现</Badge>
为某操作action注册特有的 middleware
### resourcer.registerResourceMiddleware(resourceName, options) <Badge>未实现</Badge>
为某资源resource注册特有的 middleware
### resourcer.use
注册 resourcer 全局 middleware
```ts
resourcer.use(async (ctx, next) => {
// code...
await next();
})
```
<Alert title="为什么要提供不同的中间件注册方法?" type="warning">
虽然大部分框架都提供了中间件,但是中间件的执行顺序(优先级)依赖于编码顺序,这种方式非常不利于插件化管理。因此,在 Resourcer 设计思想里,将中间件做了分层,不同层级的 middlewares 不依赖于编码顺序,而是如下顺序:
1. 首先koa 层:`koa.use`
2. 其次resourcer 层:`resourcer.use`
3. 再次resource 层(每个资源独立):`resourcer.registerActionMiddleware`
4. 最后action 层:`resourcer.registerResourceMiddleware`
不过,每层的中间件执行顺序还依赖于编码顺序,如有需要再进行更细微的改进。
</Alert>