写在前面:这不是官方教程,是一个开发者折腾几个月后的复盘。项目叫cj-tauri,Apache-2.0 之外的代码全部 MIT,欢迎来踩。
一、为什么要造这个轮子
华为的仓颉语言开放之后,我一直在想一个问题:用仓颉能不能写出像模像样的桌面应用?
答案是:语言本身没问题,缺的是生态。Rust 那边有 Tauri——后端静态编译、前端就是系统自带的WebView,几十 MB 的运行时都不用带,安装包能压到几 MB。这个「混合开发」的架构形态非常对我胃口:UI 交给 HTML/CSS/JS(成熟、随便找人写),业务和系统能力交给编译型语言(快、稳、不解释执行)。
那仓颉缺的就是这么个东西:一个把系统 WebView 和仓颉后端粘起来的框架。
于是我写了 cj-tauri。名字直白——仓颉版的 Tauri 2:
| Tauri | cj-tauri |
|---|---|
| wry/tao(WebView 宿主) | WebViewHost+ 平台桥(C 语言写的native/) |
| invoke/command(IPC) | 双向桥:invoke/resolve/listen/emit |
| capability/permission | 能力清单 + 路径 scope(RFC-003) |
架构上有条铁律:平台差异只允许出现在两处——仓颉侧的@When条件编译、C 桥的native/platforms/<平台>/。IPC、能力校验、命令分发这些核心逻辑全部跨平台共用。
目前 Windows(WebView2)和 Linux(WebKitGTK)都实机跑通了,鸿蒙 ArkWeb 是预留位,契约文档先写好,能力位如实报 0——没做的绝不假装做了。
顺带一提工程上的选择:C 桥不用 FFI 直接从仓颉怼 Win32/GTK,是因为两边运行时差异太大(仓颉是 M:N 轻量线程,堆上协程栈会被 JSC 的栈边界校验直接 abort,GTK 的全部调用必须在原生 pthread 里跑)。C 是两边都服气的中间人。
项目地址:https://yangyongzhen.github.io/cj-tauri/
二、拿它写了个真的应用:Markdown 笔记软件
框架光有 demo 不算数,得拿它写个自己每天会用的东西。于是有了markdown-notes:左侧文件树、中间编辑、右侧实时预览,Typora 那种路子。
整个二进制包4.9M ,是时候换掉Typora啦。
windows下二进制包地址: https://atomgit.com/qq8864/cj-tauri/tree/main/examples/markdown-notes/dist-single
几个我比较得意的细节:
安全模型是真的在工作。页面对文件系统没有任何直接访问,只能调 fs 插件的五个命令;而能力清单里写着路径 scope——
notes/工作区和$HOME白名单之外一律被拒,.ssh、.git、*.secret.md在 deny 列表里先拦一道。前端自己不做任何路径检查,把路径原样交给命令,判定只在框架共享层的一个文件里,不存在第二份会写错的实现。-渲染器是自写的,
createElement+textContent构造,不碰innerHTML——笔记正文从结构上就不可能是注入点。页内自检里专门有一条断言:往正文里塞<img src=x onerror=alert(1)>,验证它只是个无害的文本节点。后来预览升级到了marked + Highlight.js(vendor 进仓库、装配期内联),但白名单清洗那层没松过:脚本、iframe、事件属性、javascript:一律剥掉。五种界面主题(跟随系统 / 浅色 / 深色 / 护眼绿 / 墨黑),全是 CSS 变量换肤, 选择记忆在 store 插件里,重启还在。
打包出来是一个 4.8 MB 的 exe。页面、vendor 库、能力清单全部 base64 编进二进制,双击就跑,不需要任何运行时安装——主题跟随系统、localStorage 持久这些体验都还在。 WebView2 的用户数据档案也挪去了
%LOCALAPPDATA%,分发目录干干净净只有 exe 和你的笔记。
三、这轮踩的坑(都修了,过程比结论有意思)
写框架和写应用的区别在于:应用出 bug 你改应用,框架出 bug 所有应用陪葬。最近一轮Windows 适配,连续踩了几个很典型的:
1. 「我只用了三个插件,为什么要链 SQLite?」
把 markdown-notes 拿到 Windows 上构建,链接器开口就要sqlite3_libversion。原因很物理:仓颉的链接粒度是「包 = 静态库」,之前十几个插件全挤在cjTauri.plugins一个包里,编成一个对象文件——你 import 任何一个,整个包连着 sqlite 的 FFI 桩一起进链接。
修法是把插件拆成一个插件一个子包,各编各的静态库,没引用就不链接。拆完之后fs/store/dialog 三件套的产物导入表里干干净净,sqlite 一个字节都没有。
教训:链接粒度是包级的,包的组织方式就是依赖面。插件多了以后这事躲不掉,早拆早清净。
2.$HOME/**写了等于没写
测试时发现打开桌面上的 md 文件被拒——按清单里的$HOME/**明明该放行。查下来是分隔符:$HOME在 Windows 展开出USERPROFILE原值C:\Users\yang(反斜杠),而页面路径走了规范化变成正斜杠,白名单匹配是逐字节比的,C:\Users和c:/users` 永远不相等。修复是让清单模式和页面路径走同一套分隔符归一,顺手把 Windows 文件系统不区分大小写也折进去。
这种 bug 最阴的地方在于:它在 Linux 上永远不复现,清单又是声明式的,看起来「写了」,实际是死的。
3. 同一个文件在列表里出现两次,点第二次就报错
「打开」对话框选中工作区里的文件,我为了让它出现在侧栏,往列表里插了一条完整路径的条目——但工作区条目在列表里存的是裸文件名,同一文件于是有了两个身份。点裸名那条正常,点路径那条,前端把「含斜杠」当成「绝对路径」放行,落盘就成了notes/notes/x.md。修复是只对真正的绝对路径(盘符开头)做补录。
顺藤摸瓜还带出另一个:原生对话框在 Windows 返回的是反斜杠路径,而前端判断「是不是绝对路径」只认/——于是一切 Windows 路径都被当成相对路径拼进工作区。这种「Linux 上对、Windows 上错」的分支,真是每个跨平台项目都要交一遍学费。
4. 菜单勾选框可以同时全亮
视图菜单的单选组居然能勾出「既编辑又预览」。查下来是最无聊也最经典的一种 bug:匹配菜单事件报文时写了"id":"view.mode."(带闭合引号)当前缀去匹配,而真实报文是"id":"view.mode.edit"——多写的那半个引号让匹配永远落空,整组勾选态回写从未执行过。函数注释里现在留了一行给后人:前缀匹配不要求闭合引号。
四、现在到哪一步了
截至这篇写下的时候(2026 年 10 月),cj-tauri 0.8.0:
- 15 个示例,从 hello 到串口示波器、MQTT 客户端、音乐播放器,各有各的实机取证
- 12 个官方插件,全部按「一个插件一个子包」组织——应用只链接自己用到的
- 262 个单元测试+ 每个示例的页内自检 + Linux/Windows 双平台的 E2E 入口,证据落成可对账的 JSON(断言名、命中次数、运行时版本),不是一句「跑过了」
- 脚手架 CLI(
cj-tauri create / dev / build / run / info),Vue、React 模板可选,零 Node 的内联模板是缺省 - Windows、Linux 实机跑通;鸿蒙 ArkWeb 有契约文档和目录占位,如实报 0
markdown-notes 是我拿来当「能力活样板」的那个:文件 IPC、路径 scope、store 记忆、原生菜单、主题切换、单文件分发,一个应用把框架的关键面都踩了一遍。
五、几句真心话
「用新语言写桌面框架」这件事,听起来像在重复造轮子。但这几个月下来我的体感是:
轮子不重要,轮子上的安全螺纹才重要。能力清单、路径 scope、命令三处联动、「声明 ≠ 放行」——这些设计不是从 Tauri 抄来的装饰,是这类混合开发架构真正值钱的部分。
仓颉版的差别只在执行语言和运行时,安全模型必须一步到位,不能等出了事再补。
另一个体感是:跨平台的 bug 都藏在「看起来一样」的地方。路径分隔符、大小写、编码码页、对话框返回值——每一个都在 Linux 上对、Windows 上错。对付它们的办法只有实机取证:每个行为变更都留下日志证据,不靠「我这边跑过了」。
项目在 https://atomgit.com/qq8864/cj-tauri 上,MIT 协议,README 里有各示例的运行截图。如果你正好在学仓颉,或者想找一个「后端是静态编译的、前端是 WebView 的」轻量桌面方案,欢迎拿去玩;markdown-notes 的 exe 可以直接下载双击,不需要装任何 SDK。踩到坑了欢迎提 issue——大概率是我还没踩到的那种。