refactor(native): isolate exact WVJB compatibility adapter
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/** External native handler names registered by the legacy H5 client. */
|
||||
export const REGISTERED_NATIVE_HANDLER_NAMES = [
|
||||
'getVideoinfo',
|
||||
'sharelogin',
|
||||
'gameui_play_voice',
|
||||
'gameui_stop_voice',
|
||||
'sharesuccess',
|
||||
'getphoneinfo',
|
||||
'getAddressBook',
|
||||
'phonestate',
|
||||
'appservice',
|
||||
'getaudiourl',
|
||||
'getBattery',
|
||||
'getwifiLevel',
|
||||
'getnetwork',
|
||||
'shakeEnd',
|
||||
'yPaytype',
|
||||
'getlocationinfo',
|
||||
'getWebdata',
|
||||
'PayuserPaytypePaystate',
|
||||
'setPostUrl',
|
||||
'getphoto',
|
||||
] as const;
|
||||
|
||||
/** External native methods called by the legacy H5 client. */
|
||||
export const NATIVE_CALL_NAMES = [
|
||||
'getphoto',
|
||||
'h5Webpay',
|
||||
'getVideoinfo',
|
||||
'getphoneInfo',
|
||||
'getAddressBook',
|
||||
'createRoom',
|
||||
'exitRoom',
|
||||
'accreditlogin',
|
||||
'cancellogin',
|
||||
'OpenurlTitleData',
|
||||
'getOther',
|
||||
'voicePlaying',
|
||||
'gameCopytext',
|
||||
'opencamera',
|
||||
'gamepastetext',
|
||||
'getGameinstall',
|
||||
'getGameplay',
|
||||
'getphonestate',
|
||||
'ypayType',
|
||||
'SwitchOverGameData',
|
||||
'backgameData',
|
||||
'startlocation',
|
||||
'getlocationinfo',
|
||||
'srcIsloop',
|
||||
'opensaoma',
|
||||
'startshake',
|
||||
'stopshake',
|
||||
'SwitchShake',
|
||||
'sharephotourl',
|
||||
'browser',
|
||||
'mediaTypeAudio',
|
||||
'prepareaudio',
|
||||
'vibrator',
|
||||
'repeatvibrator',
|
||||
'canclevibrator',
|
||||
'orientation',
|
||||
'notification',
|
||||
'friendsSharetypeUrlToptitleDescript',
|
||||
] as const;
|
||||
|
||||
export type RegisteredNativeHandlerName = (typeof REGISTERED_NATIVE_HANDLER_NAMES)[number];
|
||||
export type NativeCallName = (typeof NATIVE_CALL_NAMES)[number];
|
||||
export type NativeSettingName = 'agent' | 'gameconfig' | 'gameserver' | 'servertype';
|
||||
|
||||
/** Handler arguments remain opaque until a consuming feature defines its contract. */
|
||||
export type NativeHandler = (...args: unknown[]) => unknown;
|
||||
export type NativeResponseCallback = (...args: unknown[]) => void;
|
||||
|
||||
export interface NativeBridgeChannel {
|
||||
registerHandler(name: string, handler: NativeHandler): void;
|
||||
callHandler(name: string, data: unknown, callback?: NativeResponseCallback): void;
|
||||
}
|
||||
|
||||
export interface NativeSettingsPort {
|
||||
getOtherName(name: NativeSettingName): string | number;
|
||||
getChannelName(): string | number;
|
||||
getMarketName(): string | number;
|
||||
}
|
||||
|
||||
export interface NativeBridgePort {
|
||||
isReady(): boolean;
|
||||
register(name: RegisteredNativeHandlerName, handler: NativeHandler): void;
|
||||
call(name: NativeCallName, data: unknown, callback?: NativeResponseCallback): void;
|
||||
dispose(): void;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import type { NativeSettingName, NativeSettingsPort } from './native-contracts.ts';
|
||||
|
||||
export interface NativeSettingsChannel {
|
||||
getothername(name: NativeSettingName): string | number;
|
||||
getchannelName(): string | number;
|
||||
getmarketname(): string | number;
|
||||
}
|
||||
|
||||
/** Exact-name adapter over the object injected at window.settings. */
|
||||
export class NativeSettingsAdapter implements NativeSettingsPort {
|
||||
constructor(private readonly settings: NativeSettingsChannel) {}
|
||||
|
||||
getOtherName(name: NativeSettingName): string | number {
|
||||
return this.settings.getothername(name);
|
||||
}
|
||||
|
||||
getChannelName(): string | number {
|
||||
return this.settings.getchannelName();
|
||||
}
|
||||
|
||||
getMarketName(): string | number {
|
||||
return this.settings.getmarketname();
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
NATIVE_CALL_NAMES,
|
||||
REGISTERED_NATIVE_HANDLER_NAMES,
|
||||
type NativeBridgeChannel,
|
||||
type NativeBridgePort,
|
||||
type NativeCallName,
|
||||
type NativeHandler,
|
||||
type NativeResponseCallback,
|
||||
type RegisteredNativeHandlerName,
|
||||
} from './native-contracts.ts';
|
||||
|
||||
export interface BridgeLoaderElement {
|
||||
readonly tagName: string;
|
||||
readonly style: { display: string };
|
||||
src: string;
|
||||
}
|
||||
|
||||
export interface WebViewJavascriptBridgeEnvironment {
|
||||
WebViewJavascriptBridge?: NativeBridgeChannel;
|
||||
WVJBCallbacks?: Array<(bridge: NativeBridgeChannel) => void>;
|
||||
readonly document: {
|
||||
addEventListener(name: string, listener: () => void, capture: boolean): void;
|
||||
removeEventListener(name: string, listener: () => void, capture: boolean): void;
|
||||
createElement(tagName: 'iframe'): BridgeLoaderElement;
|
||||
readonly documentElement: {
|
||||
appendChild(element: BridgeLoaderElement): void;
|
||||
removeChild(element: BridgeLoaderElement): void;
|
||||
};
|
||||
};
|
||||
setTimeout(callback: () => void, delay: 0): unknown;
|
||||
}
|
||||
|
||||
export type DisposeBridgeSetup = () => void;
|
||||
|
||||
/**
|
||||
* Reproduces the legacy WVJB bootstrap ordering, including continuing after
|
||||
* an immediate bridge callback into the callback-queue/iframe block.
|
||||
*/
|
||||
export function setupWebViewJavascriptBridge(
|
||||
environment: WebViewJavascriptBridgeEnvironment,
|
||||
callback: (bridge: NativeBridgeChannel) => void,
|
||||
): DisposeBridgeSetup {
|
||||
let readyListener: (() => void) | undefined;
|
||||
|
||||
if (environment.WebViewJavascriptBridge) {
|
||||
callback(environment.WebViewJavascriptBridge);
|
||||
} else {
|
||||
readyListener = () => callback(environment.WebViewJavascriptBridge as NativeBridgeChannel);
|
||||
environment.document.addEventListener(
|
||||
'WebViewJavascriptBridgeReady',
|
||||
readyListener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (environment.WVJBCallbacks) {
|
||||
environment.WVJBCallbacks.push(callback);
|
||||
} else {
|
||||
environment.WVJBCallbacks = [callback];
|
||||
const loader = environment.document.createElement('iframe');
|
||||
loader.style.display = 'none';
|
||||
loader.src = 'wvjbscheme://__BRIDGE_LOADED__';
|
||||
environment.document.documentElement.appendChild(loader);
|
||||
environment.setTimeout(() => environment.document.documentElement.removeChild(loader), 0);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (readyListener) {
|
||||
environment.document.removeEventListener(
|
||||
'WebViewJavascriptBridgeReady',
|
||||
readyListener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
const callbackIndex = environment.WVJBCallbacks?.indexOf(callback) ?? -1;
|
||||
if (callbackIndex >= 0) environment.WVJBCallbacks?.splice(callbackIndex, 1);
|
||||
};
|
||||
}
|
||||
|
||||
function includesName<const Name extends string>(names: readonly Name[], name: string): name is Name {
|
||||
return (names as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
/** Per-instance facade over one WVJB channel. */
|
||||
export class WebViewJavascriptBridgeAdapter implements NativeBridgePort {
|
||||
private channel: NativeBridgeChannel | undefined;
|
||||
private readonly handlers = new Map<RegisteredNativeHandlerName, NativeHandler>();
|
||||
private disposed = false;
|
||||
private readonly disposeSetup: DisposeBridgeSetup;
|
||||
|
||||
constructor(environment: WebViewJavascriptBridgeEnvironment) {
|
||||
this.disposeSetup = setupWebViewJavascriptBridge(environment, channel => {
|
||||
if (this.disposed || this.channel) return;
|
||||
this.channel = channel;
|
||||
for (const [name, handler] of this.handlers) channel.registerHandler(name, handler);
|
||||
});
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
this.assertUsable();
|
||||
return this.channel !== undefined;
|
||||
}
|
||||
|
||||
register(name: RegisteredNativeHandlerName, handler: NativeHandler): void {
|
||||
this.assertUsable();
|
||||
if (!includesName(REGISTERED_NATIVE_HANDLER_NAMES, name)) {
|
||||
throw new Error(`Unknown registered native handler: ${String(name)}`);
|
||||
}
|
||||
this.handlers.set(name, handler);
|
||||
this.channel?.registerHandler(name, handler);
|
||||
}
|
||||
|
||||
call(name: NativeCallName, data: unknown, callback?: NativeResponseCallback): void {
|
||||
this.assertUsable();
|
||||
if (!includesName(NATIVE_CALL_NAMES, name)) {
|
||||
throw new Error(`Unknown native call: ${String(name)}`);
|
||||
}
|
||||
if (!this.channel) throw new Error('Native bridge is not ready');
|
||||
this.channel.callHandler(name, data, callback);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return;
|
||||
this.disposeSetup();
|
||||
this.handlers.clear();
|
||||
this.channel = undefined;
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
private assertUsable(): void {
|
||||
if (this.disposed) throw new Error('Native bridge adapter is disposed');
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* 原生↔H5 异步桥(WebViewJavascriptBridge,WVJB)+ window.settings 同步取值。
|
||||
*
|
||||
* 严格对齐原 gameabc 项目(05_Func.js:2627 起)的外部契约:
|
||||
* - 14 个 handler 名白名单(与原生 app 逐字对齐)
|
||||
* - `window.WVJBCallbacks` / `WebViewJavascriptBridgeReady` 事件名
|
||||
* - `window.settings.getothername(name)` 同步取值 API
|
||||
* - 回调签名 `(data, responseCallback)` 与数据结构
|
||||
*
|
||||
* 内部命名(接口/函数/常量)走现代专业风格,与上述外部契约清晰隔离。
|
||||
*
|
||||
* 架构边界:本模块只暴露受限 facade,不允许 framework 业务代码直接读
|
||||
* window/bridge 内部。
|
||||
*/
|
||||
|
||||
/** window 上的原生注入对象(05_Func.js:2467-2471 同步取值)。 */
|
||||
declare global {
|
||||
interface Window {
|
||||
settings?: {
|
||||
/** 同步取一个原生配置项,无则返回空串 */
|
||||
getothername(name: string): string;
|
||||
};
|
||||
/** WVJB 启动前的全局回调队列(外部契约,不能改名) */
|
||||
WVJBCallbacks?: Array<(bridge: NativeBridgeChannel) => void>;
|
||||
/** WebView 注入 WVJB 的事件名(外部契约) */
|
||||
WebViewJavascriptBridgeReady?: Array<() => void>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生桥通道契约 —— WVJB bridge 实例的最小子集。
|
||||
* 内部命名为 "Channel" 以避免缩写、明确"双向通信通道"语义。
|
||||
*/
|
||||
export interface NativeBridgeChannel {
|
||||
/** H5 注册供原生侧调用的 handler */
|
||||
registerHandler(name: string, handler: NativeHandler): void;
|
||||
/** H5 主动调用原生侧 */
|
||||
callHandler(name: string, data: unknown, responseCallback?: (resp: unknown) => void): void;
|
||||
}
|
||||
|
||||
/** H5 注册供原生调用的 handler 函数签名(外部 WVJB 协议约定) */
|
||||
export type NativeHandler = (data: unknown, responseCallback: NativeResponseCallback) => unknown;
|
||||
|
||||
/** 响应回调:原生侧通过此把结果回传给 H5 */
|
||||
export type NativeResponseCallback = (responseData: unknown) => void;
|
||||
|
||||
/**
|
||||
* 14 个已注册 handler 名白名单(05_Func.js:2651+ 原项目实装)。
|
||||
* 与原生 app 端逐字对齐 —— app 按这些名字调,改名 app 也要改。
|
||||
*
|
||||
* 字面量联合类型:registerHandler/callHandler 入参必须在此集合内,
|
||||
* 拼写错误编译期就发现,集合外名字运行时显式抛错(第二准则:不静默兜底)。
|
||||
*/
|
||||
export const NATIVE_HANDLER_NAMES = [
|
||||
'getVideoinfo',
|
||||
'sharelogin',
|
||||
'sharesuccess',
|
||||
'gameui_play_voice',
|
||||
'gameui_stop_voice',
|
||||
'getphoneinfo',
|
||||
'getAddressBook',
|
||||
'phonestate',
|
||||
'appservice',
|
||||
'getaudiourl',
|
||||
'getBattery',
|
||||
'getwifiLevel',
|
||||
'getnetwork',
|
||||
'shakeEnd',
|
||||
] as const;
|
||||
|
||||
export type NativeHandlerName = (typeof NATIVE_HANDLER_NAMES)[number];
|
||||
|
||||
function isNativeHandlerName(name: string): name is NativeHandlerName {
|
||||
return (NATIVE_HANDLER_NAMES as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
/** createBridge 初始化选项 */
|
||||
export interface BridgeOptions {
|
||||
/** 通道就绪回调(原生侧 WebView bridgeReady 时触发一次) */
|
||||
onReady(channel: NativeBridgeChannel): void;
|
||||
/**
|
||||
* 初始化超时的兜底(可省略)。
|
||||
* 原生侧可能永不触发 WebViewJavascriptBridgeReady,业务可在此降级。
|
||||
*/
|
||||
onTimeout?(reason: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受限 Bridge facade —— 框架业务访问原生能力的唯一入口。
|
||||
*
|
||||
* 创建后立即尝试初始化:
|
||||
* 1. 把 setup 回调 push 到 window.WVJBCallbacks(原生侧约定的入口)
|
||||
* 2. 原生侧 WebView bridgeReady 时遍历 WVJBCallbacks 调用每个 cb → onReady 被触发
|
||||
*
|
||||
* 一旦通道就绪,onReady 被调用一次。
|
||||
*/
|
||||
export interface Bridge {
|
||||
/** 当前通道是否已就绪 */
|
||||
isReady(): boolean;
|
||||
/** H5 注册供原生侧调用(白名单校验) */
|
||||
registerHandler(name: NativeHandlerName, handler: NativeHandler): void;
|
||||
/** H5 调用原生侧(白名单校验) */
|
||||
callHandler(name: NativeHandlerName, data: unknown, cb?: NativeResponseCallback): void;
|
||||
}
|
||||
|
||||
export function createBridge(opts: BridgeOptions): Bridge {
|
||||
const g = globalThis as any;
|
||||
// 外部契约:把 setup cb push 到 window.WVJBCallbacks
|
||||
if (!g.WVJBCallbacks) g.WVJBCallbacks = [];
|
||||
let ready = false;
|
||||
g.WVJBCallbacks.push((channel: NativeBridgeChannel) => {
|
||||
ready = true;
|
||||
// 缓存通道供 callHandler 委托(避免 onReady 闭包外传)
|
||||
g.__activeChannel = channel;
|
||||
opts.onReady(channel);
|
||||
});
|
||||
|
||||
return {
|
||||
isReady() { return ready; },
|
||||
registerHandler(name, handler) {
|
||||
if (!isNativeHandlerName(name)) {
|
||||
throw new Error(`registerHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`);
|
||||
}
|
||||
if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调');
|
||||
g.__activeChannel.registerHandler(name, handler);
|
||||
},
|
||||
callHandler(name, data, cb) {
|
||||
if (!isNativeHandlerName(name)) {
|
||||
throw new Error(`callHandler: handler "${name}" 不在白名单(第二准则:不静默兜底)`);
|
||||
}
|
||||
if (!ready) throw new Error('NativeBridge not ready: 通道未初始化,请先等 onReady 回调');
|
||||
g.__activeChannel.callHandler(name, data, cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 window 取原生注入的配置项(05_Func.js:2467-2471 的 getothername)。
|
||||
*
|
||||
* @param name 配置项名(渠道/包信息/gameserver 覆盖等)
|
||||
* @returns 原生侧返回值;缺失时返回空串(对齐原项目:无则空字符串而非 undefined)
|
||||
* @throws 原生侧未注入 window.settings 时显式抛错(不静默返回空串——避免下游误用)
|
||||
*/
|
||||
export function getNativeSetting(name: string): string {
|
||||
const settings = (globalThis as any).window?.settings;
|
||||
if (!settings || typeof settings.getothername !== 'function') {
|
||||
throw new Error(
|
||||
`getNativeSetting("${name}"): window.settings 未注入(原生侧未加载或非 WebView 环境)`,
|
||||
);
|
||||
}
|
||||
return settings.getothername(name);
|
||||
}
|
||||
Reference in New Issue
Block a user