Cocos Creator 3.8.6 × HarmonyOS NEXT:JS 调 ArkTS 那条"命名规约桥"到底怎么搭才不翻车

做 Creator 3.8.6 迁鸿蒙的朋友,十有八九会在同一件事上卡住:JS 脚本里想调一个 ArkTS 的原生方法,文档说"复用跨平台调原生静态方法的通用桥接接口",但落地时才发现——方法名怎么写、ArkTS 那边怎么暴露、参数为啥到了对岸变成了 undefined——全靠几条不太显眼的命名规则撑着。 规则不摸透,桥搭上了也传不了货。


一、先想通一件事:这条"桥"为什么非得靠命名规约吃饭?

Cocos Creator 3.x 的 JS 运行时(在 NEXT 上本质是 ArkCompiler 的方舟 JS 引擎)跑在引擎的 C++ 核心之上,C++ 再通过 NAPI 跟 ArkTS 侧通信。
所以 JS → ArkTS 的调用链路,抽象出来长这样:

你的游戏 JS 代码
  → Creator 的原生桥接封装(参数序列化)
    → C++ JSB / NAPI 层(按"类名.方法名"去查找导出符号)
      → ArkTS 侧绑定的原生模块(exports 出来的方法)

关键点来了——NAPI 到 ArkTS 这个边界,不是反射天堂,它更像一个严格的注册表
ArkTS 模块在注册到 NAPI 时,会把自己暴露的方法放进一个有限的结构里(可以理解为一张 string → function 的查找表)。桥那头(C++)拿到你传过来的 "类名" + "方法名",就去表里做字符串匹配,匹配上了才 call。

这套机制在 iOS/Android 上是 className + methodSignature(ObjC selector / Java fully-qualified),在 HarmonyOS NEXT 上本质一致:靠字符串规约定位目标函数
所以 Cocos 3.8.6 说的"核心规则围绕前缀命名和方法名规范展开",翻译成人话就是:

ArkTS 那边暴露的方法,必须能被桥用 [类名]_[方法名] 拼出来的 key 找到;JS 这边传参时,复杂数据走 JSON 字符串,不要指望它自动拆装箱。

二、一次 JS → ArkTS 调用的完整旅程

flowchart TD
    subgraph JS ["🎮 Creator JS 层"]
        A["JS 业务代码调用\nnative.jsCallStaticMethod / 桥接封装"]:::js
    end

    subgraph Serialize ["📦 参数序列化(关键隘口)"]
        B["参数被扁平化为\n字符串/数字等基本值\n(复杂对象 → JSON.stringify)"]:::ser
    end

    subgraph Cpp_NAPI ["⚙️ C++ NAPI 桥接层(Creator 引擎内置)"]
        C["按约定拼 lookupKey\n= className + '_' + methodName\n去模块导出表匹配"]:::cpp
        D{"找到匹配?\nlookupTable[lookupKey]"}
    end

    subgraph ArkTS_Side ["🟣 ArkTS / ETS 原生模块"]
        E["执行目标方法\n解析 paramStr(JSON.parse)\n→ 调真实系统能力"]:::ark
        F["可选:结果回传\n(JSON.stringify(result))"]:::ark
    end

    A --> B --> C --> D
    D -- "✅ 命中" --> E --> F -->|"回调/Promise/全局事件"| A
    D -- "❌ 未命中\n(MethodNotFound / TypeError)" --> G["桥返回 undefined / 空\n或抛异常 → 你看到控制台警告"]:::fail

    style JS fill:#E3F2FD,stroke:#2196F3,stroke-width:2px,color:#0D47A1
    style Serialize fill:#FFF8E1,stroke:#F9A825,stroke-width:2px
    style Cpp_NAPI fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#1B5E20
    style ArkTS_Side fill:#F3E5F5,stroke:#9C27B0,stroke-width:2px,color:#4A148C
    style fail fill:#FFEBEE,stroke:#F443F36,stroke-width:2px,color:#B71C1C

你盯着这个图看十秒就会发现:90% 的坑不在 JS 也不在 ArkTS 逻辑,而在两个隘口——序列化方式 + lookup key 拼法。名字拼错一个下划线,或者参数没串成字符串,调用就静默蒸发了。


三、ArkTS 侧:要"暴露什么"、怎么暴露才符合桥的规矩

Creator 3.8.6 的鸿蒙适配体系里,ArkTS 原生模块一般不是随便找个 .ets 写个函数就行的,它需要被放到 工程约定的原生目录结构 里,通常是:

native/
  ohos/
    entry/
      src/main/ets/
        cocosBridge/
          GameBridge.ets        ← 你暴露给 JS 调用的"类"
          index.ets             ← 注册出口

3.1 核心写法:函数要"可被字符串找到"

最常见、最稳的写法:把方法挂到一个对象上,导出该对象,然后在 NAPI 注册时让桥认出来
Cocos 的惯例是把"类名"当命名空间前缀,函数名用 _ 拼接——所以你看到的典型风格是这样的:

// native/ohos/entry/src/main/ets/cocosBridge/GameBridge.ets
// 这个类不是真的 TS class,而是"桥认知的命名空间对象"
export const GameBridge = {

  // ── 规则:桥会按 "GameBridge_showToast" 来找这个方法 ──
  //     className  = "GameBridge"
  //     methodName  = "showToast"
  //     实际 key   = "GameBridge_showToast"

  GameBridge_showToast(paramsStr: string): string {
    try {
      const p = JSON.parse(paramsStr || '{}');
      // 调用鸿蒙系统能力
      console.info(`[GameBridge] toast: ${p.text}`);

      // 真机上你可以用 @kit.ArkUI 的 promptAction
      // 这里先做 console 版,确保桥先通
      return JSON.stringify({ ok: true });
    } catch (e) {
      return JSON.stringify({ ok: false, error: String(e) });
    }
  },

  // 第二个方法:读设备信息
  GameBridge_getDeviceLabel(paramsStr: string): string {
    const ctx = getContext(this);
    const label = `${ctx.applicationInfo.name ?? 'game'}@${ctx.applicationInfo.versionName ?? '?'}`;
    return JSON.stringify({ label });
  },
};

// index.ets —— 需要被 bridge 注册进 NAPI 的导出点
export default GameBridge;
这个写法里最关键的心智模型
你在 ArkTS 里不是写 class GameBridge { showToast() {} } 然后用 new
桥那边的 NAPI 查找逻辑是字符串 → function,所以函数必须作为对象的直接属性存在,且属性名必须匹配 ClassName_MethodName
这就是为什么文档强调"前缀命名和方法名规范"——它不是一个风格建议,是桥的寻址协议。

四、JS 侧:Creator 里怎么"伸手"调到上面那个 toast

Creator 3.8.6 在 NEXT 上的 JS→原生桥,通常走下面这种调用式(不同分支可能包装略有不同,但语义一致):

// assets/scripts/native/Bridge.ts
export class NativeBridge {
  // 通用静态调用封装
  static call(className: string, methodName: string, params: any = null): string | undefined {
    // 注意:参数必须序列化成字符串传过去(NAPI 边界的可靠交集)
    const paramStr = params !== null ? JSON.stringify(params) : '{}';

    // Cocos Creator 3.x 在 NEXT 的桥入口(具体名称以你用的 3.8.6 分支实际暴露为准)
    const ret = (jsb ?? globalThis).jsCallStaticMethod?.(
      className,
      methodName,
      paramStr   // ← 通常就这一个参数位:JSON 字符串
    );

    return typeof ret === 'string' ? ret : undefined;
  }

  // 业务方法:吐司
  static toast(text: string) {
    return this.call('GameBridge', 'showToast', { text });
  }

  // 业务方法:拿设备标签
  static getDeviceLabel(): string | undefined {
    const r = this.call('GameBridge', 'getDeviceLabel', {});
    try { return JSON.parse(r ?? '{}').label; } catch { return undefined; }
  }
}

调用处就很干净了:

// 某个按钮回调里
NativeBridge.toast('HarmonyOS NEXT 桥接 OK 🎉');
console.log('device:', NativeBridge.getDeviceLabel());

五、你一定会踩的"差异"——为什么有的项目写着像、跑着就不通

差异 1:class Foo { bar() } 看起来对,但桥找不到

有人会写:

// 这种写法在桥的字符串查找体系里很危险
export class GameBridge {
  showToast(p: string) {}
}

问题是:类方法在 ArkTS/TS 编译后不一定是 GameBridge.showToast 这种 flat key,而且桥那边的 NAPI 注册表是按 "GameBridge_showToast" 做字符串匹配,不是走 Reflect.get(instance, 'showToast')
所以最稳就是我上面给的那种 plain object + ClassName_MethodName 显式拼接 的写法——丑一点,但桥认。

差异 2:参数传对象 { text: 'hi' } 而不是 JSON 字符串

桥的 NAPI 边界通常只担保基本值(number/string)稳定穿越。你直接塞对象:

//很可能在对岸变成 "[object Object]" 或 undefined
jsb.jsCallStaticMethod('GameBridge','showToast', { text:'hi' });

正确做法永远是:你来控制序列化


jsb.jsCallStaticMethod('GameBridge','showToast', JSON.stringify({ text:'hi' }));

ArkTS 侧对应地 JSON.parse(paramsStr)

差异 3:调试时看不到报错,只看到"没反应"

因为 NAPI 侧找不到 key 时,很多实现会选择 fail silent / return undefined,而不是抛红字异常——毕竟游戏运行时不能因为一个统计事件桥断掉就白屏。
排查口诀:先看 ArkTS 的方法名是不是 ClassName_MethodName 拼法;再看 JS 传的 className / methodName 拼写;最后确认 JSON.stringify 没炸(比如参数里含循环引用)


六、小拓展

目前 Creator 3.8.6 的桥是"稳定可用"状态;API 22 如果动这块,大概率不是废除它,而是收紧边界 + 更严格类型。你需要提前守三条线:

1. 别依赖 jsb 的全局隐式存在当保险箱

API 22 的 ArkCompiler/运行时可能更 aggressively 清理或重排全局命名(尤其如果未来走 ComponentV2 或更强的沙箱化)。
你现在的封装里加一行能力嗅探,以后就不用地毯式改:

// Bridge.ts (JS侧)
private static get bridgeAPI(): any {
  return (globalThis as any).jsb ?? (globalThis as any).nativeBridge ?? null;
}

2. ArkTS 侧别用花哨的动态 Proxy / 运行时挂属性

API 22 的 ArkTS 对对象形状追踪会更严(趋向更静态可分析)。你现在的 const GameBridge = { GameBridge_foo(){} } 这种 plain object 反而是最抗未来的形态;别想着"优雅反射自动注册"去赌。

3. NAPI 模块路径/注册方式可能标准化成官方模板

如果你现在用的是 Creator 3.8.6 生成的 entry/ets 模板,它未来大概率只会被官方模板升级推动你同步(而不是破坏性删除重来)。守的规矩就是:桥的契约 = ClassName_MethodName key + JSON 字符串传参——这条不变,你的 JS 调用代码基本不用动;ArkTS 侧最多跟着模板对齐目录/导出写法。


七、总结一下下

Cocos Creator 3.8.6 在 HarmonyOS NEXT 上 JS→ArkTS 的调用,说穿了就是一个靠字符串寻址的 NAPI 导出表 + 一个靠 JSON 字符串过边界的序列化约定
你只要守住两条纪律——ArkTS 暴露的方法名严格遵守 ClassName_MethodName、JS 传参永远自己 JSON.stringify 并且对面 JSON.parse——这条桥就会非常老实、非常好排查、也最能扛未来的 API 升级。


蓝胖子样样好
79 声望701 粉丝

Never give up,and you will be successful