它是什么

一句话: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 走完的链路

前端页面 (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)。

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

npx cj-tauri doctor

生成工程

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

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

npx cj-tauri create myapp --template vue     # Vue 3
npx cj-tauri create myapp --template react   # React 18

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

① 实现 handler:

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}")
    }
}

② 注册进应用:

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

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

{
  "identifier": "default",
  "windows": ["main"],
  "commands": ["shout", "system:version"],
  "events": ["tick"]
}

前端调用:

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:
{
  "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 清单:

[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 从清单里派生出来,不手写、也不手改:

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

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

Windows 上的音乐播放器(cj-tauri 示例)

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

鸿蒙 PC 上的同一份示例

打包分发

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

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

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

AI 聊天客户端:多服务商 + 内置模型网页版

SQLite 浏览器:数据库工具也是同一个框架写的

几个真实踩过的坑

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

① 三引号字符串会处理反斜杠转义。 把 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 |

  • 仓库:<https://atomgit.com/qq8864/cj-tauri>(AtomGit,国内直连) / <https://github.com/yangyongzhen/cj-tauri>(GitHub 镜像)
  • 官网(含 24 张实机截图):<https://yangyongzhen.github.io/cj-tauri/>
  • npm:<https://www.npmjs.com/package/cj-tauri>
  • 许可证:MIT

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐