zjchina 的四层架构——把复杂性藏到引擎盖下
2026 年 10 月 1 日。zjchina 从「一个站」长成了「一个框架」之后,一直欠自己一篇交代:我们一边在官网讲 ROS 2 让子系统「用同一套语言交流」,一边又在自己仓库里用四种语言各说各话(C++ 后端、Rust 前端(编译到 WASM)、JS 薄壳、鸿蒙 ArkTS 壳)。这两个说法怎么不打架?答案不是靠某一门语言一统天下,而是靠一层一层把复杂性压下去——直到开发者只剩两件事:这一页长什么样,这一页干什么。这篇文章把四层摆开,讲清楚中间那一层为什么是最重的。
一、为什么需要分层
官网讲 ROS 2,说的其实是一个老故事:运动控制、机器视觉、AI、物流,本来各说各话,一旦统一成一套消息与接口,就从「遥控器」长成了「神经系统」:
运动控制 机器视觉 AI 物流
↓ ↓ ↓ ↓
+-----------------------------+
| 统一的消息与接口 |
+-----------------------------+
↑ 从「遥控器」到「神经系统」
zjchina 借鉴的正是这个内核——统一的是接口契约,不是编程语言。ROS 2 自己就是 C++(rclcpp)、Python(rclpy)多绑定、靠 DDS 的 IDL 串起来的。
但 zjchina 的多语言更「实」:C++26 的 Drogon 后端扛并发、Rust 编译到 wasm 一库多端、JS/CSS 做浏览器薄壳、ArkTS 做鸿蒙原生壳。这是各取所长,不是叠床架屋。真正的风险只有一条——契约散在多处,一处改、多处忘。分层要解决的,就是把这个风险按层次拆开,让每一层只承担一种复杂性,谁也别越界。
二、四层全景
四层的依赖方向单向向内,越往下越像地基,越往上越像「身份」:
- L0 基岩:tokio / rustls / wasm runtime,加上自绘渲染内核(zjchina-render,wgpu + fontdue)。这一层是永不重写的——不是因为它完美,而是因为它是地基,动了就全塌。
- L1 借用:dioxus 组件 DSL、axum、pulldown-cmark 这些前端 / SSR 侧借件,加上 C++26 的 Drogon 后端(HTTP 路由 + 数据层)。我们对它只握替换权:好用就用,坏了就换,但不为它改自己的身份。
- L2 自有:zjchina-core + zjchina-ui,这才是 zjchina 的身份层。core 钉契约,ui 钉组件树,双端(SSR 与 CSR)共用同一棵树。
- L3 AI 空位:MCP / Agent 契约层。这是刻意留白的一层——不急着填,等上面的三层站稳,再让 AI 协作者从契约层长出来。
一张图看全四层:
L3 AI 框架空位 MCP / Agent 契约层 ← 刻意留白,等一件真实用例
L2 自有身份层 zjchina-core + zjchina-ui ← 契约 + 组件树,单一真源
L1 借用层 dioxus / axum / cmark + Drogon (C++) ← 只握替换权
L0 基岩 tokio / rustls / wasm + 自绘内核 ← 永不重写
依赖方向:单向向内(上层依赖下层,下层不碰上层)
四层看下来,C++ 好像缺席了——它其实藏在 L1 借用层,是 zjchina 的后端身份。在一个 Rust 当道的框架里,为什么仍保住 C++26 的 Drogon?两条理由:C++ 是全世界共享的语言,四十年的生态、共识与人手,让后端不至于被一门语言的单点风险锁死;同时,C++ 到今天仍是能和底层操作系统直接通话的语言——进程、内存、网络、文件系统的控制力,决定了后端这一层站得稳不稳。借来的东西可以替换,选它守后端,是因为这两条一时半会儿换不掉。
渲染也走一条明确路由(ADR-0005):自绘为尊,web 兜底。真机设备端(工厂大屏、机器人 HMI)没有浏览器,只能自绘;对外网走浏览器 DOM + SSR,保住 SEO 和零安装。鸿蒙是这四端里的「第四端」:一个 ArkTS 原生壳,用 ArkWeb 加载站点、复用站点的 Service Worker 做离线缓存,只借窗口能力(防截屏、全屏沉浸),不重写业务、不碰契约层。
三、契约层是一等公民
四层里最容易被当成「实现细节」忽略、却是整个框架命门的是——契约。
多语言通信的痛,从来不在「用了四种语言」,而在「契约没有被当作一等公民」。ROS 2 用 IDL 代码生成一次解决;我们手写 DTO 两边对齐,就等于把最容易出错的地方交给了最不可靠的「人肉记忆」。所以 ADR-0006 把它升格:契约层是横切 L2 与 L3 的公共语言,不是某层私有的实现细节。
做法照抄 ROS 2 的思路,只是换成自己的材料:以 zjchina-core 的 serde DTO 为单一真源,导出 Schema,两端从契约生成强类型、再加漂移锁。这样「Rust 侧改了字段」和「C++ 侧忘了同步」之间的那条裂缝,就永远缝上了。
四、把复杂性藏到引擎盖下
四条边界线的终点,指向同一句愿景:开发者只写页面布局和页面功能。
开发者:只写「这一页长什么样」「这一页干什么」
+-----------------------------------------------+
| 组件 DSL + 页面容器 |
+-----------------------------------------------+
| 引擎盖(存在,但不被日常触碰) |
| 通信 · 序列化 · 渲染 · 跨语言契约对齐 |
+-----------------------------------------------+
| L0 - L3 四层实现 |
+-----------------------------------------------+
换句话说,通信、序列化、渲染、跨语言对齐——这些都是「引擎盖下面的东西」,应该像汽车的引擎一样存在但不被日常触碰。上面的开发者面对的是一个声明式的组件 DSL 和几个页面容器,他不需要知道请求穿过 JS → Drogon → SSR → core 时发生了什么,只需要知道「这个组件显示在右上角」。
这也是为什么我们坚持 facade 单一 import 点、坚持组件层纯净展示:每多一条隐式的边界,开发者就少一分需要背的复杂性。
五、还没写完的半句
诚实地说,这张图现在只画到了七成:
- 契约层的 C++ 侧代码生成还没落地——今天 Schema 只导给自己看,还没喂给后端生成强类型结构。
- L3 的 MCP / Agent 契约层是刻意留白,等一件真实用例来填,而不是现在硬造一个。
但方向已经钉死了。四层摆齐的那天,zjchina 就能像 ROS 2 一样说这句话:我们多语言各说各话,但统一说一种接口语言——而这一次,说这句话的是我们自己的框架,不是别人家的。
分层不是为了显得复杂,是为了把复杂塞进看不见的地方;留下的那一张脸,才是要交给开发者的东西。