Owl 框架架构:宿主、契约与扩展点
讲 Owl 前后端怎么由宿主按契约装上扩展:运行壳、子应用、可选接口、前端子系统,以及进程内总线和可靠事件两条通道。源码在 github.com/bit-labs-cn。
本文目录

Owl 框架架构:宿主、契约与扩展点
一句话结论:Owl 是一套宿主加契约的架构。宿主掌握进程和页面,扩展通过契约交上自己的构造函数、路由和可选能力。
本文只画这套架构。架构图是主干,文字说明每一层负什么责、扩展从哪个口接进去。表怎么建、按钮怎么授权,仍看使用手册。
框架源码在 github.com/bit-labs-cn。
1. 你其实在问什么
要看懂这套架构,实际上是三个问题:
- 边界怎么划。 哪一层是框架,哪一层是宿主,哪一层才是业务扩展。
- 扩展从哪几个口接进去。 哪些契约必须实现,哪些接口用到再实现。
- 一次启动按什么顺序装配。 配置、容器、路由、迁移、后台组件各自在哪一步。
本文回答这三件。不回答某个业务系统里有哪些功能。
2. 先建立直觉:承重墙和房间
把宿主想成楼的承重墙和电梯。水电总闸只有一套:配置、日志、校验、链路追踪、数据库连接、HTTP 路由。每个扩展是一间房,自己带门牌、家具和门牌号:名字、构造函数、路由和菜单。
这个类比在这里成立:房间不相互拆墙。扩展不引用别的扩展的服务层。跨房间的事实,走可靠事件这条通道。
这个类比在这里不成立:房间不能拆下来就当一栋楼。没有运行壳的进程,没有路由,也没有数据库。
3. 整体架构
图:从调用方到宿主,再到扩展
flowchart TB
client[调用方]
frontHost[前端宿主]
core[前端核心]
skin[皮肤]
subFront[前端子系统]
backHost[后端宿主]
box[依赖容器]
base[基础能力]
ext[业务扩展]
infra[存储与消息]
client --> frontHost
frontHost --> core
core --> skin
core --> subFront
frontHost --> backHost
backHost --> box
box --> base
box --> ext
ext --> infra
从上往下是四层。
| 层 | 是什么 | 负责什么 |
|---|---|---|
| 前端框架 | owl-ui 的核心层 |
认证、动态路由、权限、HTTP。核心层不引用页面组件 |
| 皮肤 | theme/default 或另一套皮肤 |
布局、登录页、错误页。换皮肤只换这一层 |
| 后端框架 | owl |
创建应用、依赖注入、配置落盘、迁移、后台组件的启停 |
| 扩展 | 实现契约的模块 | 自己的规则、表、路由和页面 |
前端宿主调 createFlexAdmin({ subsystems }),再把页面挂到页面根节点。后端宿主调 owl.NewApp(扩展...)。WebShell() 听 HTTP;只跑命令时用 ConsoleShell。依赖注入用 dig:容器里放的是构造函数,谁依赖某个类型,就由容器按参数类型接上。
基础能力每个进程都会装,扩展不再各自拿一次:
| 能力 | 作用 |
|---|---|
| 配置 | 读 conf/,并允许环境变量覆盖 |
| 日志 | 结构化日志 |
| 链路追踪 | 跨请求的追踪 |
| 进程内总线 | 同一进程里的内存发布订阅,不落盘 |
| 应用配置 | 应用级选项 |
| 校验器 | 请求校验与文案 |
存储与消息是另一组能力,由运行壳按需放进容器,同一进程每种只注册一次:HTTP 路由、数据库、Redis、对象存储、权限引擎、邮件、消息队列。数据库走 GORM,驱动可以是 MySQL、PostgreSQL 或 SQLite,模型本身不写死某一种库的专有类型。
一个扩展内部再分四层,依赖只能往下:
flowchart LR
h[接口处理]
s[领域服务]
r[数据访问]
m[数据模型]
h --> s --> r --> m
接口处理只绑定参数并调用服务。服务写规则。仓库访问数据库。模型对应表。
4. 扩展体系
扩展不是插件市场。后端扩展点是一组 Go 接口,前端扩展点是启动时传入的子系统定义。实线是装配时一定会经过的关系,虚线是用到再实现的可选接口。运行壳由宿主里的一个扩展提供,其余扩展不再各带一套。
图:扩展点
flowchart TD
host[宿主]
shell[运行壳]
sub[子应用契约]
binds[构造函数]
route[路由菜单]
cfg[系统配置]
bg[后台组件]
front[前端子系统]
views[视图模块]
kit[皮肤套件]
host --> shell
host --> sub
sub --> binds
sub --> route
sub -.-> cfg
sub -.-> bg
front --> views
kit --> front
4.1 子应用:后端必须实现的契约
每个后端扩展实现 owl.SubApp。结构体里必须有一个名叫 app、类型为 foundation.Application 的字段。框架用反射把应用实例填进去。服务提供者也一样。
| 方法 | 什么时候调用 | 放什么 |
|---|---|---|
Name |
始终 | 扩展标识。权限前缀用它 |
ServiceProviders |
装配初期 | 这个扩展要带进进程的基础设施。运行壳已经注册过时,这里返回空 |
Binds |
装配初期 | 仓库、服务、接口处理的构造函数 |
RegisterRouters |
只有网页模式 | 本扩展的 HTTP 路由 |
RegisterMenus |
只有网页模式 | 菜单树。前端靠它找到页面 |
RegisterCommands |
命令行模式 | 挂到命令行上的命令 |
Bootstrap |
迁移之前 | 种子、进程内监听。此时表还没建好 |
RegisterMigrate |
全部引导之后 | 要建表的模型列表 |
BeforeMigrate / AfterMigrate |
每次启动都走 | 改表前后的修补和种子 |
权限标识由扩展名、模块英文名和方法名拼出,形如 order:order:Create。接口处理实现 ModuleName(),路由用路由构建器登记。登记结果进入全局路由表,供权限中间件和菜单使用。
4.2 服务提供者:把基础设施放进容器
服务提供者是基础设施的扩展点,不是业务模块的扩展点。它做四件事:
| 方法 | 时机 | 做什么 |
|---|---|---|
Conf |
注册之前 | 缺文件时把默认配置写进 conf/;已有文件只补缺键,不覆盖已有值 |
Register |
配置落盘之后 | 只登记构造函数,此时不读配置 |
Boot |
迁移之后 | 依赖都就绪之后的收尾初始化 |
Description |
启动日志 | 人能读懂的一句说明 |
先落盘再加载,是为了第一次启动时配置已经在磁盘上。全部提供者的默认配置写完,才第一次解析日志并加载 conf/。
4.3 两个可选接口
子应用契约本身不加方法。多出来的能力用另一个接口表示。没实现,就当这个扩展没有这件事。
- 系统配置登记
setting.Registrar。实现后,这个扩展的配置项会进入配置目录。网页模式和命令行都会收集,而且发生在注册路由之前。 - 后台组件
foundation.BackgroundHost。实现后,组件在迁移和提供者Boot之后才启动。进程退出时先取消上下文,再按启动的相反顺序停止。
后台组件不放在 Bootstrap。那时迁移还没执行,读写业务表的循环会跑在还不存在的表上。
4.4 前端的两个扩展点
业务页面从子系统插进来。子系统用 defineSubsystem() 定义,必须在 createFlexAdmin 的参数里传入。路由安装之后再注册,刷新首页会是白屏。
| 字段 | 作用 |
|---|---|
name |
子系统唯一名字 |
routes |
静态路由,可选 |
viewModules |
页面文件到组件的映射 |
viewModulesPathPrefix |
把本地路径换成后端菜单里的 component 前缀 |
menuContributions |
按父菜单名字挂上子菜单 |
login / locales / install |
登录页、文案、挂载前的安装钩子 |
后端菜单的 component 必须和这个前缀加上页面相对路径对上。页面组件的 name 必须和菜单项的 name 完全一致,多标签和页面缓存才指向同一个页面。
皮肤是另一条线。核心层通过皮肤注册表取布局、登录页和错误页。新皮肤实现同一组项目后再调用核心启动,业务子系统不用改。
4.5 依赖只能单向
框架不依赖业务扩展。扩展依赖框架的契约和容器。两个业务扩展不互相引用对方的服务层。共享能力放在容器里,由上层按类型取用。用回调或全局变量把依赖环藏起来,依然是环。
5. 一次启动怎么把扩展接上
图:网页模式的启动顺序
flowchart LR
a[创建应用]
b[配置落盘]
c[注册工厂]
d[加载配置]
e[路由菜单]
f[子应用引导]
g[哈希迁移]
h[提供者启动]
i[后台组件]
j[开始监听]
a --> b --> c --> d --> e --> f --> g --> h --> i --> j
命令行模式走同一条链的引导、迁移、提供者启动和后台组件,不注册路由和菜单,最后执行命令。命令失败时进程会退出,所以后台组件要在那之前停掉。
迁移按模型计算结构哈希。只有变化的模型才执行建表。迁移前钩子串行,迁移后钩子并行,两边每次都会跑。
配置和存储目录先找可执行文件所在目录,那里没有再找当前工作目录。服务方式跑时,工作目录和程序目录常常不是同一处。
图:一次请求怎么穿过这些层
sequenceDiagram
participant 浏览器
participant 前端宿主
participant 权限闸门
participant 接口处理
participant 领域服务
participant 数据访问
浏览器->>前端宿主: 打开菜单
前端宿主->>权限闸门: 带令牌请求
权限闸门->>接口处理: 校验通过
接口处理->>领域服务: 已绑定的参数
领域服务->>数据访问: 查询或事务
数据访问-->>浏览器: 统一响应
登录后,前端拿到菜单树,用菜单上的 component 去对子系统提供的视图模块。按钮显不显示,由前端的权限列表决定;接口放不放行,由后端的令牌和权限引擎决定。两边都要过。
6. 例子:一个新能力怎么挂上宿主
假设宿主里已经有运行壳:路由、数据库和 Redis 都在容器里。现在要加上「订单」这个能力。订单只是说明扩展点怎么用,不代表某个已经上线的系统。
后端新增一个实现 SubApp 的类型:
Name返回order。以后的权限标识都以它开头。ServiceProviders返回空。数据库和路由用宿主里那一份。Binds依次放上仓库、服务、接口处理的构造函数。接口处理的参数是服务,服务的参数是仓库,容器自己把链接上。RegisterRouters和RegisterMenus登记这个能力的接口和菜单。RegisterMigrate交出订单表的模型。宿主在全部扩展引导之后统一建表。- 若要在配置页里出现,再实现系统配置登记。若要在迁移之后跑后台循环,再实现后台组件。
前端新增一个子系统,把订单页面放进视图模块,前缀与后端菜单的 component 一致,并在宿主启动时传入。
输入是:宿主已经有运行壳,订单只带自己的契约实现。输出是:同一个进程里多了一组路由和一张表,同一个管理页里多了一个能对上菜单的页面。订单没有再建一条数据库连接。
7. 两条通道:进程内总线和可靠事件
扩展之间传递事实,架构上有两条通道,用途不同。
| 进程内总线 | 可靠事件 | |
|---|---|---|
| 位置 | 基础能力,每个进程都有 | 另一套契约和运行时,用到再接 |
| 落点 | 内存 | 业务行和待发记录同一事务 |
| 适合 | 同一进程里的通知,例如权限变更后刷新策略 | 跨扩展的业务事实,进程退出后仍要在 |
| 投递 | 谁订阅谁收到 | 一次接收按订阅生成多份投递,每份一条队列 |
图:可靠事件经过的站
flowchart LR
tx[业务事务]
box[本地待发]
hub[事件中心]
queue[每订阅一队]
inbox[本地消费]
ack[回执]
tx --> box --> hub --> queue --> inbox --> ack
ack --> hub
发布方把业务写入和待发记录放在同一个数据库事务里。事务回滚,两边一起消失。中心按订阅生成多份投递。消费方先占住收件箱的唯一键,再在同一事务里做业务并写成功回执。失败先回滚业务,再单独记失败回执。
收到、进队列、业务成功是三步。每一步有自己的记录。调度循环是后台组件,在迁移完成之后才启动。
这条通道里,还没闭合的架构边界要算进去:消息通道的确认和断线重建还没闭合;这条路径还没在 MySQL 和 PostgreSQL 上验收。可视化流程设计、可执行任意脚本的规则引擎、用历史事件重建数据库、跨库分布式事务,都不在这套契约里,也不加占位接口。
8. 两种部署
同一套契约可以装成两种进程。区别只在于运行壳和扩展怎么组合,不在于业务规则。
图:组合进程和独立进程
flowchart TB
combo[组合进程]
shell1[一份运行壳]
ext1[扩展甲]
ext2[扩展乙]
alone[独立进程]
shell2[一份运行壳]
ext3[单个扩展]
combo --> shell1
combo --> ext1
combo --> ext2
alone --> shell2
alone --> ext3
组合进程里,运行壳只出现一次。其余扩展只交构造函数、路由和菜单。再挂一份运行壳,数据库、路由和 Redis 会注册两次。
独立进程里,先挂运行壳,再挂那一个扩展。只挂业务扩展的进程,启动后没有路由,也没有数据库。
还有一种部署形态:实现安装契约,由命令行完成安装、启动和停止,真正跑起来时再进入上面的 WebShell。这是进程怎么交给操作系统,业务仍走同一套子应用契约。
9. 常见误区
- ❌ 把架构图画成已经做出来的业务程序清单。 ✅ 架构图画层、契约和装配顺序。具体做了哪些业务,不改变这些层怎么接。
- ❌ 每个扩展都注册一次数据库和路由。 ✅ 这些是进程级的。一个进程只由运行壳注册一次。
- ❌ 返回空的提供者列表表示这个扩展不用数据库。 ✅ 它表示用宿主已经放进容器的那一个连接。
- ❌ 用进程内总线传递跨扩展的业务事实。 ✅ 那条总线不落盘。进程一退,事件就没了。业务事实走待发记录和事件中心。
- ❌ 在前端框架包里直接加业务页面。 ✅ 业务页面放在子系统。框架只提供登录、布局和错误页。
- ❌ 中心已经收到就等于业务已经做成。 ✅ 收到、进队列、业务成功是三件事。
10. 三句话回顾
- Owl 的架构是宿主加契约:后端一个进程装多个子应用,前端一个页面装多个子系统。
- 扩展点分必做和可选。必做的是子应用方法、构造函数绑定、路由菜单;用到再做的是系统配置和后台组件。运行壳只注册一次路由和数据库。
- 进程内总线传递同进程通知,可靠事件传递跨扩展事实。消息通道的确认、断线重建和多数据库验收,仍是这条通道上没有闭合的边界。
修订记录
- v1 2026-11-13:初稿。把宿主里已经装上的业务程序也写进了正文。
- v2 2026-11-13:重写正文。只留宿主、契约、扩展点、启动顺序和两条事件通道。业务程序清单从正文拿掉,改记进常见误区。
- v3 2026-11-13:文首附上源码地址 https://github.com/bit-labs-cn。
评论
加载评论中…