从 Mini NocoBase Demo 看无代码平台的微内核与插件化设计

Jiayu Xu Lv2

最近整理了一个 mini_nocobase_learning_demo,它不是 NocoBase 的源码复刻,而是用 Python 标准库写出来的一个学习版微内核。项目规模很小,但把无代码平台里最关键的几件事串起来了:动态表、Resource Action、权限、UI Schema、工作流、审批、图表、文件和导入导出。

这类 demo 的价值不在于“又写了一套 CRUD”,而在于它能帮助我们看清一个无代码平台到底在抽象什么。

普通业务系统通常是把能力直接写死在代码里:

1
2
3
4
5
6
代码里写用户表
代码里写订单表
代码里写订单接口
代码里写订单页面
代码里写审批逻辑
代码里写权限判断

无代码平台的思路不一样。它更像一套可注册、可解释、可组合的运行时:

1
2
3
4
5
6
7
核心只提供注册点
插件负责注册能力
元数据描述表结构
UI Schema 描述页面
Resource Action 描述接口动作
ACL 描述权限
Workflow Definition 描述流程

也就是说,平台核心不应该知道“订单”是什么,也不应该知道“报销审批”是什么。它只需要知道:有人注册了一张表、注册了一批动作、注册了一套权限、注册了一些 UI 区块和流程节点。运行时根据这些注册表把能力组合起来。

微内核:Core 不写业务

在这个 demo 里,App 是整个系统的入口,但它本身很薄:

1
2
3
4
5
6
7
8
9
class App:
def __init__(self, *, db_path=":memory:", storage_dir=None):
self.acl = ACL()
self.ui = UIRegistry()
self.collections = CollectionManager(self)
self.resources = ResourceManager(self)
self.workflow = WorkflowEngine(self)
self.events = EventBus(self)
self.plugins = PluginManager(self)

这里没有 CRM、订单、审批、图表这些业务概念,只有几个运行时组件:

1
2
3
4
5
6
7
ACL                 权限注册表
UIRegistry UI 能力注册表
CollectionManager 元数据和动态表管理
ResourceManager Resource Action 分发
WorkflowEngine 工作流执行器
EventBus 事件总线
PluginManager 插件生命周期管理

这就是微内核架构的味道:核心提供机制,业务通过插件进入系统。

如果你以后要设计一个可扩展平台,最先要问的不是“我要不要加一个订单模块”,而是“订单模块需要挂到哪些注册点上”。这个问题一变,代码组织方式就会变。

元数据驱动:表结构不是写死的

无代码平台一定绕不开动态数据建模。用户在页面上新增一个数据表,后端必须能够把它变成真实可查询、可写入的数据结构。

demo 里用 sys_collectionssys_fields 保存元数据:

1
2
sys_collections  保存 collection 名称、标题、来源插件
sys_fields 保存字段名、字段类型、控件类型、必填、默认值、关联配置

同时,每个 collection 会映射成真实业务表:

1
2
3
customers -> biz_customers
orders -> biz_orders
files -> biz_files

当插件注册订单表时,大概是这样的:

1
2
3
4
5
6
7
8
9
10
app.collections.define_collection(
"orders",
"订单",
[
{"name": "order_no", "type": "string", "title": "订单号"},
{"name": "customer", "type": "belongsTo", "target": "customers"},
{"name": "amount", "type": "decimal", "title": "金额"},
],
plugin="crm",
)

它背后做了几件事:

1
2
3
4
1. 写入 sys_collections
2. 创建 biz_orders 真实表
3. 写入 sys_fields 字段元数据
4. 给真实表 ALTER TABLE ADD COLUMN

后续新增字段也是同一个思路:先写元数据,再同步物理表。

这里还有一个很容易被忽略的安全点。动态 SQL 不能只靠参数绑定,因为表名、字段名不能作为普通参数传入。值可以参数绑定,identifier 必须做白名单校验:

1
2
3
4
5
6
IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")

def safe_ident(name: str) -> str:
if not IDENT_RE.match(name):
raise ValueError(f"unsafe SQL identifier: {name!r}")
return name

无代码平台经常要把用户配置变成 SQL,这个边界必须非常明确:

1
2
SQL 值:参数绑定
表名和字段名:白名单校验后再拼接

否则动态建模会直接变成动态注入风险。

Resource Action:接口也可以注册出来

NocoBase 风格的接口不是传统 REST 路径,而是更偏命令式:

1
2
3
4
5
6
7
/api/orders:list
/api/orders:get/1
/api/orders:create
/api/orders:update/1
/api/orders:destroy/1
/api/orders:salesByStatus
/api/approval:submit

这个形式的好处是统一。CRUD、图表聚合、审批提交、文件上传、导入导出,本质上都可以抽象成:

1
resource + action

插件可以注册一个 action:

1
app.resources.register_action("orders", "salesByStatus", handler, "echarts")

也可以给某个 collection 一次性注册通用 CRUD:

1
app.resources.register_collection_crud("orders", "crm")

请求进来后,ResourceManager 做统一分发:

1
2
3
4
5
6
7
1. 解析 /api/orders:create
2. 得到 resource=orders, action=create
3. 检查 ACL
4. 注入 data scope
5. 找到 handler
6. 执行 handler
7. 包装响应

这个设计的关键不只是“路由好注册”,而是它把平台里所有可调用能力都放进了同一个协议里。前端按钮、区块动作、工作流节点、导入导出入口,都可以指向某个 resource action。

ACL:权限必须进入后端查询

很多系统会把权限理解成“前端按钮显不显示”,但这在平台型系统里远远不够。只要接口还在,用户绕过前端直接请求就可能越权。

demo 里的 ACL 支持 role/resource/action 规则,也支持简单的数据范围:

1
2
3
app.acl.allow("admin", "*", "*")
app.acl.allow("editor", "orders", "list", data_scope={"owner_id": "$user_id"})
app.acl.allow("viewer", "orders", "get")

当 editor 查询订单时,ResourceManager 会先做权限检查,再把数据范围注入请求:

1
2
3
4
5
6
7
8
9
editor list orders

ACL 检查通过

注入 data_scope: owner_id = 当前用户

CollectionManager.list 合并 filter

生成带 WHERE 的 SQL

这条链路非常重要。权限不应该停留在 UI 层,而应该进入后端查询层。尤其是无代码平台,用户可以动态建表、动态配置页面、动态开放接口,如果 ACL 不在统一分发层执行,后面每个插件都会重复踩坑。

UI 也能插件化

很多人说到插件系统,只会想到后端接口。但无代码平台里,前端能力同样需要注册。

demo 里的 UIRegistry 维护几类配置:

1
2
3
4
blockTypes        区块类型,例如 table、form、chart
fieldInterfaces 字段控件,例如 input、select、recordPicker
actionTypes 动作类型,例如 submitApproval、export
pages 页面 schema

插件可以这样注册一个页面:

1
2
3
4
5
6
7
8
9
10
11
app.ui.register_page(
"crm",
{
"title": "CRM",
"blocks": [
{"type": "table", "collection": "customers"},
{"type": "table", "collection": "orders"},
],
},
"crm",
)

真实前端会读取类似 manifest 的结构,然后知道系统里有哪些页面、哪些区块、每个区块绑定哪个 collection、哪些字段应该使用哪个控件。

所以 UI 不是写死的页面文件,而是插件注册出来的 schema。插件不只是在后端多一个 API,它也可以给前端多一种字段控件、多一种区块、多一个动作按钮。

事件和工作流:业务变化后的第二条链路

平台型系统里,创建一条业务记录通常不是终点。订单创建后要通知管理员,报销提交后要生成审批任务,文件上传后可能要做扫描,客户更新后可能要写审计。

如果这些逻辑都塞进 CRUD 里,核心会越来越重。

demo 的处理方式是事件总线加工作流:

1
2
3
4
5
6
7
8
9
10
11
CollectionManager.create("orders")

INSERT INTO biz_orders

emit orders.created

EventBus 通知监听器

WorkflowEngine 匹配 trigger

按 workflow steps 执行节点

Workflow 本身也不写死节点类型,而是让插件注册:

1
2
app.workflow.register_node_type("set_field", set_field, "workflow")
app.workflow.register_node_type("notify", notify, "notification")

订单创建后的流程可以用 JSON 描述:

1
2
3
4
5
6
7
8
9
app.workflow.create_workflow(
name="订单创建后通知管理员",
trigger="orders.created",
steps=[
{"type": "set_field", "config": {"field": "status", "value": "submitted"}},
{"type": "notify", "config": {"user_id": 1, "message": "新订单 {order_no}"}},
],
plugin="crm",
)

这里体现了一个很典型的解释器模式:工作流定义是数据,运行时读取这份数据,再通过 node registry 找到真正的执行函数。

插件生命周期:能力挂载需要顺序

插件不是简单 import 一下就完事。它需要依赖、加载顺序和生命周期。

demo 里的插件有这些钩子:

1
2
3
4
5
6
7
after_add
before_load
load
install
after_enable
after_disable
remove

通常可以这样理解:

1
2
3
before_load   先注册 collection、字段、基础模型
load 再注册 API、ACL、UI、workflow 节点、事件监听
install 最后初始化默认数据或默认流程

比如一个 CRM 插件可能会:

1
2
3
4
5
6
7
8
9
10
11
before_load:
注册 customers 和 orders 表

load:
注册 customers/orders CRUD
注册订单图表 action
注册 CRM 页面
注册 editor/viewer/admin 权限

install:
创建订单创建后的默认 workflow

PluginManager.enable("crm") 会先递归启用依赖插件,再依次执行生命周期。这样 UI 核心、用户系统、工作流、通知这些基础能力先准备好,CRM 插件才能把自己的能力挂上去。

一条订单请求如何串起所有机制

看一条订单创建请求,平台能力就串起来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/api/orders:create

ResourceManager.parse_path

ACL.can(editor, orders, create)

CollectionManager.create("orders")

读取 orders 字段元数据

校验字段、处理默认值、动态 INSERT

写入 biz_orders

emit orders.created

WorkflowEngine 执行订单创建流程

set_field 把状态改成 submitted

notify 创建管理员通知

返回订单记录

这条链路里,订单本身只是一个插件定义出来的 collection。接口是注册出来的,权限是注册出来的,事件是通用机制发出的,工作流节点也是插件注册的。

这就是从“写业务系统”到“写平台”的区别。

审批为什么不应该写死在业务表里

审批是另一个很好的例子。

如果你只做一个报销系统,很容易把审批字段写在报销单表里:

1
2
3
expense_requests.status
expense_requests.approver_id
expense_requests.approved_at

但平台化以后,审批应该是一套独立运行时。它通过 collection + record_id 指向任意业务记录:

1
2
3
4
5
approval_tasks
collection = expense_requests
record_id = 123
approver_id = 3
status = pending

提交审批时:

1
2
3
4
5
6
7
8
9
10
11
/api/approval:submit

读取目标业务记录

创建 approval_tasks

保存业务快照

更新原业务记录状态为 approving

通知审批人

审批通过时:

1
2
3
4
5
6
7
8
9
/api/approval_tasks:approve

检查当前用户是否是审批人

更新 approval_tasks.status = approved

写 approval_actions 历史

更新原业务记录状态为 approved

这样审批插件就不属于某一张表,而是可以服务订单、报销、合同、请假等任意 collection。

这个 demo 还不是生产系统

学习版 demo 的目标是把机制讲清楚,不是直接当生产框架用。如果要把这套思路真正落地,还要补很多工程能力。

第一是数据库迁移。ALTER TABLE ADD COLUMN 只能覆盖最简单的动态字段新增,生产里还要处理字段重命名、类型变更、索引、唯一约束、大表迁移、回滚、多实例迁移锁和插件版本 migration。

第二是事务边界。订单创建后触发工作流,工作流又更新订单、创建通知。这里要明确:工作流失败是否回滚订单?通知失败是否影响主流程?审批提交失败时业务状态如何恢复?学习版可以简单处理,生产系统必须设计清楚。

第三是插件卸载。禁用一个插件容易,卸载一个插件很难。要不要删除业务表?要不要保留数据?其他插件依赖它怎么办?历史审批任务引用了它的 collection 怎么办?这些都不是一个 remove() 钩子能简单解决的。

第四是安全和性能。动态 filter、动态 sort、关联 appends、数据范围权限、工作流执行、图表聚合都可能成为慢查询来源。文件上传、Workflow HTTP 节点、插件扩展点也都需要额外的安全边界。

所以这个 demo 更适合作为架构骨架学习:先理解注册表和运行时怎么协作,再考虑生产级实现。

可以带走的几个结论

这套 mini demo 最值得记住的不是 Python 代码,而是几个架构判断。

微内核只提供机制,不写具体业务。插件通过生命周期把 collection、resource action、ACL、UI schema、workflow node、event listener 挂进系统。

Collection 和 Field 是元数据,但业务数据应该进入真实物理表。只有这样,查询、索引和迁移才有进一步优化空间。

Resource + Action 是统一调用协议。它能把 CRUD、图表、审批、文件、导入导出都放进同一种分发模型里。

ACL 必须在后端统一执行,并且要进入数据查询层。前端隐藏按钮只是体验优化,不是安全边界。

Workflow 是事件驱动的解释器。流程定义是数据,节点能力来自插件注册,运行时根据事件触发并逐步解释执行。

审批、图表、文件、导入导出都不应该变成 core 的一部分。它们应该证明 core 的扩展点设计是否足够稳。

一句话总结:

无代码平台的核心不是帮你少写几个 CRUD,而是把“数据、接口、页面、权限、流程”都变成可注册、可解释、可组合的运行时能力。

如果要用 Go、Java 或其他语言重写类似后端,也不要一上来就写业务接口。先把这些 registry 和生命周期设计好:

1
2
3
4
5
6
7
8
9
PluginManager
CollectionRegistry
ResourceRegistry
ACLRegistry
UIRegistry
EventBus
WorkflowEngine
StorageRegistry
NotificationRegistry

这些骨架稳定以后,CRM、ECharts、Workflow、Approval、File、ImportExport 才能自然成为插件,而不是慢慢把核心拖成一个巨大的业务泥球。

  • 标题: 从 Mini NocoBase Demo 看无代码平台的微内核与插件化设计
  • 作者: Jiayu Xu
  • 创建于 : 2026-05-18 19:03:59
  • 更新于 : 2026-05-18 19:05:20
  • 链接: https://www.rasior.com/2026/05/18/20260518_mini-nocobase-plugin-architecture/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。