从 Mini NocoBase Demo 看无代码平台的微内核与插件化设计
最近整理了一个 mini_nocobase_learning_demo,它不是 NocoBase 的源码复刻,而是用 Python 标准库写出来的一个学习版微内核。项目规模很小,但把无代码平台里最关键的几件事串起来了:动态表、Resource Action、权限、UI Schema、工作流、审批、图表、文件和导入导出。
这类 demo 的价值不在于“又写了一套 CRUD”,而在于它能帮助我们看清一个无代码平台到底在抽象什么。
普通业务系统通常是把能力直接写死在代码里:
1 | 代码里写用户表 |
无代码平台的思路不一样。它更像一套可注册、可解释、可组合的运行时:
1 | 核心只提供注册点 |
也就是说,平台核心不应该知道“订单”是什么,也不应该知道“报销审批”是什么。它只需要知道:有人注册了一张表、注册了一批动作、注册了一套权限、注册了一些 UI 区块和流程节点。运行时根据这些注册表把能力组合起来。
微内核:Core 不写业务
在这个 demo 里,App 是整个系统的入口,但它本身很薄:
1 | class App: |
这里没有 CRM、订单、审批、图表这些业务概念,只有几个运行时组件:
1 | ACL 权限注册表 |
这就是微内核架构的味道:核心提供机制,业务通过插件进入系统。
如果你以后要设计一个可扩展平台,最先要问的不是“我要不要加一个订单模块”,而是“订单模块需要挂到哪些注册点上”。这个问题一变,代码组织方式就会变。
元数据驱动:表结构不是写死的
无代码平台一定绕不开动态数据建模。用户在页面上新增一个数据表,后端必须能够把它变成真实可查询、可写入的数据结构。
demo 里用 sys_collections 和 sys_fields 保存元数据:
1 | sys_collections 保存 collection 名称、标题、来源插件 |
同时,每个 collection 会映射成真实业务表:
1 | customers -> biz_customers |
当插件注册订单表时,大概是这样的:
1 | app.collections.define_collection( |
它背后做了几件事:
1 | 1. 写入 sys_collections |
后续新增字段也是同一个思路:先写元数据,再同步物理表。
这里还有一个很容易被忽略的安全点。动态 SQL 不能只靠参数绑定,因为表名、字段名不能作为普通参数传入。值可以参数绑定,identifier 必须做白名单校验:
1 | IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") |
无代码平台经常要把用户配置变成 SQL,这个边界必须非常明确:
1 | SQL 值:参数绑定 |
否则动态建模会直接变成动态注入风险。
Resource Action:接口也可以注册出来
NocoBase 风格的接口不是传统 REST 路径,而是更偏命令式:
1 | /api/orders:list |
这个形式的好处是统一。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 | 1. 解析 /api/orders:create |
这个设计的关键不只是“路由好注册”,而是它把平台里所有可调用能力都放进了同一个协议里。前端按钮、区块动作、工作流节点、导入导出入口,都可以指向某个 resource action。
ACL:权限必须进入后端查询
很多系统会把权限理解成“前端按钮显不显示”,但这在平台型系统里远远不够。只要接口还在,用户绕过前端直接请求就可能越权。
demo 里的 ACL 支持 role/resource/action 规则,也支持简单的数据范围:
1 | app.acl.allow("admin", "*", "*") |
当 editor 查询订单时,ResourceManager 会先做权限检查,再把数据范围注入请求:
1 | editor list orders |
这条链路非常重要。权限不应该停留在 UI 层,而应该进入后端查询层。尤其是无代码平台,用户可以动态建表、动态配置页面、动态开放接口,如果 ACL 不在统一分发层执行,后面每个插件都会重复踩坑。
UI 也能插件化
很多人说到插件系统,只会想到后端接口。但无代码平台里,前端能力同样需要注册。
demo 里的 UIRegistry 维护几类配置:
1 | blockTypes 区块类型,例如 table、form、chart |
插件可以这样注册一个页面:
1 | app.ui.register_page( |
真实前端会读取类似 manifest 的结构,然后知道系统里有哪些页面、哪些区块、每个区块绑定哪个 collection、哪些字段应该使用哪个控件。
所以 UI 不是写死的页面文件,而是插件注册出来的 schema。插件不只是在后端多一个 API,它也可以给前端多一种字段控件、多一种区块、多一个动作按钮。
事件和工作流:业务变化后的第二条链路
平台型系统里,创建一条业务记录通常不是终点。订单创建后要通知管理员,报销提交后要生成审批任务,文件上传后可能要做扫描,客户更新后可能要写审计。
如果这些逻辑都塞进 CRUD 里,核心会越来越重。
demo 的处理方式是事件总线加工作流:
1 | CollectionManager.create("orders") |
Workflow 本身也不写死节点类型,而是让插件注册:
1 | app.workflow.register_node_type("set_field", set_field, "workflow") |
订单创建后的流程可以用 JSON 描述:
1 | app.workflow.create_workflow( |
这里体现了一个很典型的解释器模式:工作流定义是数据,运行时读取这份数据,再通过 node registry 找到真正的执行函数。
插件生命周期:能力挂载需要顺序
插件不是简单 import 一下就完事。它需要依赖、加载顺序和生命周期。
demo 里的插件有这些钩子:
1 | after_add |
通常可以这样理解:
1 | before_load 先注册 collection、字段、基础模型 |
比如一个 CRM 插件可能会:
1 | before_load: |
PluginManager.enable("crm") 会先递归启用依赖插件,再依次执行生命周期。这样 UI 核心、用户系统、工作流、通知这些基础能力先准备好,CRM 插件才能把自己的能力挂上去。
一条订单请求如何串起所有机制
看一条订单创建请求,平台能力就串起来了:
1 | /api/orders:create |
这条链路里,订单本身只是一个插件定义出来的 collection。接口是注册出来的,权限是注册出来的,事件是通用机制发出的,工作流节点也是插件注册的。
这就是从“写业务系统”到“写平台”的区别。
审批为什么不应该写死在业务表里
审批是另一个很好的例子。
如果你只做一个报销系统,很容易把审批字段写在报销单表里:
1 | expense_requests.status |
但平台化以后,审批应该是一套独立运行时。它通过 collection + record_id 指向任意业务记录:
1 | approval_tasks |
提交审批时:
1 | /api/approval:submit |
审批通过时:
1 | /api/approval_tasks:approve |
这样审批插件就不属于某一张表,而是可以服务订单、报销、合同、请假等任意 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 | PluginManager |
这些骨架稳定以后,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 进行许可。