---
title: NocoBase
toc: menu
---

# NocoBase

NocoBase 采用微内核架构，框架只保留核心，各类功能以插件形式扩展。

<img src="./NocoBase.png" style="max-width: 800px; width: 100%;">

## 微服务 - Microservices

为了更快的理解 NocoBase，我们先创建一个应用，新建一个 app.js 文件，代码如下：

```ts
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);
```

终端运行

```bash
# 根据配置生成数据库表结构
node app.js db:sync
# 启动应用
node app.js start --port=3000
```

相关 users 表的 REST API 就生成了

```bash
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()` 自定义其他操作，如登录、注册、注销等。

```ts
app.actions({
  async login(ctx, next) {},
  async register(ctx, next) {},
  async logout(ctx, next) {},
}, {
  resourceName: 'users', // 属于 users 资源
});
```

以上自定义操作的 HTTP API 为：

```bash
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`，无所谓使用什么请求方法，如：

```bash
# 更新操作
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 是这样的：

```ts
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.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 多态

比如一个微型博客的表结构可以这样设计：

```ts
// 用户
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 示例：

```ts
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 创建数据

```ts
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 创建数据

```ts
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，可以提炼的资源有：

- `users`
- `users.posts`
- `posts`
- `posts.tags`
- `posts.comments`
- `posts.author`
- `tags`
- `tags.posts`
- `comments`
- `comments.user`

<Alert title="Collection 和 Resource 的关系与区别" type="warning">

- Collection 定义数据的 schema（结构和关系）
- Resource 定义数据的 action（操作方法）
- Resource 请求和响应的数据结构由 Collection 定义
- Collection 默认自动同步给 Resource
- Resource 的概念更大，除了对接 Collection 以外，也可以对接外部数据或其他自定义

</Alert>

资源相关 API 有：

- `app.resource()`
- `app.actions()`
- `ctx.action`

一个资源可以有多个操作。

```ts
// 数据类
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) {},
  },
});
```

常规操作可以用于不同资源

```ts
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()` 只设置参数，如：

```ts
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`

示例：

```ts
async function (ctx, next) {
  const { resourceName, resourceId, filter, fields } = ctx.action.params;
  // ...
}
```

`ctx.action.mergeParams()` 主要用于多来源参数合并，以 `filter` 参数为例。如：客户端请求日期 2021-09-15 创建的文章

```bash
GET /api/posts:list?filter={"created_at": "2021-09-15"}
```

资源设置锁定只能查看已发布的文章

```ts
app.resource({
  name: 'posts',
  actions: {
    list: {
      filter: { status: 'publish' }, // 只能查看已发布文章
    },
  },
})
```

权限设定，只能查看自己创建的文章

```ts
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 参数，三个来源的参数最终会合并在一起作为最终的过滤条件：

```ts
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()` 里处理。

```ts
// 创建数据时，执行 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()` 添加。如：

```ts
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，也可以用于控制中间件的插入位置。

```ts
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:**` 插件相关

自定义：

```ts
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()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载，不需要的流程可缺失。如：

**最简单的插件**

```ts
app.plugin(function pluginName1() {

});
```

这种方式添加的插件会直接载入，无需安装。

**JSON 风格**

```ts
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 风格**

```ts
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**

```ts
app.plugin('@nocobase/plugin-action-logs');
```

插件信息也可以直接写在 `package.json` 里

```js
{
  name: 'pluginName1',
  displayName: '插件名称',
  version: '1.2.3',
  dependencies: {
    pluginName2: '1.x', 
    pluginName3: '1.x',
  },
}
```

**插件 CLI**

```bash
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 用于数据库和服务器的测试，如：

```ts
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 配合使用，可以直接使用，也可以自行改造。

插件配置

```ts
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，如：

<pre lang="tsx">
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(
  <ClientProvider client={client}>
    <MemoryRouter initialEntries={['/admin']}>
      <RouteSwitch routes={[]}/>
    </MemoryRouter>
  </ClientProvider>,
  document.getElementById('root'),
);
</pre>

更多细节，可以通过 `create-nocobase-app` 初始化项目脚手架并体验。

```bash
yarn create nocobase-app my-nocobase-project
```

nocobase-app 默认使用 umijs 作为项目构建工具，并集成了 Server 作数据接口，初始化的目录结构如下：

```bash
|- src
  |- pages
  |- apis
|- .env
|- .umirc.ts
|- package.json
```

## 场景 - Cases

小型管理信息系统，具备完整的前后端。

<img src="./MiniMIS.png" style="max-width: 300px; width: 100%;">

API 服务，无客户端，提供纯后端接口。

<img src="./API.png" style="max-width: 280px; width: 100%;">

小程序 + 后台管理，只需要一套数据库，但有两套用户和权限，一套用于后台用户，一套用于小程序用户。

<img src="./MiniProgram.png" style="max-width: 600px; width: 100%;">

SaaS 服务（共享用户），每个应用有自己配套的数据库，各应用数据完全隔离。应用不需要用户和权限模块，SaaS 主站全局共享了。

<img src="./SaaS2.png" style="max-width: 450px; width: 100%;">

SaaS 服务（独立用户），每个应用有自己的独立用户模块和权限，应用可以绑定自己的域名。

<img src="./SaaS1.png" style="max-width: 450px; width: 100%;">
