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

刚开始运行 Cabloy Basic 时,通常会看到两类命令:

bash 复制代码
# 启动完整的 Cabloy 服务
npm run dev

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

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

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

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

两个端口,两种工作节奏

7102:Vona集成式SSR

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

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

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

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

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

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

9000:Zova独立式SSR

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

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

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

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

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


Cabloy 项目的全栈原理

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

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

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

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

text 复制代码
浏览器请求
  → 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:

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

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

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

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

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

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

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

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

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

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

bash 复制代码
npm run zova :openapi:generate training-student

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

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

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

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

页面或表格操作只需要调用 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。

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

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

bash 复制代码
npm run build:zova:admin
npm run deps:vona

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

tsx 复制代码
// zova/.../training-student/src/component/formFieldLevel/controller.tsx

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

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

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

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

在后端,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 证明完整运行。

相关推荐
sweet丶1 小时前
为什么SIGKILL崩溃无法捕获?
全栈
nyaomaru1 小时前
你的 Type Guard 可能会悄悄地与 TypeScript 类型发生偏移 🔧
后端·typescript
怕浪猫1 小时前
DeepSeek Harness 系列图解
面试·前端框架·node.js
百万蹄蹄向前冲2 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js
前端snow2 小时前
ai agent--- 后端概念补充:Docker Compose、ElasticSearch、IK、BM25等
node.js
SL_staff2 小时前
城商行营销翻车复盘:JVS-Rules 如何用工程化机制保障规则变更的可溯性与稳定性
java·开源·全栈
EdgeEcho2 小时前
Node 里那个"只解第一帧"的坑,我用 172 行代码绕过去了
node.js
光影少年2 小时前
Redis + Node 如何支撑百万级并发
redis·后端·node.js