From 3e2b1282ba686ddee4274ecf7a73844b46d86972 Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Thu, 25 Jun 2026 09:01:35 +0800 Subject: [PATCH] =?UTF-8?q?M1:=20=E5=AE=B9=E5=99=A8=E6=8E=A5=E6=A1=A5=20+?= =?UTF-8?q?=20V1=20=E9=98=BB=E5=A1=9E=E6=80=A7=E9=AA=8C=E8=AF=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=EF=BC=88T-M1-05/06/07=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BridgeGameContainer 挂 Web + 接桥:onControllerAttached 建桥/注册 handler、 onLoadIntercept 接桥、onPageEnd 注桥 JS + 补发启动队列、aboutToDisappear 释放 - test_echo.html 最小回显 H5(getTime 往返 + nativeEcho 特殊字符转义往返) - 修复:dispatch 出站补转义单引号——lzyzsd 原版遗漏,_handleMessageFromNative('...') 单引号包裹下 data 含 ' 会破坏字符串;补转义对 H5 透明(JSON.parse 仍得原始 data) - ⚠ V1 真机通过:yy:// 被 onLoadIntercept 捕获、_fetchQueue 经 yy://return 回传、 双向回执、含 "\/'中文 特殊字符转义往返一致。技术路线成立,无需回退 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/ets/pages/BridgeGameContainer.ets | 86 +++++++++++++------ .../src/main/resources/rawfile/test_echo.html | 60 +++++++++++++ .../src/main/ets/core/BridgeController.ets | 8 +- .../src/main/ets/web/IWebController.ets | 42 +++++---- 4 files changed, 145 insertions(+), 51 deletions(-) create mode 100644 entry/src/main/resources/rawfile/test_echo.html 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); } }