NocoBase 的核心思想是“一切皆插件”。所有功能,包括核心功能,都是通过插件实现的。这种架构使得 NocoBase 具有极高的可扩展性和可定制性。插件可以用来添加新的功能、修改现有功能、集成第三方服务等等。
通过开发插件,你可以:
- 扩展数据模型:创建新的数据表 (Collections) 和字段 (Fields)。
- 创建自定义页面:使用
UI Schema或纯 React 组件构建新的前端页面。 - 添加 API 接口:定义新的后端 API (Resources and Actions) 或扩展已有的 API。
- 定制用户界面:添加新的区块、操作按钮、配置项等。
- 集成第三方服务:连接外部数据源、支付网关、消息服务等。
本指南将带你从零开始,学习 NocoBase 插件开发的完整流程。
在开始插件开发之前,请确保你已经有一个可以正常运行的 NocoBase 开发环境。推荐使用 Git 源码方式安装,这样可以方便地管理和开发你的插件。
NocoBase 支持三种方式来组织和加载插件,它们最终都会被加载到项目根目录的 node_modules 中:
-
packages/plugins(推荐用于开发) 这是开发插件时最推荐的方式。将你的插件源码放置在此目录下,NocoBase 会通过yarn workspace来管理。这种方式可以让你直接调试源码,并且yarn install会自动处理所有依赖。|- /packages/ |- /plugins/ |- /@my-scope/ |- /plugin-my-first-plugin/ |- /my-other-plugin/ -
storage/plugins用于存放已经编译好的、即插即用的插件。通过 NocoBase 后台 UI 上传的插件包就会被解压到这里。这种方式适合分发和使用已经开发完成的插件。 -
package.json的dependencies你也可以像安装普通的 npm 包一样,将 NocoBase 插件添加到主项目的package.json中。NocoBase 的许多核心插件就是通过这种方式加载的。
我们将通过一个简单的 "Hello World" 插件来快速了解开发、激活和测试的完整流程。
NocoBase 提供了强大的脚手架命令来快速创建一个插件骨架。
# 在你的 NocoBase 项目根目录下执行
yarn pm create @my-project/plugin-hello执行完毕后,你会在 packages/plugins/@my-project/plugin-hello 目录下看到新创建的插件。其目录结构如下:
|- /plugin-hello
|- /src
|- /client # 插件客户端代码 (前端)
|- index.tsx
|- /server # 插件服务端代码 (后端)
|- plugin.ts
|- package.json # 插件包信息
...
创建插件后,你需要使用 pm add 命令将其注册到 NocoBase 应用中,这样应用才能识别它。
yarn pm add @my-project/plugin-hello现在,访问你的 NocoBase 后台(默认为 http://localhost:13000/admin),进入 "插件管理" -> "本地插件",你应该能看到刚刚创建的 "hello" 插件。
让我们来定义一个名为 hello 的数据表。
创建文件 src/server/collections/hello.ts:
import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'hello',
fields: [
{
type: 'string',
name: 'name',
uiSchema: {
title: '名称',
x-component: 'Input'
}
}
],
});然后,修改 src/server/plugin.ts,在插件加载时,允许公开访问 hello 表的所有操作。
import { Plugin } from '@nocobase/server';
export class PluginHelloServer extends Plugin {
async load() {
// 允许任何人对 'hello' 表进行任何操作
// 注意:在生产环境中应使用更精细的权限控制
this.app.acl.allow('hello', '*', 'public');
}
}
export default PluginHelloServer;新插件默认是未激活状态。你可以通过两种方式激活:
通过命令行:
yarn pm enable @my-project/plugin-hello通过界面: 访问 "插件管理" -> "本地插件",找到 "hello" 插件,点击 "激活" 按钮。
插件激活时,NocoBase 会自动运行数据库迁移,将你在 collections 目录下定义的表同步到数据库中。
首先,确保你的 NocoBase 应用正在运行。
# 开发模式
yarn dev然后,使用 curl 或任何 API 测试工具来向 hello 表中插入和查询数据。
创建数据:
curl --location --request POST 'http://localhost:13000/api/hello:create' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Hello NocoBase!"
}'查询数据:
curl --location --request GET 'http://localhost:13000/api/hello:list'如果一切顺利,你应该能看到返回的数据。
src/server/plugin.ts 文件定义了插件的后端主类,它继承自 @nocobase/server 的 Plugin。这个类是插件的入口,提供了一系列生命周期方法让你可以在应用的不同阶段执行代码。
import { Plugin } from '@nocobase/server';
export class MyPluginServer extends Plugin {
// 插件被 pm add 后执行
async afterAdd() {}
// 所有插件实例创建后,load 前执行
async beforeLoad() {}
// 核心加载逻辑,在此定义 Resource、Action 等
async load() {}
// 插件首次激活时执行,用于初始化数据等
async install() {}
// 插件被启用后执行
async afterEnable() {}
// 插件被禁用后执行
async afterDisable() {}
// 插件被移除前执行
async remove() {}
}在插件类中,你可以通过 this.app 访问核心应用实例,通过 this.db 访问数据库实例。
在 NocoBase 中,数据模型被称为 Collection。你可以在插件的 src/server/collections 目录下创建 .ts 文件来定义新的数据表。每个文件默认导出一个 Collection 定义对象。
一个 Collection 主要由 name 和 fields 组成。fields 数组定义了表的字段。
字段类型 (type):
- 属性类型:
string,text,integer,date,boolean,json等。 - 关联类型:
hasOne,hasMany,belongsTo,belongsToMany。
UI Schema (uiSchema):
uiSchema 用于定义字段在前端界面上的表现,例如使用哪个组件 (x-component)、标题 (title)、校验规则 (x-validator) 等。
假设我们要创建一个简单的博客系统,包含 posts (文章) 和 tags (标签) 两个数据表,它们之间是多对多的关系。
src/server/collections/posts.ts:
import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'text', name: 'content' },
{
type: 'belongsToMany',
name: 'tags',
},
],
});src/server/collections/tags.ts:
import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'tags',
fields: [
{ type: 'string', name: 'name' },
{
type: 'belongsToMany',
name: 'posts',
},
],
});NocoBase 会自动创建第三张中间表 posts_tags 来维护这个多对多关系。
NocoBase 会自动将你定义的每个 Collection 映射为一个 API Resource,并内置了 create, list, get, update, destroy 等标准 Actions。这就是为什么我们上一步可以直接通过 /api/hello:create 来访问 API。
你可以通过 app.resourcer 或 app.actions 来定义新的 Action 或覆盖已有的 Action。
示例:覆盖 create 操作,自动关联当前用户
// 在 plugin.ts 的 load 方法中
this.app.actions({
async ['posts:create'](ctx, next) {
// 强制将创建者 ID 设置为当前登录用户
ctx.action.mergeParams({
values: {
userId: ctx.state.currentUser.id,
},
});
// 调用原始的 create action
const { create } = require('@nocobase/actions');
await create(ctx, next);
},
});NocoBase 基于 Koa,你可以使用中间件来处理请求。有三个级别的中间件注册:
this.app.use(): 应用级中间件,对所有请求生效。this.app.resourcer.use(): 资源级中间件,只对已定义的Resource请求生效。this.app.acl.use(): 权限级中间件,在权限判断前执行。
你可以监听应用和数据库的生命周期事件来执行特定逻辑。
this.app.on(): 监听应用事件,如beforeStart,afterInstall。this.db.on(): 监听数据库事件,如posts.afterCreate(posts 表创建记录后),users.beforeUpdate(users 表更新记录前)。
示例:订单创建后减库存
// 在 plugin.ts 的 beforeLoad 方法中
this.db.on('orders.afterCreate', async (order, { transaction }) => {
// ...减库存逻辑
});当你的插件版本更新,需要对数据结构或数据进行不兼容的改动时,可以创建升级脚本。
# 创建一个 migration 文件
yarn nocobase create-migration update-users-table --pkg=@my-project/plugin-hello这会在 src/server/migrations 目录下生成一个带时间戳的脚本文件。你可以在 up 方法中编写升级逻辑。用户运行 yarn nocobase upgrade 时,该脚本会被执行。
与后端类似,src/client/index.tsx 文件定义了插件的前端主类,它继承自 @nocobase/client 的 Plugin,并提供 afterAdd, beforeLoad, load 生命周期。
UI Schema 是 NocoBase 前端开发的核心。它是一种基于 JSON 的数据结构,用于声明式地描述用户界面,而无需编写复杂的 React 代码。
基本结构:
{
"name": "my-component-name",
"type": "void", // void 类型表示纯展示组件
"x-component": "Card", // 要渲染的组件名
"x-component-props": { "title": "My Card" }, // 传递给组件的 props
"properties": { // 子组件
"my-input": {
"type": "string", // string 类型表示一个字段
"x-component": "Input",
"title": "My Input"
}
}
}要渲染一个 Schema,你需要使用 <SchemaComponent> 组件。
- 创建组件: 创建一个标准的 React 组件。
- 注册组件: 在插件的
load方法中,使用this.app.addComponents({ MyComponent })将其注册到应用中。 - 在 Schema 中使用: 在
UI Schema的x-component属性中指定组件的注册名称。
示例:
// 1. 创建组件
const Hello = () => <h1>Hello from my custom component!</h1>;
class MyClientPlugin extends Plugin {
async load() {
// 2. 注册组件
this.app.addComponents({ Hello });
// ... 添加一个使用该组件的页面
}
}
// 3. 在 Schema 中使用
const schema = {
name: 'my-page',
type: 'void',
'x-component': 'Hello',
};你可以使用 app.router 和 app.pluginSettingsManager 来添加新的页面。
this.app.router.add(name, options): 添加一个常规页面。name支持点状路径来创建嵌套路由。this.app.pluginSettingsManager.add(name, options): 在 "后台管理 -> 插件设置" 中添加一个配置页面。
示例:添加一个插件配置页
// 在 client/index.tsx 的 load 方法中
this.app.pluginSettingsManager.add('my-plugin-settings', {
title: 'My Plugin Settings',
icon: 'SettingOutlined',
Component: () => <div>This is my plugin's settings page.</div>,
});NocoBase 的 "UI 编辑器" (Designable) 模式允许用户通过拖拽和配置来修改页面。你可以通过 SchemaInitializer 和 SchemaSettings 来扩展编辑器的能力。
SchemaInitializer: 定义了 "添加区块" 或 "配置操作" 菜单中的选项。你可以创建新的Initializer或向已有的Initializer中添加item。SchemaSettings: 定义了选中一个区块后,"编辑区块" 弹窗中的配置项。
通过组合使用这些能力,你可以创建出功能强大且可由用户自行配置的区块。
在前端组件中,推荐使用 @nocobase/client 提供的 useRequest hook 来发起 API 请求。它封装了 axios,并处理了加载状态、错误、刷新等逻辑。
import { useRequest } from '@nocobase/client';
const MyComponent = () => {
const { data, loading, error } = useRequest({
resource: 'hello', // 对应后端的 resource name
action: 'list', // 对应后端的 action name
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return <ul>{data?.data?.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}- 语言文件: 在
src/locale目录下创建en-US.ts,zh-CN.ts等文件。 - 使用: 在组件中,使用
react-i18next的useTranslationhook。
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('@my-project/plugin-hello'); // 插件名作为 namespace
return <div>{t('My Translation Key')}</div>;在复杂的业务场景中,插件之间经常需要互相调用或交互。NocoBase 提供了多种机制来实现插件间的通信。
你可以在一个插件中获取另一个已加载插件的实例,并直接调用其上的方法。这种方法简单直接,但会增加插件间的耦合度。
后端:
// 在 my-plugin 的 plugin.ts 的 load 方法中
import { PluginA } from '@nocobase/plugin-a';
// 获取 plugin-a 的实例
const pluginA = this.app.getPlugin<PluginA>('@nocobase/plugin-a');
// 调用 plugin-a 上的公开方法
pluginA.somePublicMethod();前端:
在插件类中,可以通过 this.app.pluginManager.get() 获取。在 React 组件中,可以使用 usePlugin hook。
import { usePlugin } from '@nocobase/client';
import { PluginA } from '@nocobase/plugin-a';
const MyComponent = () => {
const pluginA = usePlugin(PluginA); // 或 usePlugin('@nocobase/plugin-a')
// ...
}使用事件是更推荐的方式,它能有效降低插件间的耦合。一个插件可以触发一个事件,而其他任何插件都可以监听并响应这个事件,双方无需知道对方的存在。
例如,plugin-A 在完成某个操作后,可以发出一个事件:
// plugin-A:
this.app.emit('my-event', { some: 'data' });plugin-B 可以监听这个事件:
// plugin-B:
this.app.on('my-event', (data) => {
console.log('Received my-event with data:', data);
});数据库事件 (db.on) 也是一种非常强大的松耦合机制。
这是一个完美的插件间通信的例子。你的插件并不直接“调用”工作流,而是为工作流插件注册一个新的能力(节点类型),然后用户可以在工作流设计器中使用你的节点。
假设我们要创建一个“发送短信”的节点。
首先,在你的插件中创建一个新的 Instruction 类。
src/server/instructions/sms.ts:
import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
// 假设你有一个 smsService 用于发短信
const smsService = {
send: async (to, content) => { /* ... */ }
};
export class SmsInstruction extends Instruction {
// run 方法是节点的核心逻辑
async run(node, input, processor) {
// 从节点配置中获取手机号和短信内容
// 这些值可以由用户在工作流设计器中静态配置,也可以来自上一个节点的输出
const { to, content } = node.config;
try {
await smsService.send(to, content);
// 返回成功状态
return { status: JOB_STATUS.RESOLVED };
} catch (error) {
// 返回失败状态和错误信息
return { status: JOB_STATUS.REJECTED, result: error.message };
}
}
}然后,在你的插件主类中,将这个 Instruction 注册到工作流插件里。
src/server/plugin.ts:
import { Plugin } from '@nocobase/server';
import { WorkflowPlugin } from '@nocobase/plugin-workflow';
import { SmsInstruction } from './instructions/sms';
export class MySmsPluginServer extends Plugin {
async load() {
// 1. 获取工作流插件的实例
const workflowPlugin = this.app.getPlugin<WorkflowPlugin>('workflow');
if (workflowPlugin) {
// 2. 注册一个新的节点类型,名为 'send-sms'
workflowPlugin.registerInstruction('send-sms', SmsInstruction);
}
}
}为了让用户能在工作流设计器里看到并配置你的新节点,你还需要提供一个前端的 Instruction 定义。
src/client/instructions/sms.tsx:
import { Instruction } from '@nocobase/plugin-workflow/client';
export class SmsInstruction extends Instruction {
// 必须和后端注册的 key 一致
type = 'send-sms';
// 在设计器里显示的标题
title = '发送短信';
// 定义配置表单的 UI Schema
fieldset = {
to: {
type: 'string',
title: '手机号',
name: 'to',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
content: {
type: 'string',
title: '短信内容',
name: 'content',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
required: true,
}
};
}最后,在你的客户端插件主类中注册它。
src/client/index.tsx:
import { Plugin } from '@nocobase/client';
import { WorkflowPlugin } from '@nocobase/plugin-workflow/client';
import { SmsInstruction } from './instructions/sms';
export class MySmsPluginClient extends Plugin {
async load() {
const workflowPlugin = this.app.pluginManager.get<WorkflowPlugin>('workflow');
if (workflowPlugin) {
workflowPlugin.registerInstruction(SmsInstruction.prototype.type, SmsInstruction);
}
}
}完成以上步骤后,用户在创建工作流时,就能在节点列表里找到并使用你新添加的“发送短信”节点了。这就是插件间通过“扩展-被扩展”模式进行通信的典型范例。
当你开发完成插件后,可以将其打包分发给其他人使用。
# 构建指定的插件
yarn build @my-project/plugin-hello# 将构建好的插件打包成 .tar.gz 文件
yarn nocobase tar @my-project/plugin-hello打包好的文件会存放在 storage/tar 目录下。
在另一个 NocoBase 应用中,可以通过后台的 "插件管理" -> "添加插件",上传这个 .tar.gz 文件来安装你的插件。
至此,你已经学习了 NocoBase 插件开发的完整流程。鼓励你深入探索各个部分的文档和示例,构建出功能强大的插件!