cj-tauri:用仓颉写桌面与鸿蒙 PC 应用

它是什么

一句话:cj-tauri 是仓颉语言版的 Tauri------后端用仓颉静态编译,界面仍然用 HTML/CSS/JS 写,渲染交给系统自带的 WebView,两者之间由一套 IPC 双向桥和权限白名单连起来。

这么做的理由很直接。桌面应用的界面层历来只有两条路:

  • 把浏览器打包进去(Electron 系):开发体验最好,代价是每个应用背一份 Chromium,安装包上百兆起步,内存也实打实吃在用户机器上;
  • 复用系统自带的 WebView(Tauri 系):界面照样用 Web 写,但渲染交给操作系统的 WebView,后端只管业务逻辑并直接调系统能力。产物小、启动快,也不用跟着浏览器内核升级。

cj-tauri 走的是第二条,只是把后端从 Rust 换成仓颉。换成仓颉带来两件事:一是静态编译的单文件产物(仓库里十个工具实测 4.7 MB--32.8 MB,双击即用,不装任何运行时);二是同一份业务代码能同时出 Windows、Linux 和鸿蒙 PC(2in1) 的产物------鸿蒙侧的宿主写在仓颉里,直接接 ArkWeb。

维度 Tauri 2 cj-tauri
后端语言 Rust 仓颉(cjc 静态编译)
前端 任意 Web 技术 任意 Web 技术(含 Vue 3 / React 18 模板)
前端桥 API @tauri-apps/api 的 invoke/emit/listen window.__CJ_TAURI__ 的 invoke/emit/listen
命令定义 #[tauri::command] + Builder 注册 CommandHandler 子类 + TauriApp().register()
权限模型 capabilities/*.json capabilities/*.json(同构:命令/事件白名单,默认最小权限)
目标平台 Windows / Linux / macOS / 移动端 Windows(WebView2) / Linux(WebKitGTK) / 鸿蒙 PC(ArkWeb)

调用形态刻意贴着 Tauri 走,会写 Tauri 的人基本不用重新学。

现在能做什么

当前版本 2.1.3,已经跑通的东西(每一项都有实机取证,不是路线图):

  • 三平台宿主:Windows 走 WebView2、Linux 走 WebKitGTK 4.1、鸿蒙 PC 走 ArkWeb(宿主写在仓颉侧,多窗口 label 路由已在真机取证);
  • 14 个官方插件 :fs / dialog / clipboard / menu / notification / http / ws / shell / sqlite / store / log / redis / mqtt / serial;
  • 四道白名单 :命令、事件、路径 scope、出站 host(http/ws)------默认全拒,声明才放行;
  • 33 个示例工程 :从最小 hello-world 到音乐播放器、Markdown 笔记、串口示波器、判题器、AI 聊天客户端,每个都带无人值守自检;
  • 350 个单元测试 + C 桥桩自检 259 条断言,scripts/test.sh 一条命令跑完;
  • 单文件分发 :scripts/pack-win-single.sh / winbuild.sh 打静态链接的 GUI 子系统 exe,页面与能力清单编进二进制。

架构:一次 invoke 走完的链路

javascript 复制代码
前端页面 (HTML/CSS/JS)
  │  window.__CJ_TAURI__.invoke('greet', { name: '仓颉' })
  ▼
注入的桥 JS(document-start 注入,早于页面脚本)
  │  JSON over postMessage
  ▼
WebView 宿主 host.cj
  ├─ Windows: host_webview2.cj + native/platforms/windows/bridge_win.c
  └─ Linux/鸿蒙: host_webkit.cj / ArkWeb 宿主 + 对应平台桥
  ▼
IPC 消息桥 ipc_hub.cj   ← 反序列化 / 校验 / 分发(命令在 worker 线程执行)
  ▼
能力模型 capability.cj  ← 命令与事件白名单,未声明直接拒
  ▼
你注册的 CommandHandler

几条设计上的硬约束,值得在动手之前知道:

  1. 平台差异只允许出现在两处 ------仓颉侧的 @When 条件编译,和 C 桥的 native/platforms/*/bridge_*.c。IPC、能力校验、命令分发跨平台共用,不存在「Linux 一套逻辑、Windows 另一套」。
  2. 命令是异步分发的 :handleInvoke 只做校验,通过后 spawn 到 worker 线程执行,结果经 jsSink 回投。所以慢命令(shell:exec、原生对话框)不会钉住宿主 UI 线程;反过来,worker 里也只能经 jsSink 回投,不能直接调 GTK/WebKit。
  3. 预置类能力必须在 start() 之前定稿 (窗口菜单、拖放策略这类),运行期改动走 setMenuItemState 之类的窄接口。

三步跑起来

准备环境

只差三样:仓颉 SDK(1.0.5+)、stdx、平台依赖(Windows 要 mingw gcc + WebView2 Runtime,Linux 要 libwebkit2gtk-4.1-dev + libgtk-3-dev)。

先跑一条自检命令,它会把缺什么、怎么补直接说出来:

bash 复制代码
npx cj-tauri doctor

生成工程

bash 复制代码
npx cj-tauri create myapp     # 零安装;默认零 Node 的内联 HTML 模板
cd myapp
npx cj-tauri dev              # 构建 C 桥 + cjpm build + 开窗口

create 还带两个前端模板,Vite 工程在 assets/ui/,dev 会接管它们的 dev server,改页面在窗口里热更新:

bash 复制代码
npx cj-tauri create myapp --template vue     # Vue 3
npx cj-tauri create myapp --template react   # React 18

加一条命令(三处联动,漏一处就用不了)

① 实现 handler:

cangjie 复制代码
public class ShoutCommand <: CommandHandler {
    public init() {}

    public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
        var text = "hello"
        if (let Some(v) <- args.get("text")) {
            match (v.kind()) {
                case JsonKind.JsString => text = v.asString().getValue()
                case _ => ()
            }
        }
        return JsonString("收到:${text}")
    }
}

② 注册进应用:

cangjie 复制代码
main(): Int64 {
    let app = TauriApp()
        .window(WindowConfig("我的应用", 1000, 700))
        .register("shout", ShoutCommand())
    app.run(pageHtml)
    return 0
}

③ 写进能力清单 capabilities/default.json:

json 复制代码
{
  "identifier": "default",
  "windows": ["main"],
  "commands": ["shout", "system:version"],
  "events": ["tick"]
}

前端调用:

js 复制代码
window.__CJ_TAURI__.invoke('shout', { text: 'hi' }).then(r => console.log(r));

只做 ①③ 会得到 command not registered,只做 ①② 会得到 command not allowed------这两条报错本身就是三处联动的验收标准。

权限不是开关,是四层

cj-tauri 的安全模型和 Tauri 2 是同一套思路:默认全拒,声明才放行。

  • 命令 / 事件白名单 :capabilities/*.json 里没写的命令,前端调了直接回 command not allowed,不会进业务代码。
  • 命名权限集 :插件可以声明 permissions(),把一组命令收成 fs:readonly 这样的短集名,清单里写一行就展开成成员。
  • 路径 scope :命令放行 ≠ 参数放行 。清单可以给插件写 scopes:
json 复制代码
{
  "scopes": {
    "fs": { "allow": ["$HOME/notes/**"], "deny": ["**/id_rsa"] }
  }
}

语义是先 deny 后 allow ,allow 必须显式命中,模式匹配前先规范化路径(存在则解析软链接)。越界被拒会打一行 scope denied,回给前端的是业务错误,而不是悄悄放行。

  • 出站 host 白名单 :http / ws 插件是页面可控的网络出口,host 白名单写在 scopes.http;什么都不声明等于全拒 。examples/ai-chat 就是靠这层把「页面能连哪个模型服务商」这件事钉死的。

值得强调的是:所有校验都在共享层(src/scope.cj、src/capability.cj),平台实现里不另判一份------这也是「换平台不改安全语义」的前提。

一份代码,三份产物

这是 cj-tauri 相对其他方案最实用的一点。新形态工程只有一个真相------cj-tauri.toml 清单:

toml 复制代码
[app]
  name = "music-next"
  version = "0.1.0"
  id = "com.example.music_next"   # 桌面是安装 ID,鸿蒙直接是 bundleName
  package = "music_next"

[[windows]]
  label = "main"
  title = "仓颉爱音乐 · cj-tauri"
  width = 1280
  height = 820

[assets]
  page = "ui/index.html"

[capabilities]
  dir = "assets/capabilities"

骨架由 cj-tauri sync 从清单里派生出来,不手写、也不手改:

bash 复制代码
npx cj-tauri sync desktop    # 生成 targets/desktop
npx cj-tauri sync ohos       # 生成 targets/ohos(DevEco 工程骨架)
npx cj-tauri check           # 校验清单并检测骨架漂移
bash 复制代码
music-next/
├── cj-tauri.toml          # 单一真相
├── src/                   # 业务 static 包(三端共用)
├── assets/ui/index.html   # 页面
├── assets/capabilities/   # 能力清单
└── targets/               # 生成物:desktop/ 与 ohos/,别手改

targets/ 是派生物,改它等于改缓存:下一次 sync 就覆盖回去了。业务代码、页面、能力清单三处才是你应该动的地方。

同一份 music-next 业务代码,在鸿蒙 PC 上出的产物:

打包分发

单文件包是这套框架的交付形态之一:静态链接仓颉运行时与 stdx,页面与能力清单编进二进制,WebView2Loader.dll 内嵌并在首次运行时释放,PE 子系统设成 GUI------双击就是窗口,不弹黑框,不要求机器上有 SDK。

bash 复制代码
bash scripts/winbuild.sh examples/typing-english   # 在 Linux 上交叉打出 Windows exe

实测产物:typing-english.exe 4.95 MB、导入表只剩 Windows 系统 DLL、无第三方运行库。仓库里已经随包带了十个这样的工具(AI 聊天、串口调试、Modbus、Markdown 笔记、判题器、音乐播放器......),可以直接下载来点。

几个真实踩过的坑

写这篇不是为了说「没有坑」。挑三条有代表性的:

① 三引号字符串会处理反斜杠转义。 把 HTML+JS 内联在 """ 里时,JS 的 \n 会先被仓颉吃掉变成真换行,把 JS 字符串拆断------整段 <script> 语法错误,页面里一行都不执行,而 stderr 毫无提示。内联 JS 里不要出现反斜杠。

② 子包调不了框架的 foreign func。 foreign func 没有函数体、也就没有跨包符号,单测里直接调 C 桥导出会在链接期炸(undefined symbol),编译期不报。办法是在框架包内包一层。

③ GUI 子系统 + 控制台子进程 = 桌面上多一个黑窗口。 应用产物是 GUI 子系统、没有控制台,用普通 launch 起一个控制台程序(CLI 工具),Windows 会给子进程单独配一个控制台窗口,而 stderr 一行错误都没有。解法是走框架的 launchHidden():CreateProcessW + DETACHED_PROCESS,并把三个标准句柄显式指到 NUL。

仓库里的 AGENTS.md 第 4 节维护着上百条这样的实测坑------每条都写着现象、判据和落法。这类内容比 API 文档更能省时间:它记录的是「为什么是这样」,而不只是「接口长什么样」。

从哪开始

想做的事 去哪
先看效果 仓库 examples/*/dist-single/*.exe,双击即用,不装 SDK
写自己的应用 npx cj-tauri doctor → create → dev
查命令与平台陷阱 docs/使用文档.md
看架构与决策理由 docs/设计决策记录.md、docs/安全模型.md
鸿蒙 PC 端 npx cj-tauri create myohosapp --template ohos,再 sync ohos

仓库同时托管在两个远端,一次 git push 双推。有问题直接提 issue,或者照着 AGENTS.md 的开发契约提 PR------那份契约本身就是这个项目「怎么长成这样」的记录。

相关推荐
Lonely丶墨轩13 分钟前
从一个 404 到负数 ID:第三方接口开发中的几次排错
java
聪明的笨猪猪18 分钟前
实际工程问题:AI生成MyBatis批量操作的SQL超限与性能雪崩
java·经验分享·面试·ai编程
文丑颜不良啊19 分钟前
LeetCode2501~2520题目解析
java·leetcode
mldong26 分钟前
同一个节点,同一个人,办两次:jeeflow 的已办撤回
后端·架构
瑞雪兆丰年兮34 分钟前
JavaWeb-day07-MySQL(DQL&多表设计)
java·开发语言·数据库·sql·mysql·javaweb·web
唐老板39 分钟前
Spring Boot 4 默认开虚拟线程,HikariCP 却成瓶颈
java
脚本自动化开发8 小时前
折叠双屏MU156LA:写稿修图游戏体验
后端
脚本自动化开发8 小时前
AI短剧800集讲规矩,观众忘了是AI
后端
qq_158895411119 小时前
AI 音乐生成完成后,音频 URL 为什么会过期?一套可恢复的交付方案
python·api·ai音乐