Skip to content

Latest commit

 

History

History
614 lines (453 loc) · 19.4 KB

File metadata and controls

614 lines (453 loc) · 19.4 KB

NocoBase 插件开发完全指南

1. 介绍

什么 NocoBase 插件?

NocoBase 的核心思想是“一切皆插件”。所有功能,包括核心功能,都是通过插件实现的。这种架构使得 NocoBase 具有极高的可扩展性和可定制性。插件可以用来添加新的功能、修改现有功能、集成第三方服务等等。

通过开发插件,你可以:

  • 扩展数据模型:创建新的数据表 (Collections) 和字段 (Fields)。
  • 创建自定义页面:使用 UI Schema 或纯 React 组件构建新的前端页面。
  • 添加 API 接口:定义新的后端 API (Resources and Actions) 或扩展已有的 API。
  • 定制用户界面:添加新的区块、操作按钮、配置项等。
  • 集成第三方服务:连接外部数据源、支付网关、消息服务等。

本指南将带你从零开始,学习 NocoBase 插件开发的完整流程。

2. 开始

环境准备

在开始插件开发之前,请确保你已经有一个可以正常运行的 NocoBase 开发环境。推荐使用 Git 源码方式安装,这样可以方便地管理和开发你的插件。

插件的组织方式

NocoBase 支持三种方式来组织和加载插件,它们最终都会被加载到项目根目录的 node_modules 中:

  1. packages/plugins (推荐用于开发) 这是开发插件时最推荐的方式。将你的插件源码放置在此目录下,NocoBase 会通过 yarn workspace 来管理。这种方式可以让你直接调试源码,并且 yarn install 会自动处理所有依赖。

    |- /packages/
      |- /plugins/
        |- /@my-scope/
          |- /plugin-my-first-plugin/
        |- /my-other-plugin/
    
  2. storage/plugins 用于存放已经编译好的、即插即用的插件。通过 NocoBase 后台 UI 上传的插件包就会被解压到这里。这种方式适合分发和使用已经开发完成的插件。

  3. package.jsondependencies 你也可以像安装普通的 npm 包一样,将 NocoBase 插件添加到主项目的 package.json 中。NocoBase 的许多核心插件就是通过这种方式加载的。

3. 你的第一个插件:Hello World

我们将通过一个简单的 "Hello World" 插件来快速了解开发、激活和测试的完整流程。

步骤 1: 创建插件

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   # 插件包信息
  ...

步骤 2: 添加插件到应用

创建插件后,你需要使用 pm add 命令将其注册到 NocoBase 应用中,这样应用才能识别它。

yarn pm add @my-project/plugin-hello

现在,访问你的 NocoBase 后台(默认为 http://localhost:13000/admin),进入 "插件管理" -> "本地插件",你应该能看到刚刚创建的 "hello" 插件。

步骤 3: 编写后端代码

让我们来定义一个名为 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;

步骤 4: 激活插件

新插件默认是未激活状态。你可以通过两种方式激活:

通过命令行:

yarn pm enable @my-project/plugin-hello

通过界面: 访问 "插件管理" -> "本地插件",找到 "hello" 插件,点击 "激活" 按钮。

插件激活时,NocoBase 会自动运行数据库迁移,将你在 collections 目录下定义的表同步到数据库中。

步骤 5: 测试后端 API

首先,确保你的 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'

如果一切顺利,你应该能看到返回的数据。

4. 核心概念:后端开发

插件主类 (Plugin)

src/server/plugin.ts 文件定义了插件的后端主类,它继承自 @nocobase/serverPlugin。这个类是插件的入口,提供了一系列生命周期方法让你可以在应用的不同阶段执行代码。

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 访问数据库实例。

数据表 (Collection)

在 NocoBase 中,数据模型被称为 Collection。你可以在插件的 src/server/collections 目录下创建 .ts 文件来定义新的数据表。每个文件默认导出一个 Collection 定义对象。

一个 Collection 主要由 namefields 组成。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 来维护这个多对多关系。

资源与操作 (API)

NocoBase 会自动将你定义的每个 Collection 映射为一个 API Resource,并内置了 create, list, get, update, destroy 等标准 Actions。这就是为什么我们上一步可以直接通过 /api/hello:create 来访问 API。

你可以通过 app.resourcerapp.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);
  },
});

中间件 (Middleware)

NocoBase 基于 Koa,你可以使用中间件来处理请求。有三个级别的中间件注册:

  • this.app.use(): 应用级中间件,对所有请求生效。
  • this.app.resourcer.use(): 资源级中间件,只对已定义的 Resource 请求生效。
  • this.app.acl.use(): 权限级中间件,在权限判断前执行。

事件 (Events)

你可以监听应用和数据库的生命周期事件来执行特定逻辑。

  • 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)

当你的插件版本更新,需要对数据结构或数据进行不兼容的改动时,可以创建升级脚本。

# 创建一个 migration 文件
yarn nocobase create-migration update-users-table --pkg=@my-project/plugin-hello

这会在 src/server/migrations 目录下生成一个带时间戳的脚本文件。你可以在 up 方法中编写升级逻辑。用户运行 yarn nocobase upgrade 时,该脚本会被执行。

5. 核心概念:前端开发

插件主类 (Plugin)

与后端类似,src/client/index.tsx 文件定义了插件的前端主类,它继承自 @nocobase/clientPlugin,并提供 afterAdd, beforeLoad, load 生命周期。

UI Schema

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> 组件。

自定义前端组件

  1. 创建组件: 创建一个标准的 React 组件。
  2. 注册组件: 在插件的 load 方法中,使用 this.app.addComponents({ MyComponent }) 将其注册到应用中。
  3. 在 Schema 中使用: 在 UI Schemax-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',
};

路由 (Router)

你可以使用 app.routerapp.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>,
});

扩展 UI (Designable)

NocoBase 的 "UI 编辑器" (Designable) 模式允许用户通过拖拽和配置来修改页面。你可以通过 SchemaInitializerSchemaSettings 来扩展编辑器的能力。

  • SchemaInitializer: 定义了 "添加区块" 或 "配置操作" 菜单中的选项。你可以创建新的 Initializer 或向已有的 Initializer 中添加 item
  • SchemaSettings: 定义了选中一个区块后,"编辑区块" 弹窗中的配置项。

通过组合使用这些能力,你可以创建出功能强大且可由用户自行配置的区块。

发起 API 请求

在前端组件中,推荐使用 @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>;
}

国际化 (i18n)

  • 语言文件: 在 src/locale 目录下创建 en-US.ts, zh-CN.ts 等文件。
  • 使用: 在组件中,使用 react-i18nextuseTranslation hook。
import { useTranslation } from 'react-i18next';

const { t } = useTranslation('@my-project/plugin-hello'); // 插件名作为 namespace
return <div>{t('My Translation Key')}</div>;

7. 高级主题:插件间通信

在复杂的业务场景中,插件之间经常需要互相调用或交互。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) 也是一种非常强大的松耦合机制。

案例研究:为工作流插件创建自定义节点

这是一个完美的插件间通信的例子。你的插件并不直接“调用”工作流,而是为工作流插件注册一个新的能力(节点类型),然后用户可以在工作流设计器中使用你的节点。

假设我们要创建一个“发送短信”的节点。

1. 后端实现 (Server)

首先,在你的插件中创建一个新的 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);
    }
  }
}

2. 前端实现 (Client)

为了让用户能在工作流设计器里看到并配置你的新节点,你还需要提供一个前端的 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);
    }
  }
}

完成以上步骤后,用户在创建工作流时,就能在节点列表里找到并使用你新添加的“发送短信”节点了。这就是插件间通过“扩展-被扩展”模式进行通信的典型范例。

8. 构建与分发

当你开发完成插件后,可以将其打包分发给其他人使用。

步骤 1: 构建

# 构建指定的插件
yarn build @my-project/plugin-hello

步骤 2: 打包

# 将构建好的插件打包成 .tar.gz 文件
yarn nocobase tar @my-project/plugin-hello

打包好的文件会存放在 storage/tar 目录下。

步骤 3: 安装

在另一个 NocoBase 应用中,可以通过后台的 "插件管理" -> "添加插件",上传这个 .tar.gz 文件来安装你的插件。


至此,你已经学习了 NocoBase 插件开发的完整流程。鼓励你深入探索各个部分的文档和示例,构建出功能强大的插件!