仓颉语言cj-tauri 框架开发鸿蒙应用指南:从建工程到鸿蒙PC实战

仓颉语言的cj-tauri 框架已经支持鸿蒙应用开发了。本文依据 npm 包 cj-tauri的说明与仓库里的真实落地整理,目标读者是「会写 HTML/CSS/JS、想用仓颉写后端」的鸿蒙开发者。文中每条行为都对应到具体文件或命令,可以照着做一遍;跑不通的地方也如实写了「它会怎么报错、真因在哪」。

设备环境(本文的实测基准):鸿蒙PC 华为 MateBook Pro(HAD-W32)、HarmonyOS7.0.0.107、API 26、aarch64、屏幕 3120×2080 px、密度 190%;工具链是 DevEco Studio 6.1 自带的仓颉插件。

cj-tauri是什么

cj-tauri · 仓颉版 Tauri。用华为仓颉语言实现的桌面应用混合开发框架 ------ 仓颉静态编译后端 + 系统 WebView 前端,产物不带 Chromium、几 MB 级。cj-tauri 是「仓颉语言的 Tauri 2」:一个仓颉原生的脚手架 CLI,把 Tauri 的三件套带到仓颉语言上------

三件套 在 cj-tauri 里是什么
WebView 宿主 Windows 用 WebView2、Linux 用 WebKitGTK、鸿蒙用 ArkWeb
IPC 双向桥 invoke / resolve / event,页面前端统一走 window.__CJ_TAURI__
能力安全模型 capability 白名单,命令与事件都必须在清单里声明才放行

也就是说:界面继续用 Web 技术写,后端换成静态编译的仓颉,中间那层桥与权限校验由框架提供。

它不是「把网页塞进黑框」那种壳。一条命令从页面到仓颉后端要过四关:桥脚本注入 → IPC 报文 →能力清单校验 → 命令分发(在 worker 线程上执行,不钉住宿主 UI 线程);回投同样走判据可查的通道。

换句话说,页面上按一个按钮,真的会跑到你写的那段仓颉代码里。

为什么要专门为鸿蒙写一篇?因为同一套框架现在有了第三个宿主 。桌面那两个(WebView2 / WebKitGTK)已经跑了很久,鸿蒙 PC(ArkWeb)这一路是后来拿真机一轮轮打通并留了证据的:页面里的 system:version能往返、回投报文里的 platform 是 ohos、多窗口 label 路由逐条恒等。对使用者的意义很直接。

cj-tauri npm仓地址 :www.npmjs.com/package/cj-...

bash 复制代码
npx cj-tauri create myohos --template ohos

一条命令生成一个标准 DevEco 工程 ,里面 ArkWeb 宿主、双向桥、能力清单、构建脚本都已就位,你只要动两个文件:rawfile/index.html(页面)和 cangjie/app_host.cj(命令层)。

一、开发优势

1. 一套业务代码,三个宿主

这是最能省事的一条,也是它区别于「某平台定制方案」的地方:换宿主,不换业务。

仓库里的音乐示例 examples/music(Windows/Linux 三栏播放器)搬上鸿蒙时,鸿蒙工程examples/ohos/ohos-music 里没有另抄一份业务代码 ------examples/music/src/*.cj 在构建时被复制进entry/src/main/cangjie/musiclib/(只把文件头的 package 改成应用模块的包名),页面同理内嵌编译。

所以「谁是源」很清楚:

bash 复制代码
改业务逻辑 → 改桌面工程 examples/music
改鸿蒙宿主 → 改鸿蒙工程的 main_ability / index.cj / 构建脚本

代价与收益都很清楚:你维护的是一份业务,平台差异只在宿主那一层。如果每次上鸿蒙都要维护两份代码,那这个「跨平台」就是假的------这条是作者给自己定的验收标准。

2. 后端是真原生,前端是真 Web

  • 后端是静态编译的仓颉:命令实现里可以开线程、读文件、发 http、操作 SQLite(插件)、调 FFI; 它运行在独立 worker 线程上(命令分发是异步的),慢命令不会冻住界面。
  • 前端是系统 WebView 里的真页面:HTML/CSS/JS 全都能用,做桌面级排版、动画、图表没有额外成本。
  • 桥是双向 的:页面 invoke 调后端;后端 emit 推事件给页面(页面 listen 订阅)。

对鸿蒙开发者还有一个现实好处:ArkWeb 就是系统浏览器内核,你在 Chrome 里调好的页面,搬到鸿蒙上基本不用重排(窗口尺寸口径见下文第 4 节,那一处要注意)。

3. 能力安全模型是「默认拒绝」

所有命令与事件都必须在能力清单里声明,没声明的调用会被后端直接拒掉,页面看到的是command not allowed 而不是「神秘地没反应」。鸿蒙工程里这份清单是 entry/src/main/resources/rawfile/capabilities.json:

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

设计约束是「声明 ≠ 放行,清单不引用就不生效」:插件可以声明它提供什么,但放不放行由清单说话。对一个要装进用户设备的应用,这比「全开」省心------至少你知道哪些面被暴露了。

4. 鸿蒙侧真的很薄:框架不碰 ArkUI

这是踩过坑之后定下的架构,也是它能在鸿蒙上稳定跑的前提:框架包绝不引用 ArkUI。

@Component / Web / javaScriptProxy 这类声明式 UI 的东西全部住在应用模块(entry/src/main/cangjie/index.cj),框架侧只导出不碰 ArkUI 的桥接函数。原因是硬性的:框架包一旦 import kit.ArkUI.*,合并进应用动态库后包初始化链会跳到坏地址,应用启动即 SIGSEGV (表现是白屏或秒退,而 hilog 里连一行 AbilityStage.onCreate 都没有)。

好处落在你这边:

  • 升级框架不需要动你的宿主代码;
  • 你的鸿蒙工程就是标准 DevEco 工程 (entry/ + AppScope/ + build-profile.json5 + hvigorfile.ts), IDE 里直接点 Build 也能编过;
  • 页面与能力清单是 hap 里的 rawfile 真文件,不是「构建期生成的字符串」,改完重编即可。

5. 一条命令覆盖三个平台

Windows / Linux / 鸿蒙都是同一条 cj-tauri 命令。子命令对鸿蒙会自动转交工程自带的构建脚本:

子命令 在鸿蒙工程上的行为
cj-tauri dev 转交 build-hap.sh(编 hap)
cj-tauri build 转交 build-hap.sh(编 hap)
cj-tauri run 转交 build-hap.sh --install(编 + 装机 + 拉起)
cj-tauri info 环境自检(排障先跑它)

你不需要记「鸿蒙用哪条命令、桌面用哪条」。

6. 边界如实,不假装成功

这一点容易被当成缺点,其实是长期可用的关键:平台没有的能力,框架就如实报没有。

  • 鸿蒙侧的宿主能力位(菜单 / 托盘 / 全局快捷键 / 拖放 / 原生对话框)目前如实返回 0,调用会被拒,当前还在陆续实现中,敬请期待;
  • 没有 C 桥,所以 launchHidden(后台静默子进程)/ 串口 / 托盘这类宿主能力在鸿蒙上暂时不提供;
  • 运行时装不了 TLS(设备上没有 OpenSSL 3),要访问 https 就走系统网络栈 ohos.net.http;

对做产品的人来说,这些「明确的没有」比「时好时坏」好排期得多。

二、环境准备

需要的东西不多,但缺哪一样都只会得到一句误导性的报错,所以先把清单摆出来:

# 需要什么 版本 / 说明 怎么确认它有
1 仓颉 SDK 1.2.0 ,含 stdx cjc --version
2 Node.js 能跑 npx 即可(跑 CLI 用) node -v
3 DevEco Studio 6.1,含仓颉插件(自带 hvigor / node / hdc) 目录存在即可
4 鸿蒙版 stdx 构建期要链它 目录存在即可
5 鸿蒙 PC 真机 开发者模式 + USB 调试 + 已签名 hdc list targets

2.1 仓颉 SDK(应用后端就是它)

这个 npm 包不替代仓颉 SDK :应用后端是仓颉代码,cj-tauri dev / build 要调用 SDK 里的cjc / cjpm 把你的应用编出来。装好后让 CLI 能找到它:

bash 复制代码
# Linux
export CANGJIE_HOME=/opt/cangjie/cangjie

# Windows cmd
set CANGJIE_HOME=D:\Program Files (x86)\Cangjie

不设也能跑------CLI 会探测常见安装位置(Windows D:\Program Files (x86)\Cangjie、Linux /opt/cangjie/cangjie)。装完先自检:

bash 复制代码
npx cj-tauri info        # 打印 框架 / 项目 / stdx / SDK / cjpm / C 桥 的解析结果
npx cj-tauri --version   # 这条不需要 SDK,用于确认 npx 拿到的是哪个版本

排障时第一条命令就该是 info :它把每个组件的解析路径都打出来,比逐条试错快得多。 没装 SDK 时 CLI 会给安装提示,而不是甩一句 cjpm: command not found。

2.2 拿到 CLI

bash 复制代码
npx cj-tauri --version      # 一次性使用,推荐
npm i -g cj-tauri           # 或者全局安装,之后直接 cj-tauri <子命令>

包内优先使用预编译二进制(prebuilt/<平台-架构>/),没有则把 CLI 源码拷到缓存目录用本机cjpm build 编一次(Linux/macOS ~/.cache/cj-tauri、Windows %LOCALAPPDATA%\cj-tauri),不会往全局安装目录里写产物。

一条已知限制要提前知道:首次 dev / build 会把 C 桥(桌面平台的)构建到框架目录的 native/ 下,所以那份安装目录需要可写。鸿蒙工程不走这条路(鸿蒙侧没有 C 桥),但如果你同时做桌面版,记着这条。

2.3 DevEco Studio 6.1 与仓颉插件

鸿蒙侧构建链全在 DevEco 里:hvigor(构建)、node、hdc(设备)、仓颉构建插件(编 .cj 进 .so)。工程自带的 build-hap.sh 会按顺序探测安装根(DEVECO_WIN 优先,其次D:\oh\DevEcoStudio、C:\Program Files\Huawei\DevEco Studio 等),探测不到就让你显式给:

bash 复制代码
export DEVECO_WIN='D:\Program Files\Huawei\DevEco Studio'

从命令行(headless)构建时有两个必须给的开关,否则报的错会指向「工程配置写错」:

bash 复制代码
export DEVECO_CANGJIE_PLUGIN_ENABLED=true
export NODE_PATH="$(cygpath -w "$(pwd)/node_modules")"

真因是:仓颉构建插件被判定为未启用 时,它会主动把 cangjie 的 schema 删掉 且不报错,于是终端只剩一行 00303038,而工程里明明写着 cangjieOptions 却被告知「property name must be valid」。(build-hap.sh 已经把这一组变量都设好了,你手工调 hvigorw 时才需要自己给。)

2.4 鸿蒙版 stdx

构建期要链鸿蒙版 stdx,指向它的动态库目录:

bash 复制代码
export CANGJIE_STDX_OHOS=<stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx

注意两个易错点:

  • 指的是 dynamic/stdx 这个目录,不是 stdx 的包根;
  • entry/cjpm.toml 里不要 写 ${CANGJIE_STDX_OHOS} 之类的变量:hvigor 只替换它自己的模板变量(${AARCH64_LIBS} 那一组),写别的会被替换成空串,而空串会让整条 path-option 失效------报错却是 imports package 'stdx.encoding.json' ... not added as a dependency,看着像漏配依赖。模板里写的是相对路径 ../stdx ,指向工程根上那个 stdx 联接,由 build-hap.sh 按 CANGJIE_STDX_OHOS 自动创建(Windows 用 mklink /J)。

2.5 真机与签名

  • 真机开开发者模式 + USB 调试 ,hdc list targets 能看到串号;
  • 签名 :模板不带证书 (调试证书与华为账号绑定,不该入库)。首次打开工程在 DevEco 里 File → Project Structure → Signing Configs,勾上「Automatically generate signature」。不签名也能编,产物是 *-unsigned.hap,但装不进设备 (hdc install 会报签名类错误);
  • aa start 需要屏幕已解锁:开发者模式下没法自动解锁,锁屏时表现为「启动失败」或者抓到一张锁屏截图;
  • 同一份调试签名下,两个工程共用一个 bundleName 会互相顶掉安装位 ------一台设备上只能留一个。换 bundleName 要在 DevEco 里重新自动签名一次,命令行做不了。这个期待官方开放侧载,就不用这么麻烦了,且可以拜托对 DevEco这个IDE的依赖,更方便AI。

2.6 三条「报错指向的地方不是真因」

这三条是这套工具链上最容易白走弯路的,先记下来能省一晚:

你看到的报错 真因 怎么办
Invalid value of 'DEVECO_SDK_HOME' 环境里预置了指向不存在安装的旧路径 build-hap.sh 会校验后回退;手工构建就自己 export 有效值
property name must be valid: cangjieOptions 仓颉构建插件没被加载(schema 被它自己删了) 给上 DEVECO_CANGJIE_PLUGIN_ENABLED=true + NODE_PATH
opt.exe: EnumReflection format: operand numbers error! cjc 与 opt 来自不同代 SDK(cjc 按 CANGJIE_HOME 找 opt) 用同一套 SDK 的 build-tools,别让桌面 SDK 在 PATH 上抢先

最后一条尤其值得强调:崩溃点落在 opt 不代表原因在 opt,它只是第一个发现 IR 不合法的关卡; 同时 hvigor 的常驻 daemon 用的是它自己启动时 的环境,所以「手跑 cjpm build 成功、build-hap.sh 一跑就崩」这种分裂现象,是 daemon 复用了旧环境------build-hap.sh 因此固定带 --no-daemon。

三、开发过程

3.1 生成工程

bash 复制代码
npx cj-tauri create myohos --template ohos
cd myohos

两个可选参数,一般只在前几次需要:

bash 复制代码
npx cj-tauri create myohos --template ohos \
    --bundle com.example.myapp \
    --label "我的应用"
  • --bundle:hap 的 bundleName。缺省由项目名推导成 com.example.<名字>(-、. 折成 _,数字开头补 app 前缀,因为鸿蒙要求每段以字母开头、只含字母数字下划线、至少三段);
  • --label:桌面显示的应用名,缺省等于项目名。

生成的是一个标准 DevEco 工程,鸿蒙相关的那层很薄:

bash 复制代码
myohos/
├─ AppScope/app.json5                应用级配置(bundleName)
├─ build-profile.json5               工程级构建配置(签名留空)
├─ build-hap.sh                      唯一构建入口(编 hap / 装机)
├─ entry/
│  ├─ cjpm.toml                      cjTauri 依赖 + aarch64-linux-ohos 目标段
│  ├─ build-profile.json5            cangjieOptions.checkDeviceTypes=false(放行 2in1)
│  └─ src/main/
│     ├─ module.json5                deviceTypes: 2in1 / tablet / phone
│     ├─ cangjie/
│     │  ├─ ability_stage.cj         AbilityStage
│     │  ├─ main_ability.cj          UIAbility:沙箱、rawfile 读取、窗口尺寸、loadContent
│     │  ├─ index.cj                 ArkWeb 宿主组件(javaScriptProxy 双向桥)
│     │  └─ app_host.cj              框架装配 + 示例命令(greet / timer)
│     └─ resources/rawfile/
│        ├─ index.html               页面(你主要改这里)
│        └─ capabilities.json        能力清单(你主要改这里)
├─ hvigorfile.ts / oh-package.json5   hvigor 工程描述
└─ README.md                         本工程的说明(含改哪里、坑在哪)

你日常只动两个文件 :rawfile/index.html 和 cangjie/app_host.cj。其余四段装配代码是「跑起来」的必需品,通常不用碰。

3.2 为什么鸿蒙上要四段装配

桌面上程序的入口是 main(),鸿蒙上没有「自己起个进程跑 main()」这回事:窗口属于 ArkUI,页面由 ArkWeb 组件承载 。所以模板把桌面的 main() 拆成三段,顺序不能错:

ts 复制代码
// ① 沙箱目录必须在 run() 之前给:run(html) 要把页面落盘再以 file:// 加载
//    (ArkWeb 的仓颉绑定没有 loadData,只能走文件)
let filesDir = "${this.context.filesDir}"
ArkWebRuntime.configure(filesDir)

// ② 页面与能力清单来自 hap 的 rawfile,读成字符串传进去
let pageHtml = this.readRawText("index.html")
let capsJson = this.readRawText("capabilities.json")

// ③ run() 会阻塞到 quit(),只能在非 UI 线程跑------UI 线程要留给 ArkUI 建树
let _ = spawn { startTauriApp("我的应用", pageHtml, capsJson) }

第四段落在 onWindowStageCreate:等框架把默认窗口的宿主登记好,再 loadContent:

ts 复制代码
this.logDefaultWindowRect(windowStage)    // 只读一行窗口矩形当凭证,不改尺寸(见 3.7)
this.waitAndLogHost(DEFAULT_WINDOW_LABEL) // 3 秒有界等待:宿主的页面地址要非空
windowStage.loadContent("EntryView")

这两步各自都有个「不这么写就白屏」的原因,值得知道:

  • 为什么等宿主 :组件的 build() 是一次性求值 。首帧如果宿主还没登记、页面地址是空串,就永远不建 Web 组件,onPageBegin 也不会来,页面纯白且没有任何报错------死锁。
  • 为什么在 aboutToAppear() 里同步地址 :宿主把页面落盘后才把 file:// 地址放进自己的字段,而 build() 已经求值完了。所以 index.cj 在aboutToAppear()(首次 build() 之前跑一次)里读一次宿主地址写进 @State,触发重绘。

明白这两点,你就知道以后加窗口/改宿主时该在哪一步塞代码。

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

加命令永远是三件事,缺哪个就得到对应的错:

  1. 实现一个 CommandHandler;
  2. 在装配处 .register("名字", XxxCommand());
  3. 在 rawfile/capabilities.json 的 commands 里声明 "名字"。
只做了 运行时报什么
1 + 3(忘记注册) command not registered
1 + 2(忘记清单) command not allowed

完整的最小例子(app_host.cj 里自带的 greet):

ts 复制代码
import cjTauri.*
import stdx.encoding.json.*

public class GreetCommand <: CommandHandler {
    public init() {}

    public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
        var name = "world"
        if (let Some(n) <- args.get("name")) {
            match (n.kind()) {
                case JsonKind.JsString => name = n.asString().getValue()
                case _ => ()
            }
        }
        return JsonString("Hello, ${name}! 来自仓颉后端(鸿蒙 ArkWeb 宿主)")
    }
}

装配:

ts 复制代码
let app = TauriApp()
    .window(WindowConfig(windowTitle, 1280, 820))
    .register("greet", GreetCommand())
    .register("timer", TimerCommand())

// 显式挂载清单即不自动扫描目录------hap 里本来也没有 capabilities/ 这个目录
if (capsJson.size > 0) {
    app.addCapabilityJson(JsonValue.fromStr(capsJson))
}

app.run(pageHtml)   // 阻塞到 quit()

注意 app.addCapabilityJson 这一句:鸿蒙工程里清单是构建期打进 hap 的 rawfile ,运行期没有capabilities/ 目录可扫,所以必须显式挂。忘了挂的后果是「一条命令都不放行(含 system:*)」,页面看到的全是 command not allowed------这比静默放行安全,也比「假装成功」好排查。

3.4 改页面:等桥出现,然后 invoke

页面就是普通 HTML/CSS/JS,唯一的框架约定是只经 window.__CJ_TAURI__:

js 复制代码
// 鸿蒙的 ArkWeb 没有 document-start 注入,桥是在 onPageBegin 里注入的,
// 所以页面必须「等桥出现」,不能在模块作用域直接读。
function withBridge(cb) {
  if (window.__CJ_TAURI__) { cb(window.__CJ_TAURI__); return; }
  setTimeout(function () { withBridge(cb); }, 50);
}

withBridge(function (tauri) {
  // 页面 → 仓颉
  tauri.invoke('greet', { name: 'world' })
    .then(function (data) { console.log('invoke OK:', data); })
    .catch(function (err) { console.error('invoke FAIL:', err.message); });

  // 仓颉 → 页面(事件订阅,事件名要在清单的 events 里声明)
  tauri.listen('tick', function (payload) {
    console.log('tick #' + payload.n);
  });
});

这段 withBridge 不是可选项。桌面平台的桥是 document-start 注入,鸿蒙侧拿不到那个时机(onPageBegin 已经是最早的可用点),所以任何「模块加载时立刻 invoke」的写法在鸿蒙上都会拿到undefined。模板的 index.html 已经这么写了,照抄即可。

3.5 能力清单:把「放行」写清楚

清单只有四个字段,但每个都有实际后果:

json 复制代码
{
  "identifier": "default",
  "windows": ["main"],
  "commands": ["greet", "timer", "system:version", "system:ping", "system:echo"],
  "events": ["tick"]
}
  • commands:命令白名单。除 system:version / system:ping / system:echo 这几个内置命令外,全部要自己写进去;
  • events:事件也要声明 。后端 emit 一个没声明的事件名,页面那边收不到;
  • windows:这份清单覆盖哪些窗口 label。单窗口就是 ["main"];
  • identifier:清单标识,多份清单时用于区分。

再往上一层还有个「命令放行 ≠ 参数放行 」的机制:桌面侧可以给插件的路径参数配 scope($HOME/notes/** 这类 allow/deny 规则),由共享层统一判定,插件里不另写一份逻辑。鸿蒙工程目前不用到插件的那几个路径类命令,但如果你以后加了,判定逻辑仍只有一份。

3.6 构建 → 装机 → 拉起

跑之前先把两个环境变量备好(2.3 / 2.4 的结论):

bash 复制代码
export CANGJIE_STDX_OHOS=<stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx
export DEVECO_WIN='D:\Program Files\Huawei\DevEco Studio'   # 探测不到时才需要

然后:

bash 复制代码
bash build-hap.sh              # 只编 hap,产物在 entry/build/default/outputs/default/*.hap
bash build-hap.sh --install    # 编完装机并拉起(= npx cj-tauri run)
bash build-hap.sh --clean      # 先清缓存再编(改了仓颉代码却没生效时用)

脚本做了几件容易被忽略、但少了就跳进坑里的事:

  • 探测并校验 DEVECO_SDK_HOME / NODE_HOME(环境里预置了不存在的路径时回退并说明忽略了谁);
  • 把 SDK 的 runtime/lib/windows_x86_64_cjnative 与 build-tools/bin 排进 PATH,并让 cjc 与 opt 来自同一套 SDK;
  • 建工程根的 stdx 联接(../stdx 那条相对路径靠它成立);
  • 用 --no-daemon 起 hvigor,避免 daemon 复用旧环境;
  • 装机的 hdc 用 DevEco 自带那份(sdk/default/openharmony/toolchains/hdc.exe),并且给路径前一律 MSYS_NO_PATHCONV=1------Git Bash 会把 Windows 路径改写成 /d/...,hdc 拿到就找不到文件。

装机的两条命令(脚本已封装,手工排障时用得上):

bash 复制代码
MSYS_NO_PATHCONV=1 hdc install -r <hap 的 Windows 路径>
hdc shell aa start -a EntryAbility -b com.example.myapp

⚠️ -a 后面是 module.json5 里的 ability 名(EntryAbility) ,不是仓颉类名 MainAbility------写成类名会得到 10104001。

另外:改代码没生效先怀疑产物,别改代码 。hvigor 的 CompileCangjie 与框架侧 cjpm 各有一份 增量缓存,只清一边时旧 .so 会被喂回来,对照实验当场失真。判据看产物(.so 尺寸 / llvm-nm 里的符号数),不看 build success 这一行。

3.7 窗口尺寸:默认就别动它

这是最反直觉的一条,也是踩过两版弯路之后定下的口径:鸿蒙 PC 上系统给的那扇窗就是合适的,不要自己 resize。

实测系统按桌面形态给的初始矩形是 [515 281 2090 1394],即 2090×1394 px ≈ 1100×734 vp ,约屏宽 2/3、居中留白合理。而你在 onWindowStageCreate 里紧接着 resize() 会发生什么?系统先按默认尺寸建窗、你的代码再改它------屏上看到的是「先弹一个尺寸、随即跳成另一个」,用户的原话是「刚启动时弹出来的窗口大小合适,为何后来又变了」。

所以模板只做一件事:读一行矩形打进 hilog 当凭证,不 resize、不 move。

sql 复制代码
main window keeps system default 2090x1394 px at density 190% on screen 3120x2080, rect=[515 281 2090 1394]

真要固定尺寸才自己调,两个数字上的坑先记着:

  • resize() 收的是像素,不是 vp 。写 resize(1440, 900) 在 3120×2080 的屏上只占 46% 宽,日志照打「resized」、也不抛异常,只有 hidumper 读到的矩形能揭穿它;
  • densityPixels 是 Float32 (.cj.d 里看着像 Float64),跟 Float64 直接相乘会报 invalid binary operator,甚至把同一段的 Hilog.info 报成「参数列表不匹配」------先化成整数百分比再算。顺带一提:本机密度是 190%,别按「2 倍屏」想当然。

还有一个前提:上面「系统给的就是桌面窗」成立的条件是 module.json5 的 deviceTypes 写 2in1,并且 entry/build-profile.json5 里放行了它。写成 phone / tablet 时,系统按手机应用塞进一扇约 600 宽的竖屏兼容窗------那种情况才需要自己定尺寸。

3.8 真机自证:hilog 里应该有这条链

「窗口起来了」不等于「通了」。跑通之后该有的是一条可对账的日志链 (CjTauri tag),缺哪行就知道断在哪:

less 复制代码
MainAbility OnCreated.EntryAbility
host registered after 0 polls label=main
host pageUrl label=main url="file:///data/storage/el2/base/haps/entry/files/cj-tauri-main.html"
page begin label=main
runJavaScript ok
main window keeps system default 2090x1394 px at density 190% on screen 3120x2080

取证手法有两条是硬要求:

  1. 必须流式抓 。hdc shell hilog > f 先起流 ,再 aa start 启动应用,跑完再 grep 文件。 一次性 hilog -x 会把启动瞬间那几行挤出环形缓冲(中间系统日志能刷三千多行),判据会很怪:ps 里进程活着,但 Cangjie tag 计数是 0;
  2. eprintln 在设备上完全不可见 。桌面平台的 stderr 是主要诊断通道,在鸿蒙上等于没有------ 诊断一律写 Hilog.info。

页面侧自证:hdc shell snapshot_display 抓一张图,看得到业务内容(不是白屏、不是错误提示),再点一下 system:version 看它返回版本号而不是 -。

3.9 把已有的桌面工程搬上鸿蒙

如果你的应用已经在桌面跑起来了(就像仓库里的 music / movie),迁移要改的只有「谁在跑页面」,业务代码可以一行不动。仓库里两个鸿蒙工程的做法值得照抄:

bash 复制代码
examples/music/src/*.cj(去掉 main.cj)
        └─ 复制到 entry/src/main/cangjie/<业务包>/
            └─ 只改第一行:package music_app → package <应用模块包名>.<业务包>

examples/music/ui/index.html + capabilities/default.json
        └─ base64 编码 → 编译进包的字符串常量(<业务包>/xxx_assets.cj)

于是「谁是源」很清楚:改业务逻辑去改桌面工程,改完重跑 build-hap.sh 。不把桌面工程当 path 依赖写进 entry/cjpm.toml,是因为桌面的业务模块通常是output-type = "executable"、自带 main(),把它当依赖编进动态模块属于没跑通过的形态。

顺带一个便宜的好处:页面走 base64 内嵌 ,绕开了仓颉三引号字符串会把 JS 里的反斜杠转义先吃一遍的坑(内联 JS 里写 \n 会变成真换行,把字符串或单行注释拆断,整段 <script> 语法错误、页面一行都不执行,而 stderr 毫无提示)。

搬完之后命令契约保持不变 是成败判据。观影示例的封面在桌面版走「宿主带 Referer 取图」的命令movie:image,鸿蒙侧要换成系统网络栈(见第 4 节),但命令名、入参、返回值、白名单一行没改------前端代码因此零改动。如果为了上某个平台改了命令契约,那「同一套业务」就不成立了。

四、常见坑速查

按「症状 → 真因 → 处理」排,都是真机上遇到过的:

症状 真因 处理
窗口是竖屏窄窗(约 600 宽),三栏挤成一列 deviceTypes 只写了 phone / tablet 写 2in1;若被判 01103012,在 entry/build-profile.json5 里设 cangjieOptions.checkDeviceTypes = false(插件默认只放行 phone/tablet)
所有请求都失败,页面说「连不上后台」 没声明 ohos.permission.INTERNET,实际报 SocketException: Failed to create socket: Operation not permitted. 在 module.json5 的 requestPermissions 里加上;normal 级,装了即生效
应用启动即退 / 白屏,hilog 里连 onCreate 都没有 模块里有解析不了的符号(例如无 C 桥平台的 FFI) `llvm-nm -D --undefined-only <libentry.so>
页面纯白,但 BindWebTagToWebInstance 有、page begin 没有 组件 build() 一次性求值,页面地址为空就不建 Web(死锁) 在 aboutToAppear() 里同步宿主地址进 @State;@State 不能带 private
组件传参报 missing arguments for parameter list 成员写成普通 var,或调用点挂了链式修饰 成员用 @Prop,调用点保持纯调用,尺寸交给外层容器
invoke 的 promise 既不 resolve 也不 reject 回投通道没走通:javaScriptProxy 的返回值传不回页面 (实测恒 null) 回投由原生推(页面心跳 → 宿主在 proxy 回调里 runJavaScript),模板已做好,业务不用管
页面里 window.__CJ_TAURI__ 是 undefined 没等桥出现(鸿蒙没有 document-start 注入) 用 withBridge 轮询等待,别在模块作用域直接读
命令明明注册了却报 command not registered / command not allowed 三处联动漏了一处 注册 / register() / 清单 commands 三者齐
只有 https 资源全灭,http 接口一切正常 鸿蒙侧仓颉运行时装不了 TLS(设备上没有 OpenSSL 3,MUSL-LDSO: load libcrypto_openssl.z.so failed) 换系统网络栈 ohos.net.http(createHttp() + HttpRequest.request),命令契约不变
改了仓颉代码,装上去还是旧行为 hvigor 与框架 cjpm 各有一份增量缓存 bash build-hap.sh --clean;判据看 .so 尺寸,不看 build success
aa start 起不来 屏幕锁着,或 -a 写了仓颉类名 MainAbility 解锁后重试;-a 用 EntryAbility
装第二个应用把第一个顶掉了 两个工程共用同一份调试签名与 bundleName 一台设备只留一个;换 bundleName 要在 DevEco 里重新自动签名

五、总结

把这件事拆开看,cj-tauri 在鸿蒙上提供的是一个很薄的宿主适配层 + 一套平台无关的应用骨架:

  • 你写的东西 :一份 HTML/CSS/JS 页面、若干仓颉 CommandHandler、一份能力清单;
  • 框架提供的东西:ArkWeb 宿主装配、双向桥、能力校验、命令分发(异步 worker)、事件回投;
  • 平台替你兜的东西:窗口、TLS、证书链、权限------用系统自己的实现,框架不越界。

从零到装进设备,实际只要四步:

bash 复制代码
npx cj-tauri create myohos --template ohos    # 1. 生成标准 DevEco 工程
# 2. 改 rawfile/index.html(页面)+ cangjie/app_host.cj(命令)+ capabilities.json(放行)
bash build-hap.sh --install                   # 3. 编 hap + 装机 + 拉起(= npx cj-tauri run)
hdc shell hilog > log.txt                     # 4. 流式取证:看那五、六行链是否齐全

什么时候适合用它

  • 界面重、逻辑也重:图表、编辑器、复杂排版用 Web 做得快,重活交给仓颉后端;
  • 已经在桌面(Windows / Linux)跑着一个 cj-tauri 应用 :想上鸿蒙时业务代码基本不动,只需要加一个宿主工程(examples/ohos/* 就是这条路);
  • 要一份能讲清楚安全边界的应用:能力清单是显式的,哪些命令、哪些事件被暴露一目了然;
  • 团队是「前端 + 后端」结构 :前端照常写页面,后端写仓颉命令,接口就是 invoke 契约。

什么时候先别用它

  • 需要完全原生的鸿蒙控件与系统集成(菜单栏、托盘、全局快捷键、原生对话框):鸿蒙侧这些能力位目前如实返回 0,目前还在完善中;
  • 需要多窗口:桌面色已有 label 路由与取证,鸿蒙工程模板目前是单窗口(多窗口要按 label 各挂一个宿主);
  • 需要在鸿蒙上跑 C 桥那类宿主能力(静默子进程、串口):鸿蒙侧没有 C 桥产物,暂不提供。

三条经验,比 API 更值钱

  1. 失败可能发生在「诊断能力本身失效」的地方 。鸿蒙上 eprintln 不进 hilog、hilog 缓冲会滚掉启动瞬间------这种时候先取一条不问运行期的证据(符号表 llvm-nm、产物尺寸、窗口矩形),比任何日志都快。
  2. 「调用返回成功」不等于「现象成立」 。resize() 会照打日志也不抛异常,但单位是 px 不是 vp; SetMenu 那类宿主 API 更会静默返回 FALSE。凡是宿主调用,都按「调用前读数 + 调用后复核 + 把返回值打进同一行」收口。
  3. 别为了让检查变绿去改事实。取不到的图源就让它失败(页面回退成色块),不支持的能力就如实拒绝------把边界藏起来,下次它会在用户设备上以更贵的样子出现。

参考

  • npm 包:www.npmjs.com/package/cj-...
  • 仓库与完整文档:atomgit.com/qq8864/cj-t...
  • 鸿蒙示例:examples/ohos/ohos-music(音乐)、examples/ohos/ohos-movie(观影)、examples/ohos-probe(宿主探针)
  • 鸿蒙接入的完整踩坑过程:仓库 docs/鸿蒙ArkWeb接入踩坑.md;
  • 四篇实战记在 docs/鸿蒙PC实战-01 ~ 04
相关推荐
贾伟康43 分钟前
【化学实验室|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
软件测试·移动开发·harmonyos·arkts·arkui
熊猫钓鱼>_>1 小时前
精准碰一碰近场分享与拖拽投送实战:把文件“碰“进另一台设备【鸿蒙心迹】
开发语言·华为·harmonyos·鸿蒙·openharmony
三掌柜6663 小时前
06|鸿蒙7·端侧AI实战:提示注入,守住指令边界
人工智能·华为·harmonyos
HarmonyOS_SDK5 小时前
【HarmonyOS SDK Skills】快速适配鸿蒙窗口智感旋转能力
harmonyos
马剑威(威哥爱编程)6 小时前
【HarmonyOS 7 小艺智能体实战】02:动工前的清单——账号、资质、工具链一次备齐
人工智能·华为·harmonyos
熊猫钓鱼>_>6 小时前
端侧高斯泼溅(3DGS)渲染与性能调优初探:把“实景重建“跑在手机上【鸿蒙心迹】
开发语言·3d·华为·智能手机·harmonyos
2501_919749037 小时前
分享一款实用的鸿蒙App,整体感觉不错
华为·harmonyos
三掌柜6667 小时前
04|鸿蒙7·端侧AI实战:检索增强,把证据带进回答
人工智能·华为·harmonyos