Vona 提供后端框架与运行时,Zova 提供前端框架与应用层。两者不是一个大包里的前后端目录,而是两个各自独立演进的引擎,靠生成产物 、运行时集成 和双向类型契约咬合在一起。
第一次打开 Cabloy 仓库的人,往往会先注意到顶层的两个大目录:vona/ 和 zova/。它们各有自己的 package.json、CLI、workspace 配置,甚至各自带一个 __CABLOY_BASIC__ 版本标记。这并非简单的"后端目录 + 前端目录",而是 Cabloy 全栈架构最核心的设计:一个全栈系统,两个引擎。
本文以 Cabloy Basic(版本 5.1.202,MIT 许可)仓库为基准,把这套双引擎架构完整梳理一遍。
一张图先看清整体
text
┌─────────────────────────────────────┐
浏览器 ───────▶ │ Vona(后端运行时,Koa,端口 7102) │
│ ├ SSR Site 分发(Web/Admin/...) │
│ └ 消费 Zova 构建出的 SSR 产物渲染 │
└──────────▲──────────────┬───────────┘
前端元数据反向回灌 │ │ OpenAPI 契约正向下发
(路由/组件/图标类型) │ ▼
┌──────────┴──────────────────────────┐
│ Zova(前端框架,Vue3 + Vite + TSX) │
│ 独立 SSR 开发服务器(端口 9000) │
└─────────────────────────────────────┘
关键原则是:前后端关注点分离、可以独立演进,但协作方式不是手抄接口、也不是手动拷贝构建文件,而是通过生成产物(generated artifacts)、运行时集成和共享约定完成。
- 技术底座(后端):Koa、Knex、Redis、多数据库驱动(CI 覆盖 MySQL 与 PostgreSQL)
- 技术底座(前端):Vue、Vite、Quasar 工具链、TanStack 系列库
- 版本 UI 层:Cabloy Basic 使用 DaisyUI + Tailwind CSS;Cabloy Start 使用 Vuetify。Quasar 是工程工具链,不是版本 UI 组件库
一、Vona:后端引擎
Vona 是 Cabloy 的后端半边,目标是在同一个系统中支撑 SSR、SPA、Web、Admin 多种体验,同时保持前后端足够解耦、可独立演进。
1. IoC 容器与 Bean 体系
Vona 的一切能力都组织为框架托管的 Bean ,所有 Bean 继承 BeanBase,基类已经内置了 app、ctx、bean、scope、$scope、$logger、$loggerChild、$text 等常用工作表面,业务代码不需要自己重新拼装这些访问点。
容器分两级:
| 容器 | 职责 |
|---|---|
| app container | 全局后端 Bean(单例式) |
| ctx container | 请求级作用域 Bean |
Bean 有三种身份表达形式,命名不是风格问题,它直接影响注入、配置覆盖路径、CLI 生成的组织方式:
- Bean 标识符 :
{moduleName}.{sceneName}.{beanName},如training-student.service.student - onion name :
{moduleName}:{beanName},如training-student:student - Bean scene(场景) :service、model、entity、dto、controller、startup、queue、broadcast 等操作家族;内置的全局
bean场景是有意的例外,使用简短的全局简写表面
访问一个 Bean 有四种风格:
| 访问方式 | 适用场景 | 代表写法 |
|---|---|---|
| 依赖注入 | 在类中显式装配 | @Use('training-student.service.student') serviceStudent |
| 依赖查找(推荐的简洁默认) | 普通模块业务代码 | this.scope.service.student.findOne() |
| 直接容器访问 | 需要容器级控制 | this.bean._getBean(...) / this.ctx.bean._getBean(...) |
| 新建实例 | 不应复用默认单例的流程 | this.bean._newBean(...) |
其中 this.bean._getBean(...) 走 app 级容器,this.ctx.bean._getBean(...) 走请求级容器。
2. Scope:模块资源门面
Vona 后端代码之所以能保持简洁,而不是退化成满屏的任意路径 import,关键在 Scope。每个模块的 service、model、entity、config、constant、locale、error 都挂在同一个结构化门面之后:
typescript
// 本模块资源
this.scope.model.stockBalance
// 源码中静态已知的跨模块资源
this.$scope.commerceCatalog.model.sku
// 测试或独立脚本中,持有 app 引用时
app.scope('commerce-catalog').model.sku
一个重要的设计决策是:Scope 查找不产生模块依赖边 。this.$scope.xxx 只是从"当前应用已组装好的模块集合"中解析资源,不会安装、加载目标模块,也不会造成循环依赖。只有当真的需要目标模块的可用性保证、依赖优先排序或最低兼容版本时,才需要声明 vonaModule.dependencies。
3. AOP 与结构化请求管道
Vona 在 controller、internal、external 三类切面之上提供 AOP,并有一条结构化的请求路径:
text
middleware → guard → interceptor → pipe(含 Zod 校验)→ filter
日常开发中常见的 @Core.transaction(...) 与 @Core.retryable(...) 都是这套 AOP 的应用:
@Core.transaction(...)默认传播级别是REQUIRED:无事务则开启,有事务则加入,不会随意提升隔离级别;需要独立提交边界时才用REQUIRES_NEW@Core.retryable(...)通过显式errorCodes白名单只重试可安全回放的瞬态失败,且重试的是下游 AOP 后缀,不会盲目重试外部副作用
4. 契约与数据层:正向契约的真相源
后端请求处理的主干链路是:
text
Controller → Service → Model → Entity
(DTO 推断/生成 + Zod 校验)
Controller、DTO 与 Zod schema 加上 @Api.field(...) 等装饰器元数据,最终产出 OpenAPI 文档。这份 OpenAPI 就是前端 SDK 生成唯一的契约真相------这一点在后面的契约环中至关重要。
Vona 还内置了大量长期价值能力:CRUD 导向工作流、多租户(租户即实例,普通资源 CRUD 自动按实例隔离)、多数据库/多数据源与分片、两层缓存(含跨 Model 的 modelsClear / modelsClearedBy 依赖图)、事务、认证/验证码/RBAC/菜单、文件图片上传、邮件、队列、Worker、定时任务、选举、广播、Redlock、WebSocket、指标与遥测等。
代码组织上,框架本体位于 vona/packages-vona/,CLI 位于 vona/packages-cli/cli/,业务代码位于 vona/src/suite/、vona/src/module/。
二、Zova:前端引擎
Zova 是 Cabloy 的前端半边。它融合了三种成熟框架的长处:Vue3 的响应式、React 风格的 TSX 渲染、Angular 风格的 IoC。读 Zova 代码时,不应把它退化成"普通 Vue + 一堆工具函数"来理解,而要从 controller / bean / IoC 架构进入。
1. 三个 IoC 容器统一所有状态共享
许多 Vue 3 项目会根据状态共享范围,混用组件局部 state、composables、provide/inject 和 Pinia。Zova 换了一个第一性问题:这个状态或行为由哪个 Bean 拥有?它应该挂在哪个容器作用域?
| 容器 | 作用域 | 大致对应 Vue 世界的概念 |
|---|---|---|
| sys | 系统级单例,可跨 SSR 请求存活 | 模块级单例 / 全局长生命周期状态 |
| app | 应用级 / 请求级 | app store、全局 provide 的状态 |
| ctx | 组件实例本地 | 组件局部 state、组件内 composables |
四种典型共享范围因此被收进同一套模型:
typescript
class ControllerPage {
@Use() $$localCounterState: CounterState; // 组件内部
@Use({ injectionScope: 'host' }) $$hostCounterState: CounterState; // 组件之间(层级注入)
@Use({ injectionScope: 'app' }) $$appCounterState: CounterState; // 应用全局
@Use({ injectionScope: 'sys' }) $$sysCounterState: CounterState; // 系统级
}
SSR 场景下 app 与 sys 的区分尤其关键:app 级行为可以绑定到单个应用实例或单个请求,而 sys 级行为不绑定请求,可以活过多次 SSR 请求。
所有 Bean 同样继承 BeanBase,核心成员包括 sys、app、ctx、bean、scope、$el、$event;SSR 模块扩展出 $ssr、$useMeta,页面 controller 基类提供 $params、$query,组件 controller 基类提供 $props。
2. Controller / Page / Component:TSX 渲染
Zova 的页面与组件都有 controller bean 作为逻辑载体,用 TSX 描述渲染。配套的前端工程能力包括:
- a-router 路由:带动态
params的路由必须显式定义route.name,静态路由用$router.getPagePath(...)生成规范 URL - 页面 meta 代码生成
- 双层 tabs 导航、router stack、routed dialog
requiresAuth与ssrProfile(Web 默认public;cookie 态、受保护首屏用session)显式声明
3. Model 统一状态管理与 ModelResource
驱动渲染的异步状态统一由 Model 持有,而不是散落在各 controller 的 fetch/cache 变量里:
- 用
$useStateData(...)在渲染期建立查询状态,读当前 query 拥有的响应式表面(query.data或 model 派生投影) - 资源型业务由 ModelResource 统一负责查询、缓存与 mutation;
$apiSchema或 owning resource 的 schema 可以直接驱动 ZForm、ZTable 自动渲染 - 只有低复用、一次性、不需要共享状态或缓存所有权的页面动作,才允许直接调生成的
$api
这条规则的目的很明确:schema 元数据只有一份(在后端),前端不复制 schema、不手改生成物。
4. 多 flavor 输出:一套源码,多种 SSR Site
Zova 同一份业务源码可以构建出多种 flavor(即 SSR Site)。当前仓库的实际构建产物(zova/dist/)包括:
ssr-cabloyBasicWeb/ssr-cabloyBasicAdmin:Cabloy Basic 的 Web 与 Adminssr-cabloyCommerce/ssr-cabloyCommerceAdmin:电商演示站点rest-cabloyBasicWeb等 rest 产物:供 Vona 消费的前端元数据与类型包
每个 flavor 有独立的 env 文件(zova/env/.env.*),构建时 SSR bundle 与 rest 输出必须成对产出、一起移动。
三、两大引擎如何咬合:两大通道
Vona 与 Zova 之间有两条性质完全不同的通道。
通道 1:Vona 集成 SSR(运行时通道)
同一份前端 SSR 应用,开发期有两个入口。区别不在"是不是 SSR",而在谁接收浏览器请求、谁拥有响应:
text
Vona 集成 SSR: 浏览器 → Vona(7102) → SSR Site 匹配 → Zova 构建产物渲染 → 浏览器水合
Zova 独立 SSR: 浏览器 → Zova dev server(9000) → 前端 SSR(API 仍指向 Vona)
npm run dev启动 Vona 集成 SSR:Web 入口http://localhost:7102/,Admin 入口http://localhost:7102/admin/npm run dev:zova:web/npm run dev:zova:admin启动 Zova 独立 SSR(默认 9000),用于页面、路由、水合的快速热更新迭代- 直接开 9000 端口不能替代 7102 的集成验收:它不经过 Vona 的 SSR Site 分发与构建产物交接
- 发布/验收路径是:构建 SSR 与 rest 产物 →
npm run deps:vona同步给 Vona → 从 Vona 入口做完整验收
注意"standalone"仅指可以直接访问 Zova 开发服务器,并不意味着它是一个可独立部署的 SSR 应用。新增一个可挂载的站点,需要配套的 flavor、成对的 SSR/rest 产物,以及 Vona 侧的 @SsrSite 注册。
通道 2:双向契约环(类型与代码生成通道)
类型信息双向流动,每一边各有自己的"真相源"。
正向链(后端 → 前端)
text
Controller / DTO / Zod (契约真相)
│ Vona 产出 OpenAPI
▼
Zova 生成 SDK($api)与 schema helper($apiSchema)
│ 生成代码禁止手改
▼
前端只做薄层语义封装,复用同一 resource owner
改后端契约时,先改后端真相,再重新生成前端消费方,而不是手 patch 生成文件。
反向链(前端 → 后端)
Zova 的路由、组件、图标元数据是真相,构建后回灌给 Vona 的工具链与类型提示。正确顺序是先构建 Zova、再同步依赖:
bash
npm run build:zova:admin # Admin 侧改动
npm run build:zova:web # 波及 Web flavor 时同样要跑------SSR bundle 与 rest 必须成对
npm run deps:vona # 再把 .zova-rest 产物同步给 Vona
契约环还有两类典型异常分支,处理方式截然不同:
- consumer drift(消费端漂移):重新生成即可修复,不要手改生成物
- local dependency drift(本地依赖漂移) :
.zova-rest产物已经包含预期改动,但 Vona 侧类型仍旧------此时应删除vona/node_modules重新安装依赖,而不是继续调试或手工改依赖链接
仓库本身还在 git hooks 中挂了 contract-loop-gate,提交时自动检查这条链是否被破坏。这意味着"双向契约"不是文档口号,而是被工程化护栏强制执行的工作流。
四、共同的组织骨架:suite / module / package
两个引擎都按相同的结构边界组织业务:suite(套件)→ module(模块)→ package。这是架构边界,不只是目录摆放。前后端同名套件一一对应:
| Suite | 后端 | 前端 | 演示内容 |
|---|---|---|---|
| a-commerce | vona/src/suite/a-commerce |
zova/src/suite/a-commerce |
完整电商 Admin、Web、个人中心与支付流程 |
| a-training | vona/src/suite/a-training |
zova/src/suite/a-training |
主子表表单、嵌套明细、图片与文件上传 |
| a-demo / a-home | 同构对应 | 同构对应 | Demo 与首页 |
业务消费者还显式分为两类:
- Admin Resource:后台管理,复用通用 Resource owner 与资源页
- Web self-service:Web 自助前台,使用独立的状态、页面与服务端 scope
当同一份持久化资源同时服务两类消费者时,领域与持久化边界保持唯一,但 API/DTO 契约、服务端 scope、前端状态所有权与页面架构分开。
五、用一张图记住整体分工
text
Zova(前端真相) Vona(后端真相)
────────────────── ──────────────────
Page/Component/Controller Controller → Service → Model → Entity
sys/app/ctx 三容器 + Bean app/ctx 两容器 + Bean(scene 化)
ModelResource + TanStack Query DTO + Zod 校验 + OpenAPI 产出
flavor: Basic Web/Admin、Commerce SSR Site 注册 @SsrSite、多租户实例
route/component/icon 元数据 ──反向链──▶ deps:vona 消费
$api / $apiSchema 生成 SDK ◀──正向链── OpenAPI 契约
│ │
└──── 构建产物:ssr-* / rest-* ───┘
Vona 集成 SSR(7102)统一对外
理解这套架构后,很多 Cabloy 的工作约定都会变得顺理成章:
- 为什么改后端接口后要重新生成前端 SDK,而不是手改
$api - 为什么前端路由改动后必须先跑 Zova 构建、再
deps:vona - 为什么调试全栈问题要区分 9000(前端迭代)与 7102(集成真相)
- 为什么 schema 驱动表单(ZForm/ZTable)不需要在前端重复维护字段定义
- 为什么业务代码要按 suite/module 边界组织,而不是按技术类型分目录
Vona 与 Zova 各自是完整、独立、可单独演进的框架;而 Cabloy 的价值,正是用 SSR 运行时通道与双向契约环,把它们组装成了一个面向 AI Spec-Driven Development 的完整全栈系统。