掘金热榜上的《10 MB 的桌面 AI 客户端:用仓颉 + 系统 WebView 替代 Electron》把语言、运行时和安装体积放在了同一个命题中。 这个选题涵盖“仓颉能不能写桌面界面”,但更具体的问题是:桌面应用是否必须把一套浏览器引擎和 Node.js 运行时一起交付给用户。
在本文默认口径——最终签名后的压缩安装包控制在 10MB 级——下,使用仓颉 CJNative 原生壳、系统 WebView、静态 Web 前端和受控桥接层,架构上可以成立。但这个数字不包括完整本地模型、推理运行时、常驻内存,也不代表所有系统都预装兼容的 WebView。只要把模型权重和推理引擎一起塞进安装包,10MB 这个目标就需要重新定义。
10MB 要拆成四种口径
本文默认把“10MB 级”定义为最终签名后的压缩安装包体积,不把它等同于解压目录、常驻内存或整套本地 AI 能力。
讨论“10MB 桌面 AI 客户端”时,至少要区分下面四种统计对象:
| 口径 | 包含内容 | 常见误读 | 推荐测量方式 |
|---|---|---|---|
| 压缩安装包 | 原生二进制、静态前端、桥接资源、安装器、签名等 | 被理解为完整 AI 能力的全部体积 | 直接读取最终发布产物的字节数 |
| 解压目录 | 安装后实际落盘的可执行文件、动态库、资源和更新组件 | 与压缩包体积混为一谈 | 递归统计安装目录,但排除用户数据目录 |
| 运行内存 | 原生壳、WebView 渲染进程、浏览器辅助进程及可能的 sidecar | 认为复用 WebView 就一定更省内存 | 使用系统进程监控工具采集完整进程树的峰值 |
| 模型与缓存 | 模型权重、推理运行时、下载缓存、用户会话和附件缓存 | 把“AI 客户端”默认理解为完整本地 AI | 与客户端安装包分开统计、分开验收 |
压缩安装包只是分发体积最小的口径。解压目录反映实际磁盘占用,运行内存反映资源成本,模型与缓存则决定 AI 能力的交付边界。项目还应在发布规范中明确“10MB”采用十进制 10,000,000 字节,还是二进制 10 × 1024 × 1024 字节,否则不同构建流水线很容易得出不同结论。
重量来自独立浏览器运行时
Electron 应用通常需要携带自己的 Chromium 和 Node.js 运行时。其安装体积的主要来源并非页面代码,而是这套独立运行环境。
换成原生壳加系统 WebView 后,收益来自交付边界的变化:浏览器引擎不再随每个客户端重复分发,而是由操作系统或独立安装的系统级运行时提供。仓颉在这里承担原生宿主和业务胶水角色,真正节省体积的是“复用已有运行时”。
这一收益并不意味着所有 WebView 都能无条件免费使用。Windows 上的 WebView2 属于可独立安装和更新的系统级运行时,macOS 使用系统提供的 WKWebView,而 Linux 通常依赖发行版提供的 WebKitGTK。如果应用要求固定浏览器版本,或者目标机器没有兼容运行时,就需要另行分发固定版本运行时。此时原本被省下的浏览器体积会重新出现,10MB 安装包也就不能继续沿用同一套预算。
因此,不宜宣称仓颉天然比 Electron 小。更准确的架构边界是:
原生壳负责应用能力,系统 WebView 负责页面渲染,模型与推理引擎按需外置,客户端安装包只承载必要代码。
仓颉与 WebView 分层协作
系统 WebView 方案可以拆成四层:
┌──────────────────────────────────────────────────┐
│ 仓颉 CJNative 原生壳 │
│ 窗口、托盘、菜单、文件、下载、更新、本地配置 │
│ ↕ │
│ 带版本的受控桥接协议 │
│ ┌──────────────────────────────────────────────┐ │
│ │ WebView 渲染层 │ │
│ │ HTML / CSS / JavaScript │ │
│ │ 聊天、设置、结果展示、流式状态 │ │
│ └──────────────────────────────────────────────┘ │
│ WebView 引擎 │
│ WebView2 / WKWebView / WebKitGTK │
└──────────────────────┬───────────────────────────┘
├─ HTTPS ──> 云端 AI API
└─ 进程或本地通道 ──> 外部推理 sidecar
仓颉原生壳应负责窗口生命周期、托盘、菜单、文件选择、下载、更新、本地配置和系统集成。前端只处理交互和展示,不直接拥有文件系统、Shell 或任意系统调用权限。
这种分工还可以把敏感逻辑留在原生侧。例如 API 密钥不必进入 JavaScript 环境,前端发送 chat.start,原生层完成凭据读取、请求发送、流式转发与取消。对外部本地引擎,也由原生壳负责启动、监控和退出 sidecar,避免页面直接访问任意本地端口。
代价同样明确:Electron 提供的浏览器窗口、preload、IPC、存储、更新等能力不会自动出现。迁移的主要工作集中于重新实现这些桌面层能力,这远比仅仅更换应用语言复杂。
仓颉原生壳的能力边界
资料显示,仓颉 1.0.0 LTS 于 2025 年 7 月 1 日发布,当前发布资料只提供 CJNative 后端 SDK,并配套 VS Code 插件。
CJNative 与仓颉语言本身能提供一些有利条件:静态强类型与类型推断有助于提前发现接口错误,自动内存管理以及运行时数组越界、溢出检查可以降低部分内存问题,C 语言互操作便于接入系统原生库,原生协程则适合并发网络与文件任务。
但语言能力和应用层能力必须分开看。仓颉不会自动提供 Electron 式的窗口对象、preload 注入、IPC、托盘、更新器和 WebView 宿主。即便未来这些组件以库的形式出现,也仍然需要围绕具体平台的 WebView 实现、安全边界和生命周期进行适配。
原生协程也不能替代取消机制。一次生成请求仍然需要明确的状态机、终止信号和资源回收;C 互操作解决了调用系统库的问题,却没有自动解决线程归属、ABI 兼容和回调生命周期。本文因此只讨论 CJNative,不把尚未普及的 CJVM 纳入主方案。
Windows 环境先跑通
Windows 开发环境应先完成一个最小 CJNative 闭环,再开始 WebView 和桥接工作。
-
从官方渠道下载并安装仓颉 1.0.0 LTS Windows 安装包,同时安装配套 VS Code 插件。安装包名称、SDK 路径和插件版本应以当期官方发布页为准。
-
在 IDE 中配置 CJNative SDK 路径,并将编译工具加入
PATH。随后检查编译器版本:
cjc --version
确认输出对应 1.0.0,而不是混用了其他工具链或后端。
-
用 VS Code 创建一个最小 CJNative 控制台项目,编译并运行。这个阶段只验证 SDK、编译器和插件能够协同工作,不引入 WebView、Node.js 或额外运行时。
-
接入一个系统 WebView,加载本地静态页面。先只实现窗口创建、页面加载和一个
app.ready握手,再逐步加入流式消息。 -
建立发布目录,只复制运行所需资源,并验证从干净用户目录启动。
建议先保持这样的目录边界:
desktop-ai/
├─ native-shell/ # 仓颉 CJNative 原生代码
├─ web/ # 前端源码
├─ web-dist/ # 生产构建产物
├─ platform/
│ ├─ windows/
│ ├─ macos/
│ └─ linux/
├─ scripts/
└─ dist/
这样可以避免开发依赖、测试夹具和模型文件被误装进最终安装包。
桥接层承载流式与取消
WebView 与原生壳之间不应暴露“执行命令”“读取任意路径”“打开任意 URL”之类的宽泛接口。更稳妥的方式是使用带版本、请求标识和明确事件类型的消息协议。
{"v":1,"id":"req-42","type":"chat.start","payload":{"conversationId":"conv-7","text":"hello"}}
{"v":1,"id":"req-42","type":"chat.delta","seq":1,"payload":{"text":"你好"}}
{"v":1,"id":"req-42","type":"chat.delta","seq":2,"payload":{"text":",有什么可以帮你?"}}
{"v":1,"id":"req-42","type":"chat.done","seq":3,"payload":{}}
取消消息由前端发往原生层:
{"v":1,"id":"req-42","type":"chat.cancel","payload":{}}
协议中的几个字段承担不同职责。v 用于协议升级,id 用来关联同一次请求,type 限定允许执行的动作,seq 保证流式事件顺序。错误还需要独立的终止事件,避免页面把超时、取消和服务端错误混成普通文本。
实现时需同时落实以下要求。原生层必须校验消息类型、字段和大小,并只接受允许的页面来源。前端不能提交任意文件路径,附件应通过原生文件选择器获得句柄或受控副本。API 凭据应进入系统凭据存储,确保前端只能使用能力,不能读取密钥。取消操作需要真正终止上游请求,并确保每个请求最终只终结一次,状态只能是完成、错误或取消。当流式事件积压时,原生层应设置有界队列和明确的溢出策略,同时保持语义顺序。此外,页面重载后应重新执行握手并恢复会话快照,原生层则需要清理已经失效的请求。
流式输出不应只依赖“把字符串不断拼接起来”。更可靠的实现是让原生层成为请求状态的唯一所有者,前端只负责渲染已经确认顺序的事件。
导航兼容需要独立回归
资料收录的 React Native WebView 案例记录了一个导航事件失效现象:在 13.7 及以上版本中,首次加载页面可以通过 window.reactNativeWebView.postMessage() 正常触发导航事件,但后续导航事件可能出现失效。
这不是仓颉原生 WebView 的直接证据,却说明导航和桥接不能依赖单一加载事件,更不能只在首次启动时测试一次。
至少应建立以下回归场景:
| 场景 | 需要验证的结果 |
|---|---|
| 首次加载 | WebView 创建后能完成 app.ready 握手 |
| 单页应用路由切换 | 地址变化后桥接仍然可用 |
| 页面刷新 | 旧请求被取消,新页面重新取得会话状态 |
| 重定向或跨域跳转 | 非白名单导航被阻止 |
| 流式生成中导航 | 已断开的事件不会写入新页面 |
| WebView 进程重建 | 原生层能够识别页面实例变化并重新绑定 |
| 后台再回前台 | 未完成请求、超时和连接状态保持一致 |
协议设计上应区分“文档加载完成”和“桥接通道可用”。前者只能说明页面可能已经渲染,后者需要由明确的握手消息确认。
三平台打包各有依赖
同一套前端代码在三平台上并不天然等价。WebView 引擎、资源来源、权限回调和安装包认证都需要分别处理。
| 平台 | 引擎与运行时 | 资源与存储 | 权限与安全 | 发布要求 |
|---|---|---|---|---|
| Windows | WebView2;优先复用 Evergreen 运行时,缺失时引导安装,也可选择固定版本运行时 | 建议映射到受控虚拟主机或本地源;Cookie、缓存和用户数据放在独立目录 | 处理导航、下载、摄像头、麦克风等权限请求;校验消息来源 | 使用 Authenticode 签名;固定运行时必须另行计入分发体积 |
| macOS | 系统 WKWebView,版本随操作系统变化 | 可使用文件访问或自定义 Scheme;按需求选择持久化或临时数据存储 | 处理文件、剪贴板和媒体权限;限制目录访问范围 | 使用 Developer ID 签名、公证和 stapling |
| Linux | WebKitGTK,依赖发行版包及兼容版本 | 自定义 Scheme、文件访问和缓存行为受工具包及发行版影响 | 权限提示、门户和沙箱行为可能不同 | 按目标发行版处理 DEB、RPM 或其他包;签名机制取决于分发渠道 |
Windows 的关键风险是运行时并非在所有机器上都存在。若产品坚持固定 WebView2 版本,应把固定运行时作为独立成本计算,而不是继续宣称整个下载量只有 10MB。
macOS 的 WKWebView 随系统提供,能够减少分发体积,但应用测试必须覆盖受支持系统版本。Linux 的差异更大,不能只在一两个桌面发行版上验证。WebKitGTK 版本、字体、文件对话框、通知和门户行为都可能变化,CI 应保存发行版矩阵,而不是只记录“Linux 通过”。
安装包里真正要装什么
安装包应围绕运行时必需项进行构建,不应将开发资产和可外置能力混入其中。
| 内容 | 是否进入安装包 | 说明 |
|---|---|---|
| 仓颉原生二进制 | 是 | 仅包含 Release 产物 |
| 静态前端资源 | 是 | 使用内容哈希,避免重复文件 |
| 桥接清单与版本信息 | 是 | 明确协议版本、权限和允许的消息类型 |
| 必需的动态库 | 视情况 | 只携带目标系统确实不提供的依赖 |
| 更新客户端与元数据格式 | 是 | 实际更新元数据由签名服务分发 |
| 第三方许可证与 NOTICE | 是 | 覆盖前端依赖和链接的原生库 |
| 系统 WebView 运行时 | 否 | 复用已有运行时或单独分发 |
| 模型权重与推理引擎 | 否 | 另行下载,或作为独立 sidecar 分发 |
| 用户缓存、日志、会话数据 | 否 | 安装后写入用户数据目录 |
| 调试符号 | 通常否 | 单独上传符号服务,供崩溃分析使用 |
| Source Map | 通常否 | 按安全策略上传,不直接暴露源码路径和业务代码 |
在构建产物管理上,“不安装”与“项目里没有”要分开处理。模型和 sidecar 可以由应用按需下载,也可以由用户提前安装,但必须进入单独的体积和许可证清单。否则,客户端安装包虽然是 10MB,用户首次使用时的真实下载量却可能远高于这个数字。
Electron 能力需要逐层重写
功能迁移不能仅通过更换 WebView 组件来实现。窗口、桥接、通知、更新和快捷键等能力虽有对应的平台接口,但仍需处理生命周期、注入时机、错误语义及权限隔离等差异。
| Electron 能力 | 原生壳对应方案 | 需要注意的差异 |
|---|---|---|
BrowserWindow |
原生窗口加 WebView2、WKWebView 或 WebKitGTK 宿主 | 尺寸、生命周期、焦点、全屏和关闭事件都要重新接入 |
preload |
注入本地启动脚本或窄桥接对象 | 各平台注入时机不同,不能假设完全一致 |
ipcMain / ipcRenderer |
带版本的请求与事件桥接 | 需要自行设计序列化、校验、错误和取消语义 |
| 上下文隔离 | WebView 隔离机制加最小权限桥接 | 页面不能直接持有原生对象或敏感配置 |
webContents 事件 |
导航、权限、下载、崩溃等平台回调 | 事件名称和触发时机需要逐平台适配 |
| 托盘与菜单 | 系统托盘、菜单和状态栏 API | macOS、Windows、Linux 的交互差异明显 |
| 系统通知 | 原生通知 API | 通知权限和点击回调需要单独处理 |
| 文件对话框 | 原生文件选择与保存接口 | 返回路径不能直接视为可信输入 |
| Cookie、缓存和存储 | WebView 用户数据目录与存储 API | 数据隔离、清理和保留策略不完全一致 |
shell.openExternal |
受控的外部链接打开接口 | 必须限制协议和目标域名 |
autoUpdater |
自建更新客户端、签名服务与回滚机制 | 要自行实现元数据校验、差分升级和失败恢复 |
| 全局快捷键 | 平台全局热键接口 | 冲突检测和失焦行为需要单独测试 |
| 原生 Node 模块 | C 互操作或平台原生库绑定 | Node 生态中的多数包不能直接迁移 |
utilityProcess |
外部 sidecar 进程管理 | 需要自行处理启动、通信、退出和升级 |
Electron 最难替代的是其一致性。固定 Chromium、完整 Node.js、npm 桌面插件和统一的渲染行为,都不是换一个系统 WebView 就能保留的。迁移时应把“能力缺口”列出来,再决定是否值得承担重写成本。
本地模型必须另行计量
AI 客户端的交付形态会直接决定 10MB 口径是否成立。
| 形态 | 客户端安装包 | 用户实际获得的能力 | 主要代价 |
|---|---|---|---|
| 纯云端 API | 可以控制在 10MB 级 | 模型能力由服务端提供 | 网络依赖、API 成本、数据离开本机 |
| 调用外部本地引擎 | 客户端可以控制在 10MB 级 | 本地模型由独立程序提供 | 用户总下载量包含引擎和权重,进程通信更复杂 |
| 随包分发完整推理工具 | 10MB 客户端口径失效 | 离线能力随安装包一起交付 | 权重、运行时、许可证和硬件适配都进入成本 |
因此,“支持本地模型”不等于“把模型装进客户端”。外部 sidecar 是维持小安装包最现实的方式之一,但宣传时应同时给出客户端、sidecar、模型和缓存的完整占用。
sidecar 优先使用父子进程的标准输入输出通信,可以避免开放本地端口。如果必须使用 loopback 端口,应只监听回环地址,并使用每次启动随机生成的会话令牌。原生壳还应校验 sidecar 版本,握手失败时明确报错,不能让页面连接到同机上的其他进程。
安装包与内存要分开测量
系统 WebView 减少的是随包分发的运行时内容,不会自动消除渲染进程、缓存和 GPU 相关开销。
| 指标 | 测量方式 | 需要避免的误读 |
|---|---|---|
| 最终安装包体积 | 对签名后的最终产物直接取字节数 | 用未签名包或压缩目录代替 |
| 解压目录体积 | 递归统计发布目录,排除用户数据 | 与压缩安装包使用同一数字 |
| 冷启动 | 清空应用用户数据后,从进程启动到首个可交互状态计时 | 把操作系统缓存命中当作完全冷启动 |
| 热启动 | 分别测试进程仍存活和进程已退出的再次启动 | 混用两种启动路径 |
| 首屏与首个响应 | 分别记录页面可交互、请求发出和首个流式分片到达 | 只测窗口出现时间 |
| 峰值内存 | 采集原生壳和相关 WebView 进程的峰值工作集 | 只看主进程 RSS |
| 稳定性 | 固定脚本下执行长对话、取消、刷新和重连 | 用一次短对话代替压力测试 |
在评估时,应使用相同的前端、同一套 API、相同的测试脚本以及相同的操作系统版本,比较 Electron 版本和原生壳版本。
如果迁移后安装包明显缩小,但峰值内存上升,也不应视为失败。磁盘分发成本和运行资源成本本来就是两个问题。团队需要明确当前产品更受哪一项约束。
签名更新与安全仍是底线
缩小安装包不会降低桌面应用的安全要求。原生壳拥有文件系统、凭据和进程权限,一旦桥接被滥用,影响范围可能比普通网页更大。
WebView 页面应使用严格 CSP,并只允许必要的连接目标:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; connect-src https://api.example.com; object-src 'none'; base-uri 'none'; form-action 'none'">
如果前端框架需要内联样式,应使用构建期 nonce 或 hash,而不是直接放开 unsafe-inline。连接 sidecar 时,也应把 connect-src 精确限制到对应的本地来源。
除了 CSP 配置,还应落实以下安全要求。前端 JavaScript 应随安装包本地分发,不从远程页面动态加载业务代码。导航操作必须使用白名单,外部链接由原生层确认协议后再打开。桥接层需按消息类型建立白名单,不提供任意命令执行、任意文件读取或任意 URL 打开接口。API 凭据应进入 Windows Credential Manager、macOS Keychain 或 Linux 的凭据存储机制,避免写入前端资源或普通配置文件。附件操作需限定在用户主动选择的文件及受控临时目录内,以降低权限滥用风险。更新元数据必须签名,客户端内置或固定更新验证公钥,并在更新时校验包哈希和版本。更新的失败回滚、权限提升和进程替换需要事务化处理,以防止升级中断导致客户端无法启动。最后,每次启动都应记录 WebView 引擎版本及关键能力,以便定位版本回归问题。
首次加载与后续导航还需要分别测试,因为页面可能在没有明显错误的情况下失去消息通道。桥接连通性不能仅依赖窗口尺寸变化或开发者控制台日志判断。
流水线守住体积预算
要让“10MB”成为可重复的发布指标,CI 必须测量最终产物,而不是依赖一次手工打包截图。
建议在流水线中固定执行以下步骤:
- 锁定前端依赖版本并执行生产构建,将 Source Map 单独上传。
- 编译 CJNative Release 版本,裁剪调试符号并保留崩溃符号文件。
- 只收集运行必需资源,并生成依赖清单、SBOM 和许可证报告。
- 在打包并完成平台签名与公证后,测量最终安装包及解压目录的实际体积。
- CI 应将安装包大小与基线对比,超出阈值时直接阻断发布,并在此基础上执行安装、启动、签名验证、更新和回滚冒烟测试。
体积检查可以写成独立脚本。以下示例把“10MB”按 10 × 1024 × 1024 字节处理;如果项目采用十进制 MB,应改成 10_000_000。
from pathlib import Path
INSTALLER = Path("dist/ai-client-setup.exe")
UNPACKED = Path("dist/unpacked")
LIMIT = 10 * 1024 * 1024
def directory_size(path: Path) -> int:
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
installer_size = INSTALLER.stat().st_size
unpacked_size = directory_size(UNPACKED)
print(f"installer: {installer_size} bytes")
print(f"unpacked: {unpacked_size} bytes")
print(f"limit: {LIMIT} bytes")
if installer_size > LIMIT:
raise SystemExit("installer size budget exceeded")
在 CI 中执行:
python3 scripts/check_budget.py
Windows、macOS 和 Linux 应分别输出自己的最终安装包大小。压缩算法、文件时间戳、安装器框架和签名都会影响结果,因此还需要固定构建环境和压缩参数。如果可复现构建暂时无法实现,至少应保留产物哈希、依赖锁文件和构建日志,方便定位体积变化来自哪里。
Electron 在特定场景仍然合适
系统 WebView 不是 Electron 的普遍替代品。如果产品依赖固定 Chromium 版本,或者需要完整 Node.js 与 npm 生态,Electron 的额外体积通常比迁移成本更容易接受。需要跨平台像素级一致渲染时,固定运行时比系统 WebView 更容易控制。此外,如果产品大量依赖现成的桌面插件、窗口管理或系统集成能力,或者团队更看重功能交付速度,Electron 依然具备明显优势。
如果客户端主要由 HTML 界面构成,原生能力集中在窗口、托盘、文件、更新和少量受控接口,而且安装体积是明确约束,那么“原生壳加系统 WebView”的路线更值得投入。
迁移成本不能只用最终安装包节省了多少来衡量。WebView 版本矩阵、平台权限差异、更新回滚、桥接协议和系统兼容测试都会进入长期维护成本。Electron 的体积是显性的,WebView 方案把一部分成本转移到了运行时差异和兼容性回归上。
10MB 之后仍有未解问题
系统 WebView 版本漂移仍是最难回避的问题。Windows WebView2 的更新节奏与客户端发布节奏并不一致,同一个安装包在不同机器上可能获得不同引擎版本。macOS 受系统升级影响,Linux 则可能面对更分散的 WebKitGTK 版本。
运行时缺失也不能完全依赖用户自觉解决。Windows 和 Linux 都需要可靠的检测、引导安装或独立运行时分发策略。一旦为了确定性而随包携带固定运行时,体积目标就会发生根本变化。
跨平台 API 不一致、流式桥接稳定性和协议升级也没有因为换用原生壳而消失。附件、通知、下载、后台恢复和页面重载都需要长期回归测试。安装包可复现性、不同平台的签名认证、更新包校验与失败回滚,同样需要投入完整的工程能力。
本地模型会再次改变体积方程。只要模型权重和完整推理运行时进入安装包,10MB 客户端的口径就不再成立;如果模型外置,则必须如实说明用户实际承担的是客户端、sidecar、模型与缓存的总成本。
10MB 这个指标要求团队明确分发边界和能力归属,但不能据此证明某项技术在所有场景都具备绝对优势。
[email protected]
。
我们承诺在核实信息后,将第一时间删除相关内容或断开链接。























暂无评论内容