Cabloy 双引擎架构详解:Vona 与 Zova 是如何协作的

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 与 Admin
  • ssr-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 的完整全栈系统。


延伸阅读

相关推荐
前端大斗师2 小时前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
LRL_4 小时前
跨平台 Node 模块安装指南:如何利用 pnpm/npm 配置`supportedArchitectures` 锁定平台依赖
前端·npm·node.js
GlobalAIagent7 小时前
外贸站上线前的技术自查清单:漏一项询盘可能消失
google·typescript·ar·go语言·seo
逍遥59660701011 小时前
网络设备交换机路由器 AI 配置助手
node.js·硬件工程
半糖程序员12 小时前
从零构建 Agent(11):压缩过长的上下文
typescript·agent
全栈Agent 小李13 小时前
【无标题】
前端·后端·agent·ai编程·全栈·cursor·mcp
damoluomu13 小时前
Directus 换掉 GPL 之后,Node.js CMS 到底怎么选?(三)
node.js
whi13 小时前
一个小工具,解决了一个困扰 Vue 开发者多年的类型检查难题
vue.js·typescript
shmily麻瓜小菜鸡13 小时前
Axios 中 params 与 data 的区别与原理
javascript·typescript