* feat: new version of the documentation * feat: add English catalog translation * Update quickstart.md * Update quickstart.zh-CN.md * Update quickstart.zh-CN.md * Update quickstart.zh-CN.md * Update quickstart.zh-CN.md * feat: update quickstart * update doc * update pepository api doc Co-authored-by: ChengLei Shao <chareice@live.com>
23 KiB
| order |
|---|
| 1 |
服务端内核
微服务 - Microservices
为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下:
const { Application } = require('@nocobase/server');
const app = new Application({
// 省略配置信息
});
// 配置一张 users 表
app.collection({
name: 'users',
fields: [
{ type: 'string', name: 'username' },
{ type: 'password', name: 'password' }
],
});
// 解析 argv 参数,终端通过命令行进行不同操作
app.parse(process.argv);
终端运行
# 根据配置生成数据库表结构
node app.js db:sync
# 启动应用
node app.js start --port=3000
相关 users 表的 REST API 就生成了
GET http://localhost:3000/api/users
POST http://localhost:3000/api/users
GET http://localhost:3000/api/users/1
PUT http://localhost:3000/api/users/1
DELETE http://localhost:3000/api/users/1
以上示例,只用了 10 行左右的代码就创建了真实可用的 REST API 服务。除了内置的 REST API 以外,还可以通过 app.actions() 自定义其他操作,如登录、注册、注销等。
app.actions({
async login(ctx, next) {},
async register(ctx, next) {},
async logout(ctx, next) {},
}, {
resourceName: 'users', // 属于 users 资源
});
以上自定义操作的 HTTP API 为:
POST http://localhost:3000/api/users:login
POST http://localhost:3000/api/users:register
POST http://localhost:3000/api/users:logout
自定义的 HTTP API 依旧保持 REST API 的风格,以 <resourceName>:<actionName> 格式表示。实际上 REST API 也可以显式指定 actionName,当指定了 actionName,无所谓使用什么请求方法,如:
# 更新操作
PUT http://localhost:3000/api/users/1
# 等同于
POST http://localhost:3000/api/users:update/1
# 删除操作
DELETE http://localhost:3000/api/users/1
# 等同于
GET http://localhost:3000/api/users:destroy/1
# 等同于
POST http://localhost:3000/api/users:destroy/1
NocoBase 的路由(Resourcer)基于资源(Resource)和操作(Action)设计,将 REST 和 RPC 结合起来,提供更为灵活且统一的 Resource Action API。结合客户端 SDK 是这样的:
const { ClientSDK } = require('@nocobase/sdk');
const api = new ClientSDK({
// 可以适配不同 request
request(params) => Promise.resolve({}),
});
await api.resource('users').list();
await api.resource('users').create();
await api.resource('users').get();
await api.resource('users').update();
await api.resource('users').destroy();
await api.resource('users').login();
await api.resource('users').register();
await api.resource('users').logout();
应用 - Application
NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些必要的 API,这里列一些重点:
app.db:数据库实例,每个 app 都有自己的 db。db.getCollection()数据表/数据集collection.repository数据仓库collection.model数据模型
db.on()添加事件监听,由 EventEmitter 提供db.emit()触发事件,由 EventEmitter 提供db.emitAsync()触发异步事件
app.cli,Commander 实例,提供命令行操作app.context,上下文ctx.dbctx.action当前资源操作实例action.params操作参数action.mergeParams()参数合并方法
app.constructor()初始化app.collection()定义数据 Schema,等同于app.db.collection()app.resource()定义资源app.actions()定义资源的操作方法app.on()添加事件监听,由 EventEmitter 提供app.emit()触发事件,由 EventEmitter 提供app.emitAsync()触发异步事件app.use()添加中间件,由 Koa 提供app.command()自定义命令行,等同于app.cli.command()app.plugin()添加插件app.load()载入配置,主要用于载入插件app.parse()解析 argv 参数,写在最后,等同于app.cli.parseAsync()
数据集 - Collection
NocoBase 通过 app.collection() 方法定义数据的 Schema,Schema 的类型包括:
属性 Attribute
- Boolean 布尔型
- String 字符串
- Text 长文本
- Integer 整数型
- Float 浮点型
- Decimal 货币
- Json/Jsonb/Array 不同数据库的 JSON 类型不一致,存在兼容性问题
- Time 时间
- Date 日期
- Virtual 虚拟字段
- Reference 引用
- Formula 计算公式
- Context 上下文
- Password 密码
- Sort 排序
关系 Association/Realtion
- HasOne 一对一
- HasMany 一对多
- BelongsTo 多对一
- BelongsToMany 多对多
- Polymorphic 多态
比如一个微型博客的表结构可以这样设计:
// 用户
app.collection({
name: 'users',
fields: {
username: { type: 'string', unique: true },
password: { type: 'password', unique: true },
posts: { type: 'hasMany' },
},
});
// 文章
app.collection({
name: 'posts',
fields: {
title: 'string',
content: 'text',
tags: 'belongsToMany',
comments: 'hasMany',
author: { type: 'belongsTo', target: 'users' },
},
});
// 标签
app.collection({
name: 'tags',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'posts' },
],
});
// 评论
app.collection({
name: 'comments',
fields: [
{ type: 'text', name: 'content' },
{ type: 'belongsTo', name: 'user' },
],
});
除了通过 app.collection() 配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有:
collection当前 collection 的数据结构collection.hasField()判断字段是否存在collection.addField()添加字段配置collection.getField()获取字段配置collection.removeField()移除字段配置collection.sync()与数据库表结构同步
collection.repository当前 collection 的数据仓库repository.findMany()repository.findOne()repository.create()repository.update()repository.destroy()repository.relatedQuery().for()create()update()destroy()findMany()findOne()set()add()remove()toggle()
collection.model当前 collection 的数据模型
Collection 示例:
const collection = app.db.getCollection('posts');
collection.hasField('title');
collection.getField('title');
// 添加或更新
collection.addField({
type: 'string',
name: 'content',
});
// 移除
collection.removeField('content');
// 添加、或指定 key path 替换
collection.mergeField({
name: 'content',
type: 'string',
});
除了全局的 `db.sync()`,也有 `collection.sync()` 方法。
await collection.sync();
db:sync 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。db:sync 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。
- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。
- Model 为 Sequelize.Model,详细使用说明可以查看 Sequelize 文档。
- Model 取决于适配的 ORM,Repository 基于 Model 提供统一的接口。
通过 Repository 创建数据
const User = app.db.getCollection('users');
const user = await User.repository.create({
title: 't1',
content: 'c1',
author: 1,
tags: [1,2,3],
}, {
whitelist: [],
blacklist: [],
});
await User.repository.findMany({
filter: {
title: 't1',
},
fields: ['id', 'title', 'content'],
sort: '-created_at',
page: 1,
perPage: 20,
});
await User.repository.findOne({
filter: {
title: 't1',
},
fields: ['id', 'title', 'content'],
sort: '-created_at',
page: 1,
perPage: 20,
});
await User.repository.update({
title: 't1',
content: 'c1',
author: 1,
tags: [1,2,3],
}, {
filter: {},
whitelist: [],
blacklist: [],
});
await User.repository.destroy({
filter: {},
});
通过 Model 创建数据
const User = db.getCollection('users');
const user = await User.model.create({
title: 't1',
content: 'c1',
});
资源 & 操作 - Resource & Action
Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 <resourceName>:<actionName> 格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 <associatedName>.<resourceName>:<actionName>。
Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有:
usersusers.postspostsposts.tagsposts.commentsposts.authortagstags.postscommentscomments.user
- Collection 定义数据的 schema(结构和关系)
- Resource 定义数据的 action(操作方法)
- Resource 请求和响应的数据结构由 Collection 定义
- Collection 默认自动同步给 Resource
- Resource 的概念更大,除了对接 Collection 以外,也可以对接外部数据或其他自定义
资源相关 API 有:
app.resource()app.actions()ctx.action
一个资源可以有多个操作。
// 数据类
app.resource({
name: 'users',
actions: {
async list(ctx, next) {},
async get(ctx, next) {},
async create(ctx, next) {},
async update(ctx, next) {},
async destroy(ctx, next) {},
},
});
// 非数据类
app.resource({
name: 'server',
actions: {
// 获取服务器时间
getTime(ctx, next) {},
// 健康检测
healthCheck(ctx, next) {},
},
});
常规操作可以用于不同资源
app.actions({
async list(ctx, next) {},
async get(ctx, next) {},
async create(ctx, next) {},
async update(ctx, next) {},
async destroy(ctx, next) {},
}, {
// 不指定 resourceName 时,全局共享
resourceNames: ['posts', 'comments', 'users'],
});
在资源内部定义的 action 不会共享,常规类似增删改查的操作建议设置为全局,app.resource() 只设置参数,如:
app.resource({
name: 'users',
actions: {
list: {
fields: ['id', 'username'], // 只输出 id 和 username 字段
filter: {
'username.$ne': 'admin', // 数据范围筛选过滤 username != admin
},
sort: ['-created_at'], // 创建时间倒序
perPage: 50,
},
get: {
fields: ['id', 'username'], // 只输出 id 和 username 字段
filter: {
'username.$ne': 'admin', // 数据范围筛选过滤 username != admin
},
},
create: {
fields: ['username'], // 白名单
},
update: {
fields: ['username'], // 白名单
},
destroy: {
filter: { // 不能删除 admin
'username.$ne': 'admin',
},
},
},
});
// app 默认已经内置了 list, get, create, update, destroy 操作
app.actions({
async list(ctx, next) {},
async get(ctx, next) {},
async create(ctx, next) {},
async update(ctx, next) {},
async destroy(ctx, next) {},
});
在 Middleware Handler 和 Action Handler 里,都可以通过 ctx.action 获取到当前 action 实例,提供了两个非常有用的 API:
ctx.action.params:获取操作对应的参数ctx.action.mergeParams():处理多来源参数合并
ctx.action.params 有:
- 定位资源和操作
actionNameresourceNameassociatedName
- 定位资源 ID
resourceIdassociatedId
- request query
filterfieldssortpageperPage- 其他 query 值
- request body
values
示例:
async function (ctx, next) {
const { resourceName, resourceId, filter, fields } = ctx.action.params;
// ...
}
ctx.action.mergeParams() 主要用于多来源参数合并,以 filter 参数为例。如:客户端请求日期 2021-09-15 创建的文章
GET /api/posts:list?filter={"created_at": "2021-09-15"}
资源设置锁定只能查看已发布的文章
app.resource({
name: 'posts',
actions: {
list: {
filter: { status: 'publish' }, // 只能查看已发布文章
},
},
})
权限设定,只能查看自己创建的文章
app.use(async (ctx, next) => {
const { resourceName, actionName } = ctx.action.params;
if (resourceName === 'posts' && actionName === 'list') {
ctx.action.mergeParams({
filter: {
created_by_id: ctx.state.currentUser.id,
},
});
}
await next();
});
以上客户端、资源配置、中间件内我们都指定了 filter 参数,三个来源的参数最终会合并在一起作为最终的过滤条件:
async function list(ctx, next) {
// list 操作中获取到的 filter
console.log(ctx.params.filter);
// filter 是特殊的 and 合并
// {
// and: [
// { created_at: '2021-09-15' },
// { status: 'publish' },
// { created_by_id: 1, }
// ]
// }
}
事件 - Event
在操作执行前、后都放置了相关事件监听器,可以通过 app.db.on() 和 app.on() 添加。区别在于:
app.db.on()添加数据库层面的监听器app.on()添加服务器应用层面的监听器
以 users:login 为例,在数据库里为「查询」操作,在应用里为「登录」操作。也就是说,如果需要记录登录操作日志,要在 app.on() 里处理。
// 创建数据时,执行 User.create() 时触发
app.db.on('users.beforeCreate', async (model) => {});
// 客户端 `POST /api/users:login` 时触发
app.on('users.beforeLogin', async (ctx, next) => {});
// 客户端 `POST /api/users` 时触发
app.on('users.beforeCreate', async (ctx, next) => {});
中间件 - Middleware
Server Application 基于 Koa,所有 Koa 的插件(中间件)都可以直接使用,可以通过 app.use() 添加。如:
const responseTime = require('koa-response-time');
app.use(responseTime());
app.use(async (ctx, next) => {
await next();
});
与 koa.use(middleware) 略有不同,app.use(middleware, options) 多了个 options 参数,可以用于限定 resource 和 action,也可以用于控制中间件的插入位置。
import { middleware } from '@nocobase/server';
app.use(async (ctx, next) => {}, {
name: 'middlewareName1',
resourceNames: [], // 作用于资源内所有 actions
actionNames: [
'list', // 全部 list action
'users:list', // 仅 users 资源的 list action,
],
insertBefore: '',
insertAfter: '',
});
命令行 - CLI
Application 除了可以做 HTTP Server 以外,也是 CLI(内置了 Commander)。目前内置的命令有:
init初始化db:sync --force用于配置与数据库表结构同步start --port启动应用plugin:**插件相关
自定义:
app.command('foo').action(async () => {
console.log('foo...');
});
插件 - Plugin
上文,讲述了核心的扩展接口,包括但不局限于:
- Database/Collection
app.dbdatabase 实例app.collection()等同于app.db.collection()
- Resource/Action
app.resource()等同于app.resourcer.define()app.actions()等同于app.resourcer.registerActions()
- Hook/Event
app.on()添加服务器监听器app.db.on()添加数据库监听器
- Middleware
app.use()添加中间件
- CLI
app.clicommander 实例app.command()等同于app.cli.command()
基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 app.plugin() 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如:
最简单的插件
app.plugin(function pluginName1() {
});
这种方式添加的插件会直接载入,无需安装。
JSON 风格
const plugin = app.plugin({
enable: false, // 默认为 true,不需要启用时可以禁用。
name: 'plugin-name1',
displayName: '插件名称',
version: '1.2.3',
dependencies: {
pluginName2: '1.x',
pluginName3: '1.x',
},
async install() {},
async upgrade() {},
async activate() {},
async bootstrap() {},
async deactivate() {},
async unstall() {},
});
// 通过 api 激活插件
plugin.activate();
OOP 风格
class MyPlugin extends Plugin {
async install() {}
async upgrade() {}
async bootstrap() {}
async activate() {}
async deactivate() {}
async unstall() {}
}
app.plugin(MyPlugin);
// 或
app.plugin({
name: 'plugin-name1',
displayName: '插件名称',
version: '1.2.3',
dependencies: {
pluginName2: '1.x',
pluginName3: '1.x',
},
plugin: MyPlugin,
});
引用独立的 Package
app.plugin('@nocobase/plugin-action-logs');
插件信息也可以直接写在 package.json 里
{
name: 'pluginName1',
displayName: '插件名称',
version: '1.2.3',
dependencies: {
pluginName2: '1.x',
pluginName3: '1.x',
},
}
插件 CLI
plugin:install pluginName1
plugin:unstall pluginName1
plugin:activate pluginName1
plugin:deactivate pluginName1
目前已有的插件:
- @nocobase/plugin-collections 提供数据表配置接口,可通过 HTTP API 管理数据表。
- @nocobase/plugin-action-logs 操作日志
- @nocobase/plugin-automations 自动化(未升级 v0.5,暂不能使用)
- @nocobase/plugin-china-region 中国行政区
- @nocobase/plugin-client 提供客户端,无代码的可视化配置界面,需要与 @nocobase/client 配合使用
- @nocobase/plugin-export 导出
- @nocobase/plugin-file-manager 文件管理器
- @nocobase/plugin-permissions 角色和权限
- @nocobase/plugin-system-settings 系统配置
- @nocobase/plugin-ui-router 前端路由配置
- @nocobase/plugin-ui-schema ui 配置
- @nocobase/plugin-users 用户模块
测试 - Testing
有代码就需要测试,@nocobase/test 提供了 mockDatabase 和 mockServer 用于数据库和服务器的测试,如:
import { mockServer, MockServer } from '@nocobase/test';
describe('mock server', () => {
let api: MockServer;
beforeEach(() => {
api = mockServer({
dataWrapping: false,
});
api.actions({
list: async (ctx, next) => {
ctx.body = [1, 2];
await next();
},
});
api.resource({
name: 'test',
});
});
afterEach(async () => {
return api.destroy();
});
it('agent.get', async () => {
const response = await api.agent().get('/test');
expect(response.body).toEqual([1, 2]);
});
it('agent.resource', async () => {
const response = await api.agent().resource('test').list();
expect(response.body).toEqual([1, 2]);
});
});
客户端 - Client
为了让更多非开发人员也能参与进来,NocoBase 提供了配套的客户端插件 —— 无代码的可视化配置界面。客户端插件需要与 @nocobase/client 配合使用,可以直接使用,也可以自行改造。
插件配置
app.plugin('@nocobase/plugin-client', {
// 自定义 dist 路径
dist: path.resolve(__dirname, './node_modules/@nocobase/client/app'),
});
为了满足各类场景需求,客户端 @nocobase/client 提供了丰富的基础组件:
- Action - 操作
- Action.Window 当前浏览器窗口/标签里打开
- Action.Drawer 打开抽屉(默认右侧划出)
- Action.Modal 打开对话框
- Action.Dropdown 下拉菜单
- Action.Popover 气泡卡片
- Action.Group 按钮分组
- Action.Bar 操作栏
- AddNew 「添加」模块
- AddNew.CardItem - 添加区块
- AddNew.PaneItem - 添加区块(查看面板,与当前查看的数据相关)
- AddNew.FormItem - 添加字段
- BlockItem/CardItem/FormItem - 装饰器
- BlockItem - 普通装饰器(无包装效果)
- CardItem - 卡片装饰器
- FormItem - 字段装饰器
- Calendar - 日历
- Cascader - 级联选择
- Chart - 图表
- Checkbox - 勾选
- Checkbox.Group - 多选框
- Collection - 数据表配置
- Collection.Field - 数据表字段
- ColorSelect - 颜色选择器
- DatePicker - 日期选择器
- DesignableBar - 配置工具栏
- Filter - 筛选器
- Form - 表单
- Grid - 栅格布局
- IconPicker - 图标选择器
- Input - 输入框
- Input.TextArea - 多行输入框
- InputNumber - 数字框
- Kanban - 看板
- ListPicker - 列表选择器(用于选择、展示关联数据)
- Markdown 编辑器
- Menu - 菜单
- Password - 密码
- Radio - 单选框
- Select - 选择器
- Table - 表格
- Tabs - 标签页
- TimePicker - 时间选择器
- Upload - 上传
可以自行扩展组件,以上组件基于 Formily 构建,怎么自定义组件大家查看相关组件源码或 Formily 文档,这里说点不一样的。
- 如何扩展数据库字段?
- 如何将第三方区块添加到 AddNew 模块中?
- 如何在操作栏里添加更多的内置操作?
- 如何自定义配置工具栏?
除了组件具备灵活的扩展以外,客户端也可以在任意前端框架中使用,可以自定义 Request 和 Router,如:
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ClientSDK, Application } from '@nocobase/client';
// 初始化 client 实例
const client = new ClientSDK({
request: (options) => Promise.resolve({}),
});
// 适配 Route Component
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,
AuthLayout,
RouteSchemaRenderer,
},
});
ReactDOM.render(
,
document.getElementById('root'),
);
更多细节,可以通过 create-nocobase-app 初始化项目脚手架并体验。
yarn create nocobase-app my-nocobase-project
nocobase-app 默认使用 umijs 作为项目构建工具,并集成了 Server 作数据接口,初始化的目录结构如下:
|- src
|- pages
|- apis
|- .env
|- .umirc.ts
|- package.json
场景 - Cases
小型管理信息系统,具备完整的前后端。
API 服务,无客户端,提供纯后端接口。
小程序 + 后台管理,只需要一套数据库,但有两套用户和权限,一套用于后台用户,一套用于小程序用户。
SaaS 服务(共享用户),每个应用有自己配套的数据库,各应用数据完全隔离。应用不需要用户和权限模块,SaaS 主站全局共享了。
SaaS 服务(独立用户),每个应用有自己的独立用户模块和权限,应用可以绑定自己的域名。