refactor(framework): native-bridge 内部命名现代化(外部契约保留)

外部契约逐字不变(与原生 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) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 20:49:51 +08:00
co-authored by Claude Opus 5
parent 9b69bb8611
commit 065ebb1f58
2 changed files with 131 additions and 110 deletions
@@ -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 { declare global {
interface Window { interface Window {
settings?: { settings?: {
/** 同步取一个原生配置项,无则返回空串 */ /** 同步取一个原生配置项,无则返回空串 */
getothername(name: string): string; getothername(name: string): string;
}; };
/** WVJB 启动前的全局回调队列 */ /** WVJB 启动前的全局回调队列(外部契约,不能改名) */
WVJBCallbacks?: Array<(bridge: WVJB) => void>; WVJBCallbacks?: Array<(bridge: NativeBridgeChannel) => void>;
/** WebView 注入 WVJB 的事件名 */ /** WebView 注入 WVJB 的事件名(外部契约) */
WebViewJavascriptBridgeReady?: Array<() => void>; WebViewJavascriptBridgeReady?: Array<() => void>;
} }
} }
/** WVJB bridge 实例的最小契约(只用到 framework 需要的子集) */ /**
export interface WVJB { * 原生桥通道契约 —— WVJB bridge 实例的最小子集。
* 内部命名为 "Channel" 以避免缩写、明确"双向通信通道"语义。
*/
export interface NativeBridgeChannel {
/** H5 注册供原生侧调用的 handler */ /** H5 注册供原生侧调用的 handler */
registerHandler(name: string, handler: WVJBHandler): void; registerHandler(name: string, handler: NativeHandler): void;
/** H5 主动调用原生侧 */ /** H5 主动调用原生侧 */
callHandler(name: string, data: unknown, responseCallback?: (resp: unknown) => void): void; callHandler(name: string, data: unknown, responseCallback?: (resp: unknown) => void): void;
} }
/** handler 函数:H5 注册供原生调用时,函数返回 Promise 化值 */ /** H5 注册供原生调用的 handler 函数签名(外部 WVJB 协议约定) */
export type WVJBHandler = (data: unknown, responseCallback: WVJBCallback) => unknown; export type NativeHandler = (data: unknown, responseCallback: NativeResponseCallback) => unknown;
/** 响应回调:原生侧通过此把结果回传给 H5 */ /** 响应回调:原生侧通过此把结果回传给 H5 */
export type WVJBCallback = (responseData: unknown) => void; export type NativeResponseCallback = (responseData: unknown) => void;
/** /**
* 13+ 已注册 handler 名白名单(05_Func.js:2651+ 原项目实装)。 * 14 个已注册 handler 名白名单(05_Func.js:2651+ 原项目实装)。
* 字面量联合类型:registerHandler/callHandler 入参必须在此集合内,否则显式抛错(第二准则:不静默吞包)。 * 与原生 app 端逐字对齐 —— app 按这些名字调,改名 app 也要改。
*
* 字面量联合类型:registerHandler/callHandler 入参必须在此集合内,
* 拼写错误编译期就发现,集合外名字运行时显式抛错(第二准则:不静默兜底)。
*/ */
export const WVJB_HANDLER_NAMES = [ export const NATIVE_HANDLER_NAMES = [
'getVideoinfo', 'getVideoinfo',
'sharelogin', 'sharelogin',
'sharesuccess', 'sharesuccess',
@@ -56,83 +68,84 @@ export const WVJB_HANDLER_NAMES = [
'shakeEnd', 'shakeEnd',
] as const; ] 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 { function isNativeHandlerName(name: string): name is NativeHandlerName {
return (WVJB_HANDLER_NAMES as readonly string[]).includes(name); return (NATIVE_HANDLER_NAMES as readonly string[]).includes(name);
} }
/** createNativeBridge 初始化选项 */ /** createBridge 初始化选项 */
export interface NativeBridgeOptions { export interface BridgeOptions {
/** bridge 就绪回调 */ /** 通道就绪回调(原生侧 WebView bridgeReady 时触发一次) */
onReady(bridge: WVJB): void; onReady(channel: NativeBridgeChannel): void;
/** /**
* bridge 初始化失败的兜底(可省略)。 * 初始化超时的兜底(可省略)。
* 原生侧可能永不触发 WebViewJavascriptBridgeReady,业务可能想超时降级。 * 原生侧可能永不触发 WebViewJavascriptBridgeReady,业务可在此降级。
*/ */
onTimeout?(reason: string): void; onTimeout?(reason: string): void;
} }
/** /**
* 受限 NativeBridge facade。 * 受限 Bridge facade —— 框架业务访问原生能力的唯一入口。
* *
* 创建后立即尝试初始化: * 创建后立即尝试初始化:
* 1. 若 window.WVJBCallbacks 已存在 → push 入队(Cocos/原生侧约定的初始化入口) * 1. 把 setup 回调 push 到 window.WVJBCallbacks(原生侧约定的入口)
* 2. 否则保留空方法,业务侧可调用 isReady() 查询状态 * 2. 原生侧 WebView bridgeReady 时遍历 WVJBCallbacks 调用每个 cb → onReady 被触发
* *
* 一旦 bridge 就绪,onReady 被调用一次。 * 一旦通道就绪,onReady 被调用一次。
*/ */
export interface NativeBridge { export interface Bridge {
/** 当前是否已就绪 */ /** 当前通道是否已就绪 */
isReady(): boolean; isReady(): boolean;
/** H5 注册供原生侧调用(白名单校验) */ /** H5 注册供原生侧调用(白名单校验) */
registerHandler(name: WVJBHandlerName, handler: WVJBHandler): void; registerHandler(name: NativeHandlerName, handler: NativeHandler): void;
/** H5 调用原生侧(白名单校验) */ /** H5 调用原生侧(白名单校验) */
callHandler(name: WVJBHandlerName, data: unknown, cb?: WVJBCallback): void; 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; const g = globalThis as any;
// 原生侧约定的初始化入口:把 setup cb push 到 window.WVJBCallbacks // 外部契约:把 setup cb push 到 window.WVJBCallbacks
if (!g.WVJBCallbacks) g.WVJBCallbacks = []; if (!g.WVJBCallbacks) g.WVJBCallbacks = [];
let ready = false; let ready = false;
g.WVJBCallbacks.push((bridge: WVJB) => { g.WVJBCallbacks.push((channel: NativeBridgeChannel) => {
ready = true; ready = true;
// 存到 globalThis 供 callHandler 委托 // 缓存通道供 callHandler 委托(避免 onReady 闭包外传)
g.__nbBridge = bridge; g.__activeChannel = channel;
opts.onReady(bridge); opts.onReady(channel);
}); });
return { return {
isReady() { return ready; }, isReady() { return ready; },
registerHandler(name, handler) { registerHandler(name, handler) {
if (!isKnownHandler(name)) { if (!isNativeHandlerName(name)) {
throw new Error(`registerHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); throw new Error(`registerHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`);
} }
if (!ready) throw new Error('NativeBridge not ready: bridge 未初始化'); if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调');
g.__nbBridge.registerHandler(name, handler); g.__activeChannel.registerHandler(name, handler);
}, },
callHandler(name, data, cb) { callHandler(name, data, cb) {
if (!isKnownHandler(name)) { if (!isNativeHandlerName(name)) {
throw new Error(`callHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`); throw new Error(`callHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`);
} }
if (!ready) throw new Error('NativeBridge not ready: bridge 未初始化'); if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调');
g.__nbBridge.callHandler(name, data, cb); g.__activeChannel.callHandler(name, data, cb);
}, },
}; };
} }
/** /**
* 从 window 取原生注入的配置项(05_Func.js:2467-2471 的 getothername)。 * 从 window 取原生注入的配置项(05_Func.js:2467-2471 的 getothername)。
* *
* @param name 配置项名(渠道/包信息/gameserver 覆盖等) * @param name 配置项名(渠道/包信息/gameserver 覆盖等)
* @returns 原生侧返回值;缺失时返回空串(对齐原项目:无则空字符串而非 undefined) * @returns 原生侧返回值;缺失时返回空串(对齐原项目:无则空字符串而非 undefined)
* @throws 原生侧未注入 window.settings 时显式抛错(不静默返回空串——避免下游误用)
*/ */
export function getSetting(name: string): string { export function getNativeSetting(name: string): string {
const settings = (globalThis as any).window?.settings; const settings = (globalThis as any).window?.settings;
if (!settings || typeof settings.getothername !== 'function') { if (!settings || typeof settings.getothername !== 'function') {
throw new Error( throw new Error(
`getSetting("${name}"): window.settings 未注入(原生侧未加载或非 WebView 环境)`, `getNativeSetting("${name}"): window.settings 未注入(原生侧未加载或非 WebView 环境)`,
); );
} }
return settings.getothername(name); return settings.getothername(name);
@@ -1,10 +1,11 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
createNativeBridge, createBridge,
type WVJBHandler, getNativeSetting,
type WVJBCallback, type NativeHandler,
// type-only, see below type NativeResponseCallback,
type NativeBridgeChannel,
} from '../../YouleNexus/assets/framework/platform/native-bridge.ts'; } from '../../YouleNexus/assets/framework/platform/native-bridge.ts';
/** /**
@@ -13,51 +14,52 @@ import {
function makeEnv() { function makeEnv() {
const calls: { handler: string; data: unknown }[] = []; const calls: { handler: string; data: unknown }[] = [];
const responses: Map<string, (data: any) => void> = new Map(); const responses: Map<string, (data: any) => void> = new Map();
const handlers: Map<string, WVJBHandler> = new Map(); const handlers: Map<string, NativeHandler> = new Map();
const bridge = { const channel: NativeBridgeChannel = {
registerHandler(name: string, h: WVJBHandler) { handlers.set(name, h); }, registerHandler(name, h) { handlers.set(name, h); },
callHandler(name: string, data: unknown, cb?: (resp: unknown) => void) { callHandler(name, data, cb) {
calls.push({ handler: name, data }); calls.push({ handler: name, data });
if (cb) responses.set(name, cb); if (cb) responses.set(name, cb);
}, },
_handlers: handlers,
_calls: calls,
_responses: responses,
}; };
// 模拟 WebView:bridge 已就绪,setup cb 立即触发
const g = globalThis as any; const g = globalThis as any;
const oldWVJB = g.WVJBCallbacks; const oldWVJB = g.WVJBCallbacks;
// mock:WVJBCallbacks 是空数组,createNativeBridge 调用时 push 自己的 cb
// 测试需要主动触发 onReady 时,调用 triggerReady()
g.WVJBCallbacks = []; g.WVJBCallbacks = [];
// mock 原生侧 WebView bridge 就绪:遍历 WVJBCallbacks 全调用
const triggerReady = () => { const triggerReady = () => {
// 模拟原生侧 WebView bridge 就绪,遍历 WVJBCallbacks 调用每个 cb for (const cb of [...g.WVJBCallbacks]) cb(channel);
for (const cb of [...g.WVJBCallbacks]) cb(bridge);
}; };
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(); const env = makeEnv();
let got: any = null; let got: any = null;
const nb = createNativeBridge({ const bridge = createBridge({
onReady(bridge) { got = bridge; }, onReady(c) { got = c; },
}); });
env.triggerReady(); env.triggerReady();
assert.equal(got, env.bridge, 'onReady 应收到 mock bridge'); assert.equal(got, env.channel, 'onReady 应收到 mock channel');
env.restore(); env.restore();
}); });
test('registerHandler: H5 注册供原生调用', () => { test('registerHandler: H5 注册供原生调用', () => {
const env = makeEnv(); const env = makeEnv();
let received: any = null; let received: any = null;
const nb = createNativeBridge({ const bridge = createBridge({
onReady(b) { onReady(c) {
(globalThis as any).__nbBridge = b; (globalThis as any).__activeChannel = c;
nb.registerHandler('getVideoinfo', (data) => { bridge.registerHandler('getVideoinfo', (data) => {
received = data; received = data;
return { ok: true }; return { ok: true };
}); });
@@ -67,33 +69,33 @@ test('registerHandler: H5 注册供原生调用', () => {
// 原生侧调用 // 原生侧调用
const handler = env.handlers.get('getVideoinfo'); const handler = env.handlers.get('getVideoinfo');
assert.ok(handler, 'handler 应已注册'); assert.ok(handler, 'handler 应已注册');
const ret = handler!({ vid: 'abc' }, () => {}); handler!({ vid: 'abc' }, (() => {}) as NativeResponseCallback);
assert.deepEqual(received, { vid: 'abc' }); assert.deepEqual(received, { vid: 'abc' });
env.restore(); env.restore();
}); });
test('callHandler: H5 调用原生,响应通过 cb 接收', () => { test('callHandler: H5 调用原生,响应通过 cb 接收', () => {
const env = makeEnv(); const env = makeEnv();
// onReady 必须把 bridge 存到 __nbBridge,callHandler 才能找到 const bridge = createBridge({
const nb = createNativeBridge({ onReady(c) { (globalThis as any).__activeChannel = c; },
onReady(b) { (globalThis as any).__nbBridge = b; },
}); });
env.triggerReady(); env.triggerReady();
let resp: unknown = null; 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'); assert.ok(cb, 'callHandler 应记录 cb');
cb!({ level: 80, charging: false }); cb!({ level: 80, charging: false });
assert.deepEqual(resp, { level: 80, charging: false }); assert.deepEqual(resp, { level: 80, charging: false });
env.restore(); env.restore();
}); });
test('callHandler: handler 名不在白名单 → 显式抛错(第二准则:不静默吞包)', () => { test('callHandler: handler 名不在白名单 → 显式抛错(第二准则)', () => {
const env = makeEnv(); const env = makeEnv();
const nb = createNativeBridge({ onReady: () => {} }); const bridge = createBridge({ onReady: () => {} });
env.triggerReady();
assert.throws( assert.throws(
() => nb.callHandler('unknown_handler' as any, {}, () => {}), () => bridge.callHandler('unknown_handler' as any, {}, () => {}),
/unknown_handler/, /unknown_handler/,
); );
env.restore(); env.restore();
@@ -101,42 +103,48 @@ test('callHandler: handler 名不在白名单 → 显式抛错(第二准则:不
test('registerHandler: handler 名不在白名单 → 显式抛错', () => { test('registerHandler: handler 名不在白名单 → 显式抛错', () => {
const env = makeEnv(); const env = makeEnv();
const nb = createNativeBridge({ onReady: () => {} }); const bridge = createBridge({ onReady: () => {} });
env.triggerReady();
assert.throws( assert.throws(
() => nb.registerHandler('unknown_handler' as any, () => undefined), () => bridge.registerHandler('unknown_handler' as any, () => undefined),
/unknown_handler/, /unknown_handler/,
); );
env.restore(); env.restore();
}); });
test('bridge 未就绪时 callHandler/registerHandler 显式抛错(不在初始化就调用)', () => { test('bridge 未就绪时 callHandler/registerHandler 显式抛错', () => {
// 不调用 onReady,bridge 永不触发
const g = globalThis as any; const g = globalThis as any;
const old = g.WVJBCallbacks; const old = g.WVJBCallbacks;
g.WVJBCallbacks = []; // 空队列,setup 永不触发 g.WVJBCallbacks = []; // 空队列,triggerReady 不被调用
const nb = createNativeBridge({ onReady: () => {} }); const bridge = createBridge({ onReady: () => {} });
assert.throws(() => nb.callHandler('getBattery', {}, () => {}), /not ready/i); assert.throws(() => bridge.callHandler('getBattery', {}, () => {}), /not ready/i);
assert.throws(() => nb.registerHandler('getVideoinfo', (() => ({})) as any), /not ready/i); assert.throws(() => bridge.registerHandler('getVideoinfo', (() => undefined) as any), /not ready/i);
g.WVJBCallbacks = old; g.WVJBCallbacks = old;
}); });
test('重复 createNativeBridge: 多个 instance 各自注册独立回调到 WVJBCallbacks', () => { test('重复 createBridge: 多个 instance 各自注册独立回调', () => {
const g = globalThis as any; const g = globalThis as any;
const old = g.WVJBCallbacks; const old = g.WVJBCallbacks;
g.WVJBCallbacks = []; g.WVJBCallbacks = [];
const nb1 = createNativeBridge({ onReady: () => {} }); createBridge({ onReady: () => {} });
const nb2 = createNativeBridge({ onReady: () => {} }); createBridge({ onReady: () => {} });
// 每个 createNativeBridge push 一个 cb(原生侧 bridgeReady 时全触发,业务侧通过闭包区分)
assert.equal(g.WVJBCallbacks.length, 2, '每个 instance 独立注册一个 cb'); assert.equal(g.WVJBCallbacks.length, 2, '每个 instance 独立注册一个 cb');
g.WVJBCallbacks = old; g.WVJBCallbacks = old;
}); });
test('window.settings: getSetting 同步取值', () => { test('getNativeSetting: 同步取 window.settings.getothername', () => {
const g = globalThis as any; const g = globalThis as any;
const old = g.window; const oldWindow = g.window;
g.window = { settings: { getothername: (n: string) => n === 'agentid' ? 'A' : '' } }; g.window = { settings: { getothername: (n: string) => n === 'agentid' ? 'A' : '' } };
// dynamic import to mutate after module load? 这里简单测: window injected already assert.equal(getNativeSetting('agentid'), 'A');
// 用 require 方式不可行,直接验证 window injection 模式 assert.equal(getNativeSetting('missing'), '');
assert.equal(g.window.settings.getothername('agentid'), 'A'); g.window = oldWindow;
g.window = old; });
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;
}); });