Cabloy全栈框架的两个SSR入口:Vona集成式SSR vs Zova独立式SSR

文章来源声明: 原文作者:濮水大叔; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689654965351530539; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

清晰拆解两个 SSR 入口的职责边界,并给出类型双向同步的落地链路,适合刚上手 Cabloy 的全栈开发者建立整体心智模型。

刚开始运行 Cabloy Basic 时,通常会看到两类命令:
<span># 启动完整的 Cabloy 服务</span>
npm run dev

<span># 启动 Zova 前端 SSR 开发服务</span>
npm run dev:zova:web
<span># 或</span>
npm run dev:zova:admin

它们都能打开 SSR 页面,却服务于不同的目标:

  • npm run dev 默认访问 http://localhost:7102;
  • npm run dev:zova:* 默认访问 http://localhost:9000。

初学者不需要一开始理解所有 SSR 细节,只要先记住一句话:用 9000 高效开发前端,用 7102 确认项目按生产方式完整运行。

1-zh.png

两个端口,两种工作节奏

7102:Vona集成式SSR

访问 7102 时,请求首先进入 Vona 后端服务。Vona 决定该 URL 对应哪个 SSR Site,加载对应的 Zova SSR 构建产物,再把最终 HTML 响应返回给浏览器。

因此,7102 代表的是完整的运行路径:

浏览器
  → Vona 后端服务
  → Zova SSR 构建产物渲染页面
  → 浏览器接管页面

这与生产环境的核心运行方式一致:由 Vona 承载 HTTP 请求和 SSR 集成,Zova 负责把前端页面渲染出来。

当你需要确认下面这些事情时,应该访问 7102:

  • Web 或 Admin 的访问路径是否正确;
  • Vona 是否能找到并加载正确的前端构建产物;
  • 后端 API、SSR 页面和最终 HTTP 响应能否协同工作;
  • 准备交付前,项目是否能以完整路径正常运行。

9000:Zova独立式SSR

访问 9000 时,浏览器直接进入 Zova 前端开发服务。它同样会进行 SSR 渲染,但目标是让前端开发更快:修改用户代码后可以热更新,页面、路由、首屏渲染和 hydration 问题也更容易快速定位。

浏览器
  → Zova 前端开发服务
  → SSR 渲染页面 + 用户代码热更新

因此,开发页面时可以先使用 9000:

  • 调整页面布局和交互;
  • 修改组件、路由和前端状态;
  • 检查 SSR 首屏与浏览器接管后的表现;
  • 利用热更新缩短“修改—查看结果”的反馈时间。

7102 与 9000 是 Cabloy Basic 的默认开发端口。端口可以因环境配置而变化,但两个入口的职责划分不变。


Cabloy 项目的全栈原理

Cabloy 的全栈模型围绕两个基本原则建立。

1. 前端构建产物直接参与后端 SSR

  • Zova 拥有前端应用源码,负责页面、组件、路由和前端状态;
  • Zova 生成的前端 bundle 与 SSR 相关产物,会由 Vona 的 SSR 流程加载和使用;
  • 因此,服务端渲染与浏览器 hydration 处在一条协调一致的交付路径上。

一次完整的页面访问,可以先简单理解为:

浏览器请求
  → Vona 接收请求并找到对应站点
  → Zova SSR 构建产物渲染页面并准备初始状态
  → Vona 返回 HTML
  → 浏览器 hydration 后继续运行页面

这就是 integrated SSR 的基本含义:前端 SSR 不是孤立的“页面预渲染”,而是 Vona 与 Zova 共同完成的一次全栈请求。

2. 类型信息双向流动

Cabloy 不要求前后端手工维护两份看起来相同的类型。后端 API 契约和前端结构化资源都能沿明确的方向交给另一侧消费:

  • **后端 → 前端:**Vona 生成 Swagger / OpenAPI 契约,Zova 据此生成 SDK、类型和 schema helpers;
  • **前端 → 后端:**Zova 生成 routes、components、icons、renderers 等结构化 metadata 与类型表面,供 Vona 的工具和类型提示使用。

下一节会从一个简单例子说明这两条同步方向。对初学者而言,先理解这两点就足够了:Vona 管后端入口与 SSR 集成,Zova 管前端应用与渲染;两边通过构建产物和契约信息协作。


类型为什么也需要“双向同步”?

全栈项目最容易遇到的问题之一,是后端和前端各自维护一份“看起来相同”的定义:后端改了字段,前端忘记更新;前端新增了一个可渲染资源,后端不知道如何安全引用它。

Cabloy 采用前后端分离架构,因此并不是简单地让前后端共享同一个 types.ts 类型文件,而是基于双向契约,实现前后端类型的自动生成与共享。

Vona → Zova:后端 API 变化时

当 Controller、DTO、校验规则或实体字段发生变化,业务事实在 Vona。Vona 会把它们表达为 Swagger / OpenAPI,Zova 再生成相应的 API、类型和 schema helpers:

Vona 的 API / DTO / 校验
  → Swagger / OpenAPI
  → Zova 生成的 API 与类型
  → 前端 Model / 页面使用

下面用 training-student 中已经存在的 summary/:id 接口看一遍完整过程。这个接口返回学生的摘要信息,例如等级标题、摘要文本和描述长度。

1. 后端定义接口和返回 DTO

Vona 的 Controller 声明 URL、参数和返回 DTO:

<span>// vona/.../training-student/src/controller/student.ts</span>
<span>@Web</span>.<span>get</span>(<span>'summary/:id'</span>, { <span>summary</span>: $locale(<span>'StudentSummary'</span>) })
<span>@Api</span>.<span>body</span>(v.<span>optional</span>(), v.<span>object</span>(<span>DtoStudentSummary</span>))
<span>@Core</span>.<span>serializer</span>()
<span>async</span> <span>summary</span>(
  <span>@Arg</span>.<span>param</span>(<span>'id'</span>, v.<span>tableIdentity</span>()) <span>id</span>: <span>TableIdentity</span>,
): <span>Promise</span><<span>DtoStudentSummary</span> | <span>undefined</span>> {
  <span>return</span> <span>await</span> <span>this</span>.<span>scope</span>.<span>service</span>.<span>student</span>.<span>summary</span>(id);
}

返回 DTO 再声明具体字段。比如,后端新增 summaryText 后,契约源就在这里:

<span>// vona/.../training-student/src/dto/studentSummary.tsx</span>
<span>@Dto</span><<span>IDtoOptionsStudentSummary</span>>()
<span>export</span> <span>class</span> <span>DtoStudentSummary</span> <span>extends</span> <span>$Dto.get</span>(<span>() =></span> <span>ModelStudent</span>, {
  <span>columns</span>: [<span>'id'</span>, <span>'name'</span>, <span>'mobile'</span>, <span>'level'</span>],
}) {
  <span>@Api</span>.<span>field</span>(v.<span>title</span>($locale(<span>'LevelTitle'</span>)))
  <span>levelTitle</span>: <span>string</span>;

  <span>@Api</span>.<span>field</span>(v.<span>title</span>($locale(<span>'Summary'</span>)))
  <span>summaryText</span>: <span>string</span>;
}

2. 重新生成 Zova 的 API 和类型

先确保 Vona 的 Swagger 输出已经包含这个字段,再运行:

npm run zova :openapi:generate training-student

这一步会更新 Zova 模块的生成结果,例如 API 方法、OpenAPI response type 和 schema facade。不要直接修改这些生成文件;它们会在下一次生成时被覆盖。

3. 前端直接消费生成的 API

生成后,Zova 的 API surface 会提供 trainingStudent.summary(...) 及其响应类型。前端 Model 可以用一个很薄的方法包装它:

<span>// zova/.../training-student/src/model/student.ts</span>
<span>summary</span>(<span>id: TableIdentity</span>) {
  <span>return</span> <span>this</span>.<span>$$modelResource</span>.<span>queryItem</span>({
    id,
    <span>action</span>: <span>'summary'</span>,
    <span>queryFn</span>: <span>async</span> () => {
      <span>const</span> res = <span>await</span> <span>this</span>.<span>scope</span>.<span>api</span>.<span>trainingStudent</span>.<span>summary</span>({
        <span>params</span>: { id },
      });
      <span>return</span> res ?? <span>null</span>;
    },
  });
}

页面或表格操作只需要调用 student.summary(id),就能获得包含 summaryText、levelTitle 等字段的结果。前端不需要再手写一份 StudentSummary 接口:后端 DTO 改变后,重新生成,调用处会继续使用新的类型。

这个例子的完整链路是:Vona DTO → Swagger / OpenAPI → openapi:generate → Zova API → Model / 页面。

Zova → Vona:前端资源变化时

有些事实属于前端。例如,一个自定义表单字段、表格单元格、路由或图标的具体实现权在 Zova。Vona 需要引用它们的稳定资源身份,但不会执行前端组件源码。

当前仓库的 training-student 模块有一个简单例子:Vona 定义学生等级的业务含义和可选值;Zova 则实现对应的等级选择控件和等级 badge。

Vona:Level 是什么、可取哪些值、页面应使用哪个 renderer key
  → Zova:实现这个 key 对应的表单字段和表格单元格
  → 构建并同步交接物
  → Vona 可以安全引用更新后的前端资源

当这类 Zova 资源发生变化时,使用对应 flavor 的完整构建和同步流程。例如 Admin:

npm run build:zova:admin
npm run deps:vona

这两步在代码中的作用,可以用 training-student 的等级 renderer 简化表示。Zova 先声明稳定的 renderer key,并实现具体的表单控件:

<span>// zova/.../training-student/src/component/formFieldLevel/controller.tsx</span>

<span>declare</span> <span>module</span> <span>'zova-module-a-openapi'</span> {
  <span>export</span> <span>interface</span> <span>IResourceFormFieldRecord</span> {
    <span>'training-student:formFieldLevel'</span>?: <span>IResourceFormFieldLevelOptions</span>;
  }
}

<span>@Controller</span>()
<span>export</span> <span>class</span> <span>ControllerFormFieldLevel</span> <span>extends</span> <span>BeanControllerBase</span> {
  <span>protected</span> <span>render</span>(<span></span>) {
    <span>const</span> { items = [], itemValue = <span>'value'</span>, itemTitle = <span>'title'</span> } = <span>this</span>.<span>$props</span>.<span>options</span> ?? {};
    <span>return</span> (
      <span><span><<span>div</span>></span>
        {items.map(item => (
          <span><<span>button</span> <span>key</span>=<span>{String(item[itemValue])}</span> <span>type</span>=<span>"button"</span>></span>
            {item[itemTitle]}
          <span></<span>button</span>></span>
        ))}
      <span></<span>div</span>></span></span>
    );
  }
}

npm run build:zova:admin 会生成 Admin 的 SSR 和 REST 交接产物,npm run deps:vona 再把这份产物同步到 Vona。同步完成后,Vona 的 DTO / 字段元数据就可以引用这个 key,并传入类型化的选项:

<span>// vona/.../training-student/src/entity/student.tsx</span>
<span>@Api</span>.<span>field</span>(
  v.<span>title</span>($locale(<span>'Level'</span>)),
  <span>ZovaRender</span>.<span>field</span>(<span>'training-student:formFieldLevel'</span>, {
    <span>items</span>: studentLevelItems,
    <span>placeholder</span>: $locale(<span>'Level'</span>),
  }),
  <span>ZovaRender</span>.<span>cell</span>(<span>'training-student:level'</span>, { <span>items</span>: studentLevelItems }),
  z.<span>union</span>([z.<span>literal</span>(<span>1</span>), z.<span>literal</span>(<span>2</span>), z.<span>literal</span>(<span>3</span>)]),
)
<span>level</span>: <span>number</span>;

在后端,DtoStudentSelectResItem 继承 ModelStudent,并通过 DTO 字段元数据定义列表和表单应如何呈现。前端取得这个 DTO 后,根据其中的 renderer key 和选项进行动态渲染:具体的 JSX 组件仍由 Zova 执行,DTO 本身只描述“使用哪个 renderer 以及传入什么参数”,不会直接导入或执行前端组件源码。

这里不必死记每条命令。最重要的是理解方向:后端拥有 API 与业务规则;前端拥有页面与 renderer。发生变化后,从拥有事实的一侧把契约交给另一侧。


从这里继续探索

刚接触 Cabloy 时,可以按这个顺序继续学习:

  1. 先用 9000 修改一个页面,感受 Zova 的开发和热更新体验;
  2. 再用 7102 访问同一页面,理解 Vona 是如何承载完整 SSR 请求的;
  3. 修改一个 API 字段,查看 OpenAPI 生成的前端类型如何变化;
  4. 尝试新增一个前端 renderer,了解为什么它需要构建并同步给 Vona。

随着项目变大,这套分工会让问题更容易定位:是页面开发问题、Vona 集成问题,还是契约同步问题?而不是把所有问题都归结为“前后端不一致”。

进一步阅读

先用 9000 快速创造反馈,再用 7102 证明完整运行。