From 065ebb1f58171b0b14313b01fde90c04bbd83aee Mon Sep 17 00:00:00 2001 From: Joywayer Date: Mon, 31 Aug 2026 20:49:51 +0800 Subject: [PATCH] =?UTF-8?q?refactor(framework):=20native-bridge=20?= =?UTF-8?q?=E5=86=85=E9=83=A8=E5=91=BD=E5=90=8D=E7=8E=B0=E4=BB=A3=E5=8C=96?= =?UTF-8?q?(=E5=A4=96=E9=83=A8=E5=A5=91=E7=BA=A6=E4=BF=9D=E7=95=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 外部契约逐字不变(与原生 app 零改动对接): - handler 名白名单 14 个字符串(getVideoinfo/sharelogin/...) - window.WVJBCallbacks / WebViewJavascriptBridgeReady 事件名 - window.settings.getothername API - 回调签名 (data, responseCallback) 与数据结构 内部命名走现代专业风格: - WVJB → NativeBridgeChannel(双向通信通道语义清晰) - WVJBHandler → NativeHandler - WVJBCallback → NativeResponseCallback - WVJB_HANDLER_NAMES → NATIVE_HANDLER_NAMES - WVJBHandlerName → NativeHandlerName - createNativeBridge → createBridge - getSetting → getNativeSetting(明确是原生注入) - NativeBridge interface → Bridge(隐含 Native) - isKnownHandler → isNativeHandlerName - __nbBridge → __activeChannel - __nbHandlers → __registeredHandlers 144/144 tests pass, typecheck exit 0。 Co-Authored-By: Claude Opus 5 (1M context) --- .../framework/platform/native-bridge.ts | 129 ++++++++++-------- .../platform/native-bridge.test.ts | 112 ++++++++------- 2 files changed, 131 insertions(+), 110 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts index 28a0417..f55a93f 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts @@ -1,45 +1,57 @@ /** - * 原生↔H5 异步桥(WebViewJavascriptBridge,WVJB)+ window.settings 同步取值。 + * 原生↔H5 异步桥(WebViewJavascriptBridge,WVJB)+ window.settings 同步取值。 * - * 严格对齐原 gameabc 项目(05_Func.js:2627 起)的接口名、数据格式、回调约定, - * 让原生侧零改动即可对接。 + * 严格对齐原 gameabc 项目(05_Func.js:2627 起)的外部契约: + * - 14 个 handler 名白名单(与原生 app 逐字对齐) + * - `window.WVJBCallbacks` / `WebViewJavascriptBridgeReady` 事件名 + * - `window.settings.getothername(name)` 同步取值 API + * - 回调签名 `(data, responseCallback)` 与数据结构 * - * 架构边界:本模块只暴露受限 facade,不允许 framework 业务代码直接读 window/bridge 内部。 + * 内部命名(接口/函数/常量)走现代专业风格,与上述外部契约清晰隔离。 + * + * 架构边界:本模块只暴露受限 facade,不允许 framework 业务代码直接读 + * window/bridge 内部。 */ -/** window 上的原生注入对象(05_Func.js:2467-2471 同步取值)。) */ +/** window 上的原生注入对象(05_Func.js:2467-2471 同步取值)。 */ declare global { interface Window { settings?: { - /** 同步取一个原生配置项,无则返回空串 */ + /** 同步取一个原生配置项,无则返回空串 */ getothername(name: string): string; }; - /** WVJB 启动前的全局回调队列 */ - WVJBCallbacks?: Array<(bridge: WVJB) => void>; - /** WebView 注入 WVJB 的事件名 */ + /** WVJB 启动前的全局回调队列(外部契约,不能改名) */ + WVJBCallbacks?: Array<(bridge: NativeBridgeChannel) => void>; + /** WebView 注入 WVJB 的事件名(外部契约) */ WebViewJavascriptBridgeReady?: Array<() => void>; } } -/** WVJB bridge 实例的最小契约(只用到 framework 需要的子集) */ -export interface WVJB { +/** + * 原生桥通道契约 —— WVJB bridge 实例的最小子集。 + * 内部命名为 "Channel" 以避免缩写、明确"双向通信通道"语义。 + */ +export interface NativeBridgeChannel { /** H5 注册供原生侧调用的 handler */ - registerHandler(name: string, handler: WVJBHandler): void; + registerHandler(name: string, handler: NativeHandler): void; /** H5 主动调用原生侧 */ callHandler(name: string, data: unknown, responseCallback?: (resp: unknown) => void): void; } -/** handler 函数:H5 注册供原生调用时,函数返回 Promise 化值 */ -export type WVJBHandler = (data: unknown, responseCallback: WVJBCallback) => unknown; +/** H5 注册供原生调用的 handler 函数签名(外部 WVJB 协议约定) */ +export type NativeHandler = (data: unknown, responseCallback: NativeResponseCallback) => unknown; -/** 响应回调:原生侧通过此把结果回传给 H5 */ -export type WVJBCallback = (responseData: unknown) => void; +/** 响应回调:原生侧通过此把结果回传给 H5 */ +export type NativeResponseCallback = (responseData: unknown) => void; /** - * 13+ 已注册 handler 名白名单(05_Func.js:2651+ 原项目实装)。 - * 字面量联合类型:registerHandler/callHandler 入参必须在此集合内,否则显式抛错(第二准则:不静默吞包)。 + * 14 个已注册 handler 名白名单(05_Func.js:2651+ 原项目实装)。 + * 与原生 app 端逐字对齐 —— app 按这些名字调,改名 app 也要改。 + * + * 字面量联合类型:registerHandler/callHandler 入参必须在此集合内, + * 拼写错误编译期就发现,集合外名字运行时显式抛错(第二准则:不静默兜底)。 */ -export const WVJB_HANDLER_NAMES = [ +export const NATIVE_HANDLER_NAMES = [ 'getVideoinfo', 'sharelogin', 'sharesuccess', @@ -56,83 +68,84 @@ export const WVJB_HANDLER_NAMES = [ 'shakeEnd', ] as const; -export type WVJBHandlerName = (typeof WVJB_HANDLER_NAMES)[number]; +export type NativeHandlerName = (typeof NATIVE_HANDLER_NAMES)[number]; -function isKnownHandler(name: string): name is WVJBHandlerName { - return (WVJB_HANDLER_NAMES as readonly string[]).includes(name); +function isNativeHandlerName(name: string): name is NativeHandlerName { + return (NATIVE_HANDLER_NAMES as readonly string[]).includes(name); } -/** createNativeBridge 初始化选项 */ -export interface NativeBridgeOptions { - /** bridge 就绪回调 */ - onReady(bridge: WVJB): void; +/** createBridge 初始化选项 */ +export interface BridgeOptions { + /** 通道就绪回调(原生侧 WebView bridgeReady 时触发一次) */ + onReady(channel: NativeBridgeChannel): void; /** - * bridge 初始化失败的兜底(可省略)。 - * 原生侧可能永不触发 WebViewJavascriptBridgeReady,业务可能想超时降级。 + * 初始化超时的兜底(可省略)。 + * 原生侧可能永不触发 WebViewJavascriptBridgeReady,业务可在此降级。 */ onTimeout?(reason: string): void; } /** - * 受限 NativeBridge facade。 + * 受限 Bridge facade —— 框架业务访问原生能力的唯一入口。 * - * 创建后立即尝试初始化: - * 1. 若 window.WVJBCallbacks 已存在 → push 入队(Cocos/原生侧约定的初始化入口) - * 2. 否则保留空方法,业务侧可调用 isReady() 查询状态 + * 创建后立即尝试初始化: + * 1. 把 setup 回调 push 到 window.WVJBCallbacks(原生侧约定的入口) + * 2. 原生侧 WebView bridgeReady 时遍历 WVJBCallbacks 调用每个 cb → onReady 被触发 * - * 一旦 bridge 就绪,onReady 被调用一次。 + * 一旦通道就绪,onReady 被调用一次。 */ -export interface NativeBridge { - /** 当前是否已就绪 */ +export interface Bridge { + /** 当前通道是否已就绪 */ isReady(): boolean; - /** H5 注册供原生侧调用(白名单校验) */ - registerHandler(name: WVJBHandlerName, handler: WVJBHandler): void; - /** H5 调用原生侧(白名单校验) */ - callHandler(name: WVJBHandlerName, data: unknown, cb?: WVJBCallback): void; + /** H5 注册供原生侧调用(白名单校验) */ + registerHandler(name: NativeHandlerName, handler: NativeHandler): void; + /** H5 调用原生侧(白名单校验) */ + callHandler(name: NativeHandlerName, data: unknown, cb?: NativeResponseCallback): void; } -export function createNativeBridge(opts: NativeBridgeOptions): NativeBridge { +export function createBridge(opts: BridgeOptions): Bridge { const g = globalThis as any; - // 原生侧约定的初始化入口:把 setup cb push 到 window.WVJBCallbacks + // 外部契约:把 setup cb push 到 window.WVJBCallbacks if (!g.WVJBCallbacks) g.WVJBCallbacks = []; let ready = false; - g.WVJBCallbacks.push((bridge: WVJB) => { + g.WVJBCallbacks.push((channel: NativeBridgeChannel) => { ready = true; - // 存到 globalThis 供 callHandler 委托 - g.__nbBridge = bridge; - opts.onReady(bridge); + // 缓存通道供 callHandler 委托(避免 onReady 闭包外传) + g.__activeChannel = channel; + opts.onReady(channel); }); return { isReady() { return ready; }, registerHandler(name, handler) { - if (!isKnownHandler(name)) { - throw new Error(`registerHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); + if (!isNativeHandlerName(name)) { + throw new Error(`registerHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); } - if (!ready) throw new Error('NativeBridge not ready: bridge 未初始化'); - g.__nbBridge.registerHandler(name, handler); + if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调'); + g.__activeChannel.registerHandler(name, handler); }, callHandler(name, data, cb) { - if (!isKnownHandler(name)) { - throw new Error(`callHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); + if (!isNativeHandlerName(name)) { + throw new Error(`callHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); } - if (!ready) throw new Error('NativeBridge not ready: bridge 未初始化'); - g.__nbBridge.callHandler(name, data, cb); + if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调'); + g.__activeChannel.callHandler(name, data, cb); }, }; } /** - * 从 window 取原生注入的配置项(05_Func.js:2467-2471 的 getothername)。 + * 从 window 取原生注入的配置项(05_Func.js:2467-2471 的 getothername)。 * - * @param name 配置项名(渠道/包信息/gameserver 覆盖等) - * @returns 原生侧返回值;缺失时返回空串(对齐原项目:无则空字符串而非 undefined) + * @param name 配置项名(渠道/包信息/gameserver 覆盖等) + * @returns 原生侧返回值;缺失时返回空串(对齐原项目:无则空字符串而非 undefined) + * @throws 原生侧未注入 window.settings 时显式抛错(不静默返回空串——避免下游误用) */ -export function getSetting(name: string): string { +export function getNativeSetting(name: string): string { const settings = (globalThis as any).window?.settings; if (!settings || typeof settings.getothername !== 'function') { throw new Error( - `getSetting("${name}"): window.settings 未注入(原生侧未加载或非 WebView 环境)`, + `getNativeSetting("${name}"): window.settings 未注入(原生侧未加载或非 WebView 环境)`, ); } return settings.getothername(name); diff --git a/cocoscreator_projects/framework-tests/platform/native-bridge.test.ts b/cocoscreator_projects/framework-tests/platform/native-bridge.test.ts index 732f543..dbb3d51 100644 --- a/cocoscreator_projects/framework-tests/platform/native-bridge.test.ts +++ b/cocoscreator_projects/framework-tests/platform/native-bridge.test.ts @@ -1,10 +1,11 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { - createNativeBridge, - type WVJBHandler, - type WVJBCallback, - // type-only, see below + createBridge, + getNativeSetting, + type NativeHandler, + type NativeResponseCallback, + type NativeBridgeChannel, } from '../../YouleNexus/assets/framework/platform/native-bridge.ts'; /** @@ -13,51 +14,52 @@ import { function makeEnv() { const calls: { handler: string; data: unknown }[] = []; const responses: Map void> = new Map(); - const handlers: Map = new Map(); + const handlers: Map = new Map(); - const bridge = { - registerHandler(name: string, h: WVJBHandler) { handlers.set(name, h); }, - callHandler(name: string, data: unknown, cb?: (resp: unknown) => void) { + const channel: NativeBridgeChannel = { + registerHandler(name, h) { handlers.set(name, h); }, + callHandler(name, data, cb) { calls.push({ handler: name, data }); if (cb) responses.set(name, cb); }, - _handlers: handlers, - _calls: calls, - _responses: responses, }; - // 模拟 WebView:bridge 已就绪,setup cb 立即触发 const g = globalThis as any; const oldWVJB = g.WVJBCallbacks; - // mock:WVJBCallbacks 是空数组,createNativeBridge 调用时 push 自己的 cb - // 测试需要主动触发 onReady 时,调用 triggerReady() g.WVJBCallbacks = []; + // mock 原生侧 WebView bridge 就绪:遍历 WVJBCallbacks 全调用 const triggerReady = () => { - // 模拟原生侧 WebView bridge 就绪,遍历 WVJBCallbacks 调用每个 cb - for (const cb of [...g.WVJBCallbacks]) cb(bridge); + for (const cb of [...g.WVJBCallbacks]) cb(channel); }; - return { bridge, restore: () => { g.WVJBCallbacks = oldWVJB; }, calls, handlers, triggerReady }; + return { + channel, + restore: () => { g.WVJBCallbacks = oldWVJB; }, + calls, + handlers, + responses, + triggerReady, + }; } -test('createNativeBridge: bridge 就绪时 setup cb 触发', () => { +test('createBridge: 通道就绪时 onReady 触发', () => { const env = makeEnv(); let got: any = null; - const nb = createNativeBridge({ - onReady(bridge) { got = bridge; }, + const bridge = createBridge({ + onReady(c) { got = c; }, }); env.triggerReady(); - assert.equal(got, env.bridge, 'onReady 应收到 mock bridge'); + assert.equal(got, env.channel, 'onReady 应收到 mock channel'); env.restore(); }); test('registerHandler: H5 注册供原生调用', () => { const env = makeEnv(); let received: any = null; - const nb = createNativeBridge({ - onReady(b) { - (globalThis as any).__nbBridge = b; - nb.registerHandler('getVideoinfo', (data) => { + const bridge = createBridge({ + onReady(c) { + (globalThis as any).__activeChannel = c; + bridge.registerHandler('getVideoinfo', (data) => { received = data; return { ok: true }; }); @@ -67,33 +69,33 @@ test('registerHandler: H5 注册供原生调用', () => { // 原生侧调用 const handler = env.handlers.get('getVideoinfo'); assert.ok(handler, 'handler 应已注册'); - const ret = handler!({ vid: 'abc' }, () => {}); + handler!({ vid: 'abc' }, (() => {}) as NativeResponseCallback); assert.deepEqual(received, { vid: 'abc' }); env.restore(); }); test('callHandler: H5 调用原生,响应通过 cb 接收', () => { const env = makeEnv(); - // onReady 必须把 bridge 存到 __nbBridge,callHandler 才能找到 - const nb = createNativeBridge({ - onReady(b) { (globalThis as any).__nbBridge = b; }, + const bridge = createBridge({ + onReady(c) { (globalThis as any).__activeChannel = c; }, }); env.triggerReady(); let resp: unknown = null; - nb.callHandler('getBattery', {}, (r) => { resp = r; }); + bridge.callHandler('getBattery', {}, (r) => { resp = r; }); // 模拟原生侧响应 - const cb = env.bridge._responses.get('getBattery'); + const cb = env.responses.get('getBattery'); assert.ok(cb, 'callHandler 应记录 cb'); cb!({ level: 80, charging: false }); assert.deepEqual(resp, { level: 80, charging: false }); env.restore(); }); -test('callHandler: handler 名不在白名单 → 显式抛错(第二准则:不静默吞包)', () => { +test('callHandler: handler 名不在白名单 → 显式抛错(第二准则)', () => { const env = makeEnv(); - const nb = createNativeBridge({ onReady: () => {} }); + const bridge = createBridge({ onReady: () => {} }); + env.triggerReady(); assert.throws( - () => nb.callHandler('unknown_handler' as any, {}, () => {}), + () => bridge.callHandler('unknown_handler' as any, {}, () => {}), /unknown_handler/, ); env.restore(); @@ -101,42 +103,48 @@ test('callHandler: handler 名不在白名单 → 显式抛错(第二准则:不 test('registerHandler: handler 名不在白名单 → 显式抛错', () => { const env = makeEnv(); - const nb = createNativeBridge({ onReady: () => {} }); + const bridge = createBridge({ onReady: () => {} }); + env.triggerReady(); assert.throws( - () => nb.registerHandler('unknown_handler' as any, () => undefined), + () => bridge.registerHandler('unknown_handler' as any, () => undefined), /unknown_handler/, ); env.restore(); }); -test('bridge 未就绪时 callHandler/registerHandler 显式抛错(不在初始化就调用)', () => { - // 不调用 onReady,bridge 永不触发 +test('bridge 未就绪时 callHandler/registerHandler 显式抛错', () => { const g = globalThis as any; const old = g.WVJBCallbacks; - g.WVJBCallbacks = []; // 空队列,setup 永不触发 - const nb = createNativeBridge({ onReady: () => {} }); - assert.throws(() => nb.callHandler('getBattery', {}, () => {}), /not ready/i); - assert.throws(() => nb.registerHandler('getVideoinfo', (() => ({})) as any), /not ready/i); + g.WVJBCallbacks = []; // 空队列,triggerReady 不被调用 + const bridge = createBridge({ onReady: () => {} }); + assert.throws(() => bridge.callHandler('getBattery', {}, () => {}), /not ready/i); + assert.throws(() => bridge.registerHandler('getVideoinfo', (() => undefined) as any), /not ready/i); g.WVJBCallbacks = old; }); -test('重复 createNativeBridge: 多个 instance 各自注册独立回调到 WVJBCallbacks', () => { +test('重复 createBridge: 多个 instance 各自注册独立回调', () => { const g = globalThis as any; const old = g.WVJBCallbacks; g.WVJBCallbacks = []; - const nb1 = createNativeBridge({ onReady: () => {} }); - const nb2 = createNativeBridge({ onReady: () => {} }); - // 每个 createNativeBridge push 一个 cb(原生侧 bridgeReady 时全触发,业务侧通过闭包区分) + createBridge({ onReady: () => {} }); + createBridge({ onReady: () => {} }); assert.equal(g.WVJBCallbacks.length, 2, '每个 instance 独立注册一个 cb'); g.WVJBCallbacks = old; }); -test('window.settings: getSetting 同步取值', () => { +test('getNativeSetting: 同步取 window.settings.getothername', () => { const g = globalThis as any; - const old = g.window; + const oldWindow = g.window; g.window = { settings: { getothername: (n: string) => n === 'agentid' ? 'A' : '' } }; - // dynamic import to mutate after module load? 这里简单测: window injected already - // 用 require 方式不可行,直接验证 window injection 模式 - assert.equal(g.window.settings.getothername('agentid'), 'A'); - g.window = old; + assert.equal(getNativeSetting('agentid'), 'A'); + assert.equal(getNativeSetting('missing'), ''); + g.window = oldWindow; +}); + +test('getNativeSetting: window.settings 未注入 → 显式抛错(不静默兜底)', () => { + const g = globalThis as any; + const oldWindow = g.window; + g.window = {}; // 无 settings + assert.throws(() => getNativeSetting('agentid'), /window\.settings 未注入/); + g.window = oldWindow; }); \ No newline at end of file