少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比

以 @capacitor/keyboard(OpenHarmony 适配)与 cj-tauri(ArkWeb 宿主)的真实源码为例,拆解「JS 调原生」到底要跨几层、几种语言、几次 ABI 边界,并给出复杂度量化与选型建议。


Capacitor / Ioniccj-tauri
正向环节7 个环节(含系统端点)4 个环节(+1 回程)
涉及语言JS / C++ / ArkTS(3 种)JS / 仓颉(2 种)
每次调用跨 ABI2 次(C++→ArkTS、ArkTS→C++)1 次(javaScriptProxy)
方法路由每方法三处对齐 + 运行时字符串路由统一 invoke(cmd, args),查表
事件模型事件 = 永不结束的 PluginCall(keepAlive)独立 listeners 表,与 Promise 解耦
安全模型无白名单capability 白名单 + path scope
回程主动 evaluateJavaScript 注入 fromNative鸿蒙侧 50ms 心跳 pull + runJavaScript
桥接核心代码量≈ 4 600 行≈ 1 100 行(+ 142 行内联 JS 桥)

结论:单看「桥」这一层,cj-tauri 的模型明显更简单——少一种语言、少一次 ABI 边界、命令模型统一。但 Capacitor 的复杂度有相当一部分是为兼容现有生态付的税:JS 侧 @capacitor/keyboard 一行不改,鸿蒙靠「伪装成 Android」复用整套 Capacitor JS 核心。两者的复杂度来源不同,不能只用「谁代码少」下结论。


阅读约定

文中路径均相对于各自仓库根目录,文件:行号 可直接定位:

  • 插件仓库:CPF-Ionic/capacitor-keyboard(分支 main,提交 19ef1e2)
  • 运行时仓库:CPF-Ionic/openHarmony-capacitor(libcapacitor.so + ArkTS 运行时)
  • 对比仓库:qq8864/cj-tauri(提交 d129236)

术语:「环节」= 图中一个编号节点(可能只是进程内的一次方法调用);「跳」= 一次跨语言 / ABI 边界的控制权转移。Capacitor 每次调用要跨 2 跳(C++→ArkTS、ArkTS→C++),cj-tauri 只跨 1 跳(javaScriptProxy)。

cj-tauri项目地址: cj-tauri 仓库


一、为什么需要桥

混合开发里,前端跑在系统 WebView 中,原生能力在 WebView 之外。桥至少要解决四件事:

  1. 调用:JS 怎么把「插件名 + 方法名 + 参数」送到原生;
  2. 路由:原生怎么找到对应的处理函数;
  3. 回执:结果怎么回到发起那一次调用的 Promise(而不是别的调用);
  4. 事件:原生主动推送(键盘弹出、电量变化)怎么持续送到 JS,且不干扰上面的 Promise。

两个项目对这四个问题的回答方式截然不同:

  • Capacitor 选择**「每个插件方法一个入口」**:插件方法在 C++ 里靠宏注册成方法表,ArkTS 侧再写一个同名函数,两边用一条路由字符串连起来;
  • cj-tauri 选择**「一个通用 RPC 入口」**:所有调用都是 invoke(cmd, args),cmd 查表,args 是任意 JSON 对象,事件走另一条 emit/listen 通道。

下面分别拆解。


二、Capacitor 键盘插件:七环节拆解

图 1 · Capacitor 键盘插件一次 show() 的七环节链路

2.1 打包期:plugin.xml 不是装饰

插件本身只有 400 行左右,真正的「接线」由 plugin.xml 声明,hionic sync openharmony 按它改写宿主工程:

配置项作用
config-json往 rawfile/capacitor.plugins.json 追加 {pkg, classpath:"Keyboard"} —— 运行时查表的唯一依据
CMakeLists往 capacitor 模块注入 add_subdirectory(Keyboard),父工程再 --whole-archive 链接
source-file把 .h / .cpp / CMakeLists.txt / .ets 拷到 capacitor 模块固定目录
runtimeOnly="true"ArkTS 文件只参与编译不参与导出,并写进 build-profile.json5 的 runtimeOnly.sources

两个容易被忽略的点:

  • ArkTS 源码被拷进 src/main/ets/components/Keyboard/,与运行时同属一个 HAR,所以 Keyboard.ets 能用相对路径 import ... from "../PluginGlobal"、import capacitor from 'libcapacitor.so';
  • --whole-archive 是必须的。插件靠静态构造函数自注册,不加它,链接器会把「没人引用」的 Keyboard.o 整个裁掉,注册就丢了。

2.2 启动期:两级注册表

第一级是 C++ 静态自注册,一行宏:

// capacitor-keyboard/src/main/cpp/Keyboard/Keyboard.cpp:19
REGISTER_CAP_PLUGIN(Keyboard, Keyboard)

宏展开成全局 KeyboardHandler 变量,构造函数在 CCapacitorPluginFactory 里登记「类名 → 构造函数」。纯静态初始化,没有运行时反射。

第二级是运行时实例化:PluginManager::registerAllPlugins() 读 capacitor.plugins.json,按 classpath 调工厂造对象,包成 PluginHandle,以 pluginId(这里是 "Keyboard")为 key 存表。JS 侧 @capacitor/keyboard 的 capacitor.id 必须与此字符串完全一致。

第三级是方法表:

// capacitor-keyboard/src/main/cpp/Keyboard/Keyboard.cpp:72
REGISTER_PLUGIN_METHOD(Keyboard, show, PluginMethod::RETURN_PROMISE)
void Keyboard::show(PluginCall& call)
{
    m_call = call;
    executeArkTs("./Keyboard/Keyboard/ShowKeyboard", 1, "", "Keyboard", call);
}

方法名、成员函数指针、返回类型(promise / callback / none)被塞进 classFunctions()[类名][方法名]。注意这里已经暴露了两个设计特征:

  • 路由是硬编码字符串 "./Keyboard/Keyboard/ShowKeyboard",写错只能运行时发现;
  • show / hide 共用同一个 ArkTS 函数,只靠 pageValue 的 1 / 0 区分语义。

2.3 JS → C++:伪装成 Android

鸿蒙 ArkWeb 没有 Capacitor 原生使用的 addWebMessageListener,运行时的做法是注册一个名为 androidBridge 的 JS 代理对象:

// openHarmony-capacitor/src/main/cpp/getcapacitor/MessageHandler.cpp:38
ArkWeb_ProxyMethod function1 = {"postMessage", MessageHandler::postMessage, static_cast<void *>(this)};
ArkWeb_ProxyMethod methodList[1] = {function1};
ArkWeb_ProxyObject proxyObject = {"androidBridge", methodList, 1};
Application::g_controller->registerJavaScriptProxy(m_strWebTag.c_str(), &proxyObject);

配合 native-bridge.js 的平台探测:

// openHarmony-capacitor/src/main/resources/rawfile/native-bridge.js:156
const getPlatformId = (win) => {
    if (win?.androidBridge) return 'android';
    else if (win?.webkit?.messageHandlers?.bridge) return 'ios';
    ...
};

于是整个 Capacitor JS 核心走 Android 分支:

// native-bridge.js:893(节选)
cap.toNative = (pluginName, methodName, options, storedCallback) => {
    let callbackId = '-1';
    if (storedCallback && ...) {
        callbackId = String(++callbackIdCount);
        callbacks.set(callbackId, storedCallback);   // Promise 的 resolve/reject 存在 Map 里
    }
    const callData = { callbackId, pluginId: pluginName, methodName, options: options || {} };
    postToNative(callData);
    return callbackId;
};

这一步是 Capacitor 方案里最「巧」也最「脆」的地方:巧在 JS 生态零改动;脆在平台身份是「借」来的——README 里 show() 标注「仅 Android 支持」,本质就是 JS 层认定自己是 android。

2.4 C++ → ArkTS:NAPI 只传一个扁平对象

C++ 插件不能直接调 ArkTS,只能走 executeArkTs:

// openHarmony-capacitor/src/main/cpp/getcapacitor/Plugin.h:141
virtual bool executeArkTs(const std::string &strAction, const int nValue, const char *pArgs,
                          const char *pObject, PluginCall &call)
{
    void *pFunctionRefresh = Application::getFunctionRefresh(call.getWebTag());
    ...
    napi_property_descriptor desc[] = {
        {"pageIndex", ...}, {"pageValue", ...}, {"pageArgs", ...},
        {"pageObject", ...}, {"pageWebTag", ...}};
    ...
    napi_call_function((napi_env)Application::g_env, nullptr, function, 1, argv, NULL);
}

它做四件事:

  1. 按 webTag 取出该 WebView 预先注册的 JS 函数引用(来自 InitCordova 的第 3 个参数 PluginHandler);
  2. 用 napi_create_object_with_properties 拼一个五字段扁平对象;
  3. napi_call_function 同步调用;
  4. 目标函数执行完后,通过 onArkTsResult 回传结果。

五个字段的含义:

字段含义
pageIndex路由串,如 "./Keyboard/Keyboard/ShowKeyboard"
pageValue一个整数槽位(0/1 表示隐藏/显示)
pageArgs一个字符串槽位(通常是 JSON 串)
pageObject插件名("Keyboard")
pageWebTag发起调用的 WebView 标识

只有两个通用参数槽,这是 show / hide 不得不共用一个函数的原因,也是这套桥扩展性受限的根源。

2.5 ArkTS:动态 import() 路由

PluginHandler 的路由规则非常朴素——最后一个 / 前是模块路径,后面是函数名:

// openHarmony-capacitor/src/main/ets/components/PluginHandler.ets:43
let pos: number = nativeAttribute.pageIndex.lastIndexOf("/");
let module: string = nativeAttribute.pageIndex.substring(0, pos);
let functionName = nativeAttribute.pageIndex.substring(pos + 1);
import(module).then((ns: ESObject) => {
  ns[functionName](nativeAttribute);
});

到插件自己的 ArkTS 代码里,就是普通鸿蒙开发:

// capacitor-keyboard/src/main/ets/components/Keyboard/Keyboard.ets:100
export function ShowKeyboard(pageIndex: NativeAttribute) {
  if (pageIndex.pageValue == 1) {
    let textConfig: inputMethod.TextConfig = { inputAttribute: inputAttribute };
    inputMethod.getController().attach(true, textConfig, (err: BusinessError) => {
      if (err) {
        capacitor.onArkTsResult(JSON.stringify({ result: "failed", errorMsg: err.message }), ...);
        return;
      }
      capacitor.onArkTsResult(JSON.stringify({ result: "" }), ...);
    });
  }
  if (pageIndex.pageValue == 0) {
    inputMethod.getController().hideTextInput().catch(...);
    capacitor.onArkTsResult(JSON.stringify({ result: "" }), ...);
  }
}

事件类能力则挂在窗口事件上,并且有版本门控:

// Keyboard.ets:164
async function listenerKeyboardWillShow(pageIndex: NativeAttribute) {
  const context = getContext() as common.UIAbilityContext;
  const windowClass = await window.getLastWindow(context);
  if (deviceInfo.distributionOSApiVersion >= 60000) {   // API 20
    windowClass.on('keyboardWillShow', willShowCallback(pageIndex));
  }
}

DidShow / DidHide 的门槛是 >= 50100(API 18),与 README 的能力矩阵一致。

2.6 回程:两种语义共用一条通道

ArkTS 侧统一出口是:

capacitor.onArkTsResult(jsonString, "Keyboard", webTag)

它是一个 NAPI 导出函数,C++ 侧按 webTag 找到桥,调用插件注册的 onArKTsResult。插件的处理就是一段分发:

// capacitor-keyboard/src/main/cpp/Keyboard/Keyboard.cpp:93
void Keyboard::onArKTsResult(PluginCall& call)
{
    if (call.hasOption("notify")) {
        std::string eventName = call.getString("notify");
        if (call.hasOption("content")) {
            notifyListeners(eventName, call.getObject("content"));
        } else {
            cJSON* pJson = cJSON_CreateObject();
            notifyListeners(eventName, pJson);
            cJSON_Delete(pJson);
        }
    } else if (call.hasOption("result")) {
        std::string result = call.getString("result");
        if (result == "") m_call.resolve();
        else if (result == "failed") m_call.reject(call.getString("errorMsg"));
        else m_call.resolve(call.getObject("content"));
    }
}

两种报文语义:

报文处理用途
{"notify":"keyboardDidShow","content":{...}}遍历 m_mapEventListeners[event],逐个 resolve事件推送
{"result":""} / {"result":"failed","errorMsg":...}m_call.resolve() / m_call.reject()Promise 结算

最终由 MessageHandler::sendResponseMessage 拼成 window.Capacitor.fromNative({callbackId, success, data}),经队列交给 ArkWeb 执行,JS 侧用 callbackId 从 callbacks Map 里找回最初那个 Promise。

2.7 一次 show() 的完整时序

鸿蒙系统 ArkTS 插件 C++ 运行时 androidBridge Web (JS) 鸿蒙系统 ArkTS 插件 C++ 运行时 androidBridge Web (JS) postMessage({callbackId:"12345", pluginId:"Keyboard", methodName:"show"}) MessageHandler::postMessage(raw) cJSON 解析 + 方法表查找 executeArkTs(pageIndex, pageValue=1, ...) [NAPI] inputMethod.getController().attach(true, cfg, cb) callback onArkTsResult({"result":""}) [NAPI] Keyboard::onArKTsResult ->> m_call.resolve() window.Capacitor.fromNative({callbackId:"12345", success:true}) callbacks.get("12345").resolve()

三、cj-tauri:四环节拆解

图 2 · cj-tauri 一次 invoke() 的四环节链路

3.1 三件套架构

cj-tauri 自称「仓颉语言的 Tauri 2」,三件套对应关系很清楚:

组件对应 Tauri本仓位置
WebView 宿主tao / wrysrc/host_arkweb.cj(鸿蒙)、host_webkit.cj、host_webview2.cj
IPC 双向桥invoke / eventsrc/ipc_hub.cj、src/ipc_message.cj、src/arkweb_script.cj
能力安全模型capabilitiessrc/capability.cj、src/scope.cj

它没有复用任何 Capacitor 代码,前端桥是自己写的一份 142 行内联 JS。

3.2 前端桥:只有三个入口

// cj-tauri/src/arkweb_script.cj(ARKWEB_BRIDGE_JS 常量,节选)
invoke: function (cmd, args) {
  var id = ++seq;
  var p = new Promise(function (resolve, reject) { pending[id] = { resolve: resolve, reject: reject }; });
  post({ type: 'invoke', id: id, cmd: cmd, args: args || {}, window: labelOf() });
  return p;
},
listen: function (event, cb) { ... },
emit: function (event, payload) { ... },
_dispatch: function (msg) {
  if (msg.type === 'resolve') { /* 按 id 结算 */ }
  else if (msg.type === 'event') { /* 按 label 过滤 + 回调 */ }
}

对比 Capacitor 的 toNative,差别一目了然:没有 pluginId、没有 methodName 的二元组,只有 cmd 一个字符串和一个任意 JSON 的 args。

3.3 报文协议

协议定义写得很直白(src/ipc_message.cj 顶部注释):

JS → 仓颉 (invoke):
  {"type":"invoke","id":1,"cmd":"greet","args":{...},"window":"main"}
JS → 仓颉 (emit):
  {"type":"emit","id":2,"event":"tick","payload":{...},"window":"main"}
仓颉 → JS (resolve/reject):
  {"type":"resolve","id":1,"ok":true,"data":...}
  {"type":"resolve","id":1,"ok":false,"error":"..."}
仓颉 → JS (event):
  {"type":"event","event":"tick","payload":{...},"window":"main"}

四个字段就覆盖了调用、事件、回执、多窗口四件事。这是它比 Capacitor 简单的根本原因:Capacitor 用「插件方法表 + 每个方法一个 ArkTS 函数」表达能力,cj-tauri 用「一维命令名 + JSON 参数」表达能力。

3.4 能力校验与线程模型

// cj-tauri/src/ipc_hub.cj:244(节选,注释为原文)
private func handleInvoke(req: InvokeRequest): Unit {
    if (!this.capabilities.canInvoke(req.cmd)) {
        this.reject(req.id, "command not allowed: ${req.cmd}", req.window);
        return;
    }
    if (!this.commands.contains(req.cmd)) {
        this.reject(req.id, "command not registered: ${req.cmd}", req.window);
        return;
    }
    match (this.serialCmdToGroup.get(req.cmd)) {
        case Some(group) => this.enqueueSerial(group, req)
        case None => let _ = spawn { => this.runCommand(req) }
    }
}

三个设计点值得对照 Capacitor:

  1. 校验在调用线程同步完成,拒绝即时且顺序确定;
  2. 执行挪到 worker 线程,慢命令不会钉住宿主 UI;
  3. 默认最小权限:未在能力清单声明的命令一律拒绝。

Capacitor 侧没有等价物:只要 JS 能发消息,就能调任何已注册插件方法。这是安全性与易用性的直接取舍。

3.5 鸿蒙侧的特殊补偿:pull

javaScriptProxy 的返回值传不回页面(真机实测,返回值恒为 null),所以「页面拉、原生答」的同步取数模型不成立,只能反过来推:

// arkweb_script.cj(节选)
function pump() {
  if (pumping) return;
  pumping = true;
  try {
    var host = window.__CJ_TAURI_HOST__;
    if (host && typeof host.post === 'function') {
      host.post('{"type":"pull","window":' + JSON.stringify(labelOf()) + '}');
    }
  } finally { pumping = false; }
}
try { setInterval(pump, 50); } catch (e3) { }

对应宿主侧:

// src/arkweb_proxy.cj
@When[env == "ohos"]
public func arkWebProxyPost(label: String, raw: String): String {
    match (ArkWebRuntime.hostFor(label)) {
        case Some(h) => h.acceptPageMessage(raw)
        case None => ()
    }
    return ""
}

acceptPageMessage 会先截掉两类控制报文(reload、pull),再把真正的 IPC 报文交给 IpcHub;pull 的响应由应用侧 Web 组件的 post 回调取 takePendingScripts(),用 runJavaScript 推回页面。

这是 cj-tauri 链路里唯一比 Capacitor「更绕」的地方,而且是平台限制导致的补偿,不是设计选择。

3.6 一次 invoke() 的完整时序

CommandHandler IpcHub (仓颉) javaScriptProxy Web (JS) CommandHandler IpcHub (仓颉) javaScriptProxy Web (JS) post({type:"invoke", id:1, cmd:"greet", args:{}}) arkWebProxyPost(label, raw) canInvoke + commands.get + spawn handle("greet", args, ctx) [worker 线程] JsonValue jsSink 入队 50ms 心跳 {type:"pull"} runJavaScript({type:"resolve", id:1, ok:true, data:...}) _dispatch ->> pending[1].resolve()

四、并排对比

图 3 · 两种桥接机制的并排对比

4.1 逐维度对比

维度Capacitor(本仓库)cj-tauri谁更简单
分层JS → C++ 运行时 → ArkTS 模块 → 系统 API,3 种语言JS → 仓颉宿主 → 命令处理器 → 系统 API,2 种语言cj-tauri
JS↔原生通道伪装 Android:androidBridge.postMessage自有 javaScriptProxy.post(json)平手,都是代理对象
方法路由三处对齐:C++ 宏 + ArkTS 函数 + 硬编码路由串,靠 import() 加载一个入口 invoke(cmd, args),命令名即 map keycj-tauri
参数模型NAPI 只传 5 个扁平字段,show/hide 只能共用函数用 0/1 区分args: JsonObject,任意结构cj-tauri
Promise 配对callbackId + PluginCall;事件依赖 keepAliveid + pending Map;事件独立 listeners 表cj-tauri
回程通道C++ 主动 evaluateJavaScript,无轮询鸿蒙 50ms pull + runJavaScriptCapacitor
事件模型事件 = 永不结束的 call,插件自维护监听表hub 统一 emit/emitLatest + label 过滤cj-tauri
安全模型无白名单能力白名单 + path scope + 串行组cj-tauri(有意的复杂度)
新增一个方法改 C++ 宏、写 ArkTS 函数、写路由串实现接口 + register + 清单声明cj-tauri

4.2 复杂度量化

指标Capacitorcj-tauri
正向环节7 个环节(含系统端点)4 个环节
语言数32
ABI 边界/次调用21
桥接核心代码≈ 4 600 行≈ 1 100 行
前端桥 JS1 035 行(官方 native-bridge.js,非鸿蒙专有)142 行(内联)
新增能力接线点3 处(宏、ArkTS、路由串)3 处(接口、注册、清单)

代码量口径:Capacitor 统计 Plugin.*、PluginCall.*、Bridge.cpp、MessageHandler.cpp、PluginManager.cpp、PluginHandle.cpp、napi_init.cpp、PluginHandler.ets、PluginCall.ets、CapacitorPlugin.ets、native-bridge.js;cj-tauri 统计 ipc_hub.cj、ipc_message.cj、capability.cj、scope.cj、arkweb_proxy.cj、arkweb_script.cj。


五、简单性之外:各自付出的代价

5.1 Capacitor 侧的「薄适配层」问题

读源码过程中发现几处可以复现的缺陷,它们都源于「尽量少改上游语义」的适配策略:

  1. m_call 单实例竞态:Keyboard.h:22 只有一个 PluginCall m_call,show/hide/setScroll 每次调用都会覆盖它。连续 show() 后紧跟 hide() 时,先发起的 Promise 永远不结算。
  2. removeListener 取错参数:Keyboard.cpp:46-48 用 cJSON_GetArrayItem(pJson, 0)->valuestring 取事件名,但 JS 传的 options 对象第一个属性是 callbackId(native-bridge.js:195 字段顺序为 callbackId, eventName),于是把 callbackId 当成事件名,Keyboard.ets 的 keyValue 永远匹配不上 → window.off 实际不执行,原生监听泄漏,且 C++ 去重标志已置 false,再次 addListener 会重复注册。
  3. off 未传 callback:windowClass.off("keyboardWillShow") 会移除该事件所有监听者,可能误伤宿主其他模块。
  4. 版本不满足时静默失败:低版本直接跳过 window.on,但 C++ 已 resolve,JS 拿到合法 handle 却收不到事件。
  5. hide() 提前 resolve:Keyboard.ets:127-134 先挂 catch,随后立刻回 {result:""},Promise 在键盘真正隐藏前就成功。
  6. 跨 WebView 串扰:4 个 bool 去重标志与 m_mapEventListeners 都在单例插件对象上,多 WebView 共享。

这些不是「桥设计必然产生的」,而是「薄适配层 + 手工接线」这种工程方式的典型产物。

5.2 cj-tauri 侧的代价

  1. 新增命令要三处联动:实现 CommandHandler、TauriApp.register()、能力清单声明。漏第二处得到 command not registered,漏第三处得到 command not allowed。
  2. 鸿蒙回程有 50ms 节拍:pull 轮询带来最多 50ms 的额外延迟,队列空时仍有一次同步调用开销。桌面平台没有这个问题(C 桥主动投递)。
  3. 框架包不能引用 ArkUI:真机实测会启动即 SIGSEGV,所以 @Component / Web / javaScriptProxy 必须留在应用侧,框架只导出 ArkUI-free 的桥接函数——装置成本比 Capacitor 的「拷源码进模块」更高。
  4. 能力清单与服务端一致性:白名单提高了安全下限,但也意味着「页面能调什么」不再只由代码决定,还要维护一份清单。

5.3 一句话总结

  • Capacitor 的复杂,是「生态兼容税」:用 3 种语言、7 个环节,换来 JS 侧零改动与整个 Capacitor 插件生态的复用。
  • cj-tauri 的简单,是「自建协议的收益」:用 1 次 FFI、4 个环节,换来统一的命令/事件模型与能力沙箱,代价是平台壳要自己啃。

六、选型建议

你的场景建议理由
已有 Ionic / Capacitor 应用,要上鸿蒙Capacitor生态与 JS 代码零改动,插件可直接复用
需要大量 Capacitor 官方/社区插件Capacitor插件生态是它的核心资产
新写「原生 + WebView」鸿蒙应用,追求内核可读可控cj-tauri桥只有 1 次 FFI,命令模型统一,源码可全量审计
对安全边界(能力白名单、路径 scope)有硬要求cj-tauricapability + scope 是内建能力,不需要自己补
对回程实时性极敏感(< 10ms)Capacitor鸿蒙侧 cj-tauri 受 javaScriptProxy 限制要 50ms pull
需要多窗口 / 多 WebView 隔离cj-tauri协议自带 window label 路由;Capacitor 插件状态是单例

附录 A:源码索引

Capacitor / 键盘插件

关注点位置
插件声明与打包接线plugin.xml
静态注册与工厂src/main/cpp/getcapacitor/Plugin.h:67(REGISTER_CAP_PLUGIN)
方法表注册src/main/cpp/getcapacitor/Plugin.h:379(REGISTER_PLUGIN_METHOD)
C++→ArkTS 调用src/main/cpp/getcapacitor/Plugin.h:141(executeArkTs)
C++→ArkTS 异步版src/main/cpp/getcapacitor/Plugin.h:209(executeArkTsAsync,thread-safe function)
插件实例化src/main/cpp/getcapacitor/PluginManager.cpp:120(registerAllPlugins)
JS 消息入口src/main/cpp/getcapacitor/MessageHandler.cpp:79(postMessage)
JS→C++ 分发src/main/cpp/getcapacitor/Bridge.cpp:67(callPluginMethod)
ArkTS 路由src/main/ets/components/PluginHandler.ets:43
键盘实现src/main/ets/components/Keyboard/Keyboard.ets:100(ShowKeyboard)
键盘事件监听src/main/ets/components/Keyboard/Keyboard.ets:164
Promise 回程src/main/cpp/getcapacitor/PluginCall.cpp:66、MessageHandler.cpp:126
前端桥src/main/resources/rawfile/native-bridge.js:893(toNative)

cj-tauri

关注点位置
IPC 协议定义src/ipc_message.cj(文件头注释即协议)
命令分发src/ipc_hub.cj:213(handleRawMessage)、:244(handleInvoke)
命令接口src/ipc_hub.cj:11(CommandHandler)
能力校验src/capability.cj、src/scope.cj
鸿蒙 proxysrc/arkweb_proxy.cj
前端桥与 pullsrc/arkweb_script.cj
页面消息入口src/host_arkweb.cj:806(acceptPageMessage)
待执行脚本队列src/host_arkweb.cj:733(takePendingScripts)

附录 B:术语

术语含义
NAPINode-API,ArkTS/Node 与 C/C++ 之间的原生接口
FFIForeign Function Interface,仓颉调用 C 或嵌入 JS 引擎的接口层
javaScriptProxy鸿蒙 ArkWeb 提供的「页面 ↔ 原生」代理对象注册机制
capabilitycj-tauri 的能力白名单,声明页面可调用哪些命令 / 事件
keepAliveCapacitor 中让 PluginCall 不随一次响应结束,用于事件持续推送
pull 心跳鸿蒙侧页面定时向原生取待执行脚本的补偿机制(50ms)

本文所有结论均基于上述三个仓库的源码与注释;涉及真机行为的描述(如 javaScriptProxy 返回值不可用、框架包引用 ArkUI 会崩溃)来自 cj-tauri 的踩坑文档与代码注释,未在本环境复跑。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐