glen
· Owl · 预计阅读 11 分钟
Owl

Owl 框架架构:宿主、契约与扩展点

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

本文目录
Owl 框架架构:宿主、契约与扩展点

Owl 框架架构:宿主、契约与扩展点

一句话结论:Owl 是一套宿主加契约的架构。宿主掌握进程和页面,扩展通过契约交上自己的构造函数、路由和可选能力。

本文只画这套架构。架构图是主干,文字说明每一层负什么责、扩展从哪个口接进去。表怎么建、按钮怎么授权,仍看使用手册。

框架源码在 github.com/bit-labs-cn。

1. 你其实在问什么

要看懂这套架构,实际上是三个问题:

  1. 边界怎么划。 哪一层是框架,哪一层是宿主,哪一层才是业务扩展。
  2. 扩展从哪几个口接进去。 哪些契约必须实现,哪些接口用到再实现。
  3. 一次启动按什么顺序装配。 配置、容器、路由、迁移、后台组件各自在哪一步。

本文回答这三件。不回答某个业务系统里有哪些功能。

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 的类型:

  1. Name 返回 order。以后的权限标识都以它开头。
  2. ServiceProviders 返回空。数据库和路由用宿主里那一份。
  3. Binds 依次放上仓库、服务、接口处理的构造函数。接口处理的参数是服务,服务的参数是仓库,容器自己把链接上。
  4. RegisterRouters 和 RegisterMenus 登记这个能力的接口和菜单。
  5. RegisterMigrate 交出订单表的模型。宿主在全部扩展引导之后统一建表。
  6. 若要在配置页里出现,再实现系统配置登记。若要在迁移之后跑后台循环,再实现后台组件。

前端新增一个子系统,把订单页面放进视图模块,前缀与后端菜单的 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. 三句话回顾

  1. Owl 的架构是宿主加契约:后端一个进程装多个子应用,前端一个页面装多个子系统。
  2. 扩展点分必做和可选。必做的是子应用方法、构造函数绑定、路由菜单;用到再做的是系统配置和后台组件。运行壳只注册一次路由和数据库。
  3. 进程内总线传递同进程通知,可靠事件传递跨扩展事实。消息通道的确认、断线重建和多数据库验收,仍是这条通道上没有闭合的边界。

修订记录

  • v1 2026-11-13:初稿。把宿主里已经装上的业务程序也写进了正文。
  • v2 2026-11-13:重写正文。只留宿主、契约、扩展点、启动顺序和两条事件通道。业务程序清单从正文拿掉,改记进常见误区。
  • v3 2026-11-13:文首附上源码地址 https://github.com/bit-labs-cn。

评论

加载评论中…

暂无上一篇
暂无下一篇

BitLabs © 2026 · 论坛讨论 · 博客长文 · 文档手册