diff --git a/entry/src/main/ets/pages/BridgeGameContainer.ets b/entry/src/main/ets/pages/BridgeGameContainer.ets index 9478192..3a4b83d 100644 --- a/entry/src/main/ets/pages/BridgeGameContainer.ets +++ b/entry/src/main/ets/pages/BridgeGameContainer.ets @@ -1,42 +1,74 @@ -import { RouteName, BridgeGameParams, GenericWebParams, GenericWebResult } from '../routes/AppRoutes'; +import { webview } from '@kit.ArkWeb'; +import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge'; +import { BridgeGameParams } from '../routes/AppRoutes'; /** - * 大厅/子游戏容器壳(对应 webviewActivity,框架 §7.1)。 - * M0:仅路由壳,演示打开通用网页容器并接收 101 回传。 - * M2:填入 Web 组件 + BridgeController(桥协议 + §8/§9 handler)。 + * 大厅/子游戏容器(对应 webviewActivity,框架 §7.1)。 + * M1:挂 Web + 接桥(onControllerAttached 建桥 / onLoadIntercept 接桥 / onPageEnd 注桥), + * 加载最小回显 H5 验证 JsBridge 协议与 V1(yy:// 拦截)。 + * M2:入口 URL 改为大厅 file://.../gamehall/index.html;M3:注册全部能力 Provider。 */ @Component export struct BridgeGameContainer { pathStack: NavPathStack = new NavPathStack(); params: BridgeGameParams = { entryUrl: '', launchType: '0' }; - @State private lastWebResult: string = '(无)'; + private controller: webview.WebviewController = new webview.WebviewController(); + private adapter: WebviewControllerAdapter | undefined = undefined; + private bridge: BridgeController | undefined = undefined; - /** 打开通用网页容器(§11.2 路径 B),onPop 接收子页回传(结果码 101 语义)。 */ - private openGenericWeb(): void { - const params: GenericWebParams = { url: '', title: '活动页', data: 'hello', orientation: '0' }; - this.pathStack.pushPathByName(RouteName.GENERIC_WEB, params, (popInfo: PopInfo) => { - const result = popInfo.result as GenericWebResult; - this.lastWebResult = result !== undefined ? result.data : '(空)'; - }, false); + aboutToAppear(): void { + // 开发期开启远程调试;M5 加固时用 BuildProfile.DEBUG 守卫、release 必关(附录 B) + webview.WebviewController.setWebDebuggingAccess(true); + } + + aboutToDisappear(): void { + const a = this.adapter; + if (a !== undefined) { + a.dispose(); + } + } + + private setupBridge(): void { + const adapter = new WebviewControllerAdapter(this.controller); + const bridge = new BridgeController(adapter); + bridge.setBridgeJs(BridgeJsLoader.load(getContext(this))); + // 注册最小回显所需的原生 handler(M3 替换为全部能力 Provider) + bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => { + cb(Date.now().toString()); + }); + bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => { + cb(data); + }); + this.adapter = adapter; + this.bridge = bridge; } build() { NavDestination() { - Column({ space: 16 }) { - Text('BridgeGameContainer 壳') - .fontSize(20) - .fontWeight(FontWeight.Bold) - Text(`launchType=${this.params.launchType}`) - .fontSize(14) - .fontColor(Color.Gray) - Text(`通用网页回传:${this.lastWebResult}`) - .fontSize(14) - Button('打开通用网页(测回传)') - .onClick(() => this.openGenericWeb()) - } - .width('100%') - .height('100%') - .justifyContent(FlexAlign.Center) + Web({ src: $rawfile('test_echo.html'), controller: this.controller }) + .javaScriptAccess(true) + .domStorageAccess(true) + .fileAccess(true) + .mixedMode(MixedMode.All) + .cacheMode(CacheMode.None) + .geolocationAccess(true) + .zoomAccess(false) + .width('100%') + .height('100%') + .onControllerAttached(() => { + this.setupBridge(); + }) + .onLoadIntercept((event: OnLoadInterceptEvent) => { + const url: string = event.data.getRequestUrl(); + const b = this.bridge; + return b !== undefined ? b.onLoadIntercept(url) : false; + }) + .onPageEnd(() => { + const b = this.bridge; + if (b !== undefined) { + b.onPageEnd(); + } + }) } .hideTitleBar(true) } diff --git a/entry/src/main/resources/rawfile/test_echo.html b/entry/src/main/resources/rawfile/test_echo.html new file mode 100644 index 0000000..ab54f97 --- /dev/null +++ b/entry/src/main/resources/rawfile/test_echo.html @@ -0,0 +1,60 @@ + + + + + + + + +

JsBridge 最小回显测试(M1 / V1)

+
+ + + diff --git a/feature_bridge/src/main/ets/core/BridgeController.ets b/feature_bridge/src/main/ets/core/BridgeController.ets index 2eb893e..de4c2e9 100644 --- a/feature_bridge/src/main/ets/core/BridgeController.ets +++ b/feature_bridge/src/main/ets/core/BridgeController.ets @@ -164,8 +164,12 @@ export class BridgeController { } private dispatch(m: Message): void { - const json = MessageCodec.escape(MessageCodec.toJson(m)); - this.web.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`); + // lzyzsd 二次转义(复刻 \ 与 ");再补转义单引号——lzyzsd 原版遗漏,而出站脚本用 + // 单引号包裹 _handleMessageFromNative('...'),data 含 ' 会提前闭合字符串。补转义后 + // H5 JSON.parse 得到的仍是原始 data,对 H5 完全透明(边界行为不变)。 + const escaped = MessageCodec.escape(MessageCodec.toJson(m)); + const safe = escaped.replace(/'/g, "\\'"); + this.web.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${safe}');`); } // —— BridgeUtil 等价:从回执 URL 解析 functionName / data —— diff --git a/feature_bridge/src/main/ets/web/IWebController.ets b/feature_bridge/src/main/ets/web/IWebController.ets index 34ae812..a63678d 100644 --- a/feature_bridge/src/main/ets/web/IWebController.ets +++ b/feature_bridge/src/main/ets/web/IWebController.ets @@ -24,44 +24,42 @@ export interface IWebController { export class WebviewControllerAdapter implements IWebController { private static seq: number = 0; private readonly controller: webview.WebviewController; - private readonly uiEventId: string; + private readonly jsEventId: string; + private readonly urlEventId: string; constructor(controller: webview.WebviewController) { this.controller = controller; WebviewControllerAdapter.seq += 1; - this.uiEventId = `web.ui.runjs.${WebviewControllerAdapter.seq}`; - // 构造发生在 UI 线程(容器内)→ 订阅回调即在 UI 线程执行 - emitter.on(this.uiEventId, (ev: emitter.EventData) => { - const d = ev.data; - if (d === undefined) { - return; + this.jsEventId = `web.ui.js.${WebviewControllerAdapter.seq}`; + this.urlEventId = `web.ui.url.${WebviewControllerAdapter.seq}`; + // 构造发生在 UI 线程(容器内)→ 订阅回调即在 UI 线程执行(泛型 string payload,无 any) + emitter.on(this.jsEventId, (ev: emitter.GenericEventData) => { + const s = ev.data; + if (s !== undefined) { + this.controller.runJavaScript(s).then(() => { }).catch((_e: Object) => { }); } - const script = d['script']; - if (typeof script === 'string') { - this.controller.runJavaScript(script).then(() => { }).catch((_e: Object) => { }); - return; - } - const url = d['loadUrl']; - if (typeof url === 'string') { - this.controller.loadUrl(url); + }); + emitter.on(this.urlEventId, (ev: emitter.GenericEventData) => { + const u = ev.data; + if (u !== undefined) { + this.controller.loadUrl(u); } }); } runJavaScript(script: string): void { - const payload: Record = { 'script': script }; - const ev: emitter.EventData = { data: payload }; - emitter.emit(this.uiEventId, ev); + const ev: emitter.GenericEventData = { data: script }; + emitter.emit(this.jsEventId, ev); } loadUrl(url: string): void { - const payload: Record = { 'loadUrl': url }; - const ev: emitter.EventData = { data: payload }; - emitter.emit(this.uiEventId, ev); + const ev: emitter.GenericEventData = { data: url }; + emitter.emit(this.urlEventId, ev); } dispose(): void { - emitter.off(this.uiEventId); + emitter.off(this.jsEventId); + emitter.off(this.urlEventId); } }