10MB 级桌面 AI 客户端如何替代 Electron

掘金热榜上的《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. 从官方渠道下载并安装仓颉 1.0.0 LTS Windows 安装包,同时安装配套 VS Code 插件。安装包名称、SDK 路径和插件版本应以当期官方发布页为准。

  2. 在 IDE 中配置 CJNative SDK 路径,并将编译工具加入 PATH。随后检查编译器版本:

cjc --version

确认输出对应 1.0.0,而不是混用了其他工具链或后端。

  1. 用 VS Code 创建一个最小 CJNative 控制台项目,编译并运行。这个阶段只验证 SDK、编译器和插件能够协同工作,不引入 WebView、Node.js 或额外运行时。

  2. 接入一个系统 WebView,加载本地静态页面。先只实现窗口创建、页面加载和一个 app.ready 握手,再逐步加入流式消息。

  3. 建立发布目录,只复制运行所需资源,并验证从干净用户目录启动。

建议先保持这样的目录边界:

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 必须测量最终产物,而不是依赖一次手工打包截图。

建议在流水线中固定执行以下步骤:

  1. 锁定前端依赖版本并执行生产构建,将 Source Map 单独上传。
  2. 编译 CJNative Release 版本,裁剪调试符号并保留崩溃符号文件。
  3. 只收集运行必需资源,并生成依赖清单、SBOM 和许可证报告。
  4. 在打包并完成平台签名与公证后,测量最终安装包及解压目录的实际体积。
  5. 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 这个指标要求团队明确分发边界和能力归属,但不能据此证明某项技术在所有场景都具备绝对优势。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容