刚开始运行 Cabloy Basic 时,通常会看到两类命令:
# 启动完整的 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 代表的是完整的运行路径:
浏览器
→ 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:
// 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 后,契约源就在这里:
// 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 输出已经包含这个字段,再运行:
npm run zova :openapi:generate training-student
这一步会更新 Zova 模块的生成结果,例如 API 方法、OpenAPI response type 和 schema facade。不要直接修改这些生成文件;它们会在下一次生成时被覆盖。
3. 前端直接消费生成的 API
生成后,Zova 的 API surface 会提供 trainingStudent.summary(...) 及其响应类型。前端 Model 可以用一个很薄的方法包装它:
// 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。
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,并实现具体的表单控件:
// 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,并传入类型化的选项:
// 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 时,可以按这个顺序继续学习:
- 先用
9000修改一个页面,感受 Zova 的开发和热更新体验; - 再用
7102访问同一页面,理解 Vona 是如何承载完整 SSR 请求的; - 修改一个 API 字段,查看 OpenAPI 生成的前端类型如何变化;
- 尝试新增一个前端 renderer,了解为什么它需要构建并同步给 Vona。
随着项目变大,这套分工会让问题更容易定位:是页面开发问题、Vona 集成问题,还是契约同步问题?而不是把所有问题都归结为“前后端不一致”。
进一步阅读
- GitHub: github.com/cabloy/cabl…
- Docs: cabloy.com
- Demo: cabloy.com/demo
先用 9000 快速创造反馈,再用 7102 证明完整运行。