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,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<string, (data: any) => void> = new Map();
const handlers: Map<string, WVJBHandler> = new Map();
const handlers: Map<string, NativeHandler> = 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;
});