23 KiB
title | toc |
---|---|
NocoBase | menu |
NocoBase
NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。
微服务 - Microservices
为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下:
const { Application } = require('@nocobase/server');
const app = new Application({
// 省略配置信息
});
// 配置一张 users 表
app.collection({
name: 'users',
schema: [
{ 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.schema
数据结构collection.repository
数据仓库collection.model
数据模型
db.on()
添加事件监听,由 EventEmitter 提供db.emit()
触发事件,由 EventEmitter 提供db.emitAsync()
触发异步事件
app.cli
,Commander 实例,提供命令行操作app.context
,上下文ctx.db
ctx.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',
schema: {
username: { type: 'string', unique: true },
password: { type: 'password', unique: true },
posts: { type: 'hasMany' },
},
});
// 文章
app.collection({
name: 'posts',
schema: {
title: 'string',
content: 'text',
tags: 'belongsToMany',
comments: 'hasMany',
author: { type: 'belongsTo', target: 'users' },
},
});
// 标签
app.collection({
name: 'tags',
schema: [
{ type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'posts' },
],
});
// 评论
app.collection({
name: 'comments',
schema: [
{ type: 'text', name: 'content' },
{ type: 'belongsTo', name: 'user' },
],
});
除了通过 app.collection()
配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有:
collection.schema
当前 collection 的数据结构schema.has()
判断是否存在schema.get()
获取schema.set()
添加或更新schema.merge()
添加、或指定 key path 替换schema.replace()
替换schema.delete()
删除
collection.repository
当前 collection 的数据仓库repository.findAll()
repository.findOne()
repository.create()
repository.update()
repository.destroy()
collection.model
当前 collection 的数据模型
Schema 示例:
const collection = app.db.getCollection('posts');
collection.schema.has('title');
collection.schema.get('title');
// 添加或更新
collection.schema.set('content', {
type: 'string',
});
// 移除
collection.schema.delete('content');
// 添加、或指定 key path 替换
collection.schema.merge({
content: {
type: 'content',
},
});
除了全局的 `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 repository = app.db.getRepository('users');
const user = await repository.create({
title: 't1',
content: 'c1',
author: 1,
tags: [1,2,3],
}, {
whitelist: [],
blacklist: [],
});
await repository.findAll({
filter: {
title: 't1',
},
fields: ['id', 'title', 'content'],
sort: '-created_at',
page: 1,
perPage: 20,
});
await repository.findOne({
filter: {
title: 't1',
},
fields: ['id', 'title', 'content'],
sort: '-created_at',
page: 1,
perPage: 20,
});
await repository.update({
title: 't1',
content: 'c1',
author: 1,
tags: [1,2,3],
}, {
filter: {},
whitelist: [],
blacklist: [],
});
await repository.destroy({
filter: {},
});
通过 Model 创建数据
const User = db.getModel('users');
const user = await User.create({
title: 't1',
content: 'c1',
});
await user.updateAssociations({
author: 1,
tags: [1,2,3],
});
资源 & 操作 - Resource & Action
Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 <resourceName>:<actionName>
格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 <associatedName>.<resourceName>:<actionName>
。
Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有:
users
users.posts
posts
posts.tags
posts.comments
posts.author
tags
tags.posts
comments
comments.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
有:
- 定位资源和操作
actionName
resourceName
associatedName
- 定位资源 ID
resourceId
associatedId
- request query
filter
fields
sort
page
perPage
- 其他 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.db
database 实例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.cli
commander 实例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 服务(独立用户),每个应用有自己的独立用户模块和权限,应用可以绑定自己的域名。