M1: 容器接桥 + V1 阻塞性验证通过(T-M1-05/06/07)
- 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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
206db9730a
commit
3e2b1282ba
@@ -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())
|
||||
}
|
||||
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%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 24px; font-size: 20px; }
|
||||
h3 { color: #0a59f7; }
|
||||
.ok { color: #15803d; }
|
||||
.err { color: #dc2626; }
|
||||
#log p { margin: 6px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>JsBridge 最小回显测试(M1 / V1)</h3>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
function log(s, cls) {
|
||||
var p = document.createElement('p');
|
||||
if (cls) { p.className = cls; }
|
||||
p.textContent = s;
|
||||
document.getElementById('log').appendChild(p);
|
||||
}
|
||||
function connect(cb) {
|
||||
if (window.WebViewJavascriptBridge) {
|
||||
cb(window.WebViewJavascriptBridge);
|
||||
} else {
|
||||
document.addEventListener('WebViewJavascriptBridgeReady', function () {
|
||||
cb(window.WebViewJavascriptBridge);
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
log('页面加载完成,等待桥就绪…');
|
||||
connect(function (bridge) {
|
||||
log('✓ 桥已就绪 WebViewJavascriptBridge ready', 'ok');
|
||||
|
||||
// H5 注册 handler,供原生调用(测出站 原生→H5)
|
||||
bridge.registerHandler('echo', function (data, responseCallback) {
|
||||
log('收到 原生→H5 echo: ' + data, 'ok');
|
||||
responseCallback('H5 收到并回执: ' + data);
|
||||
});
|
||||
|
||||
// H5 调原生 getTime(测 H5→原生→回执,即 V1 全链路)
|
||||
bridge.callHandler('getTime', '', function (resp) {
|
||||
log('✓ getTime 回执: ' + resp, 'ok');
|
||||
});
|
||||
|
||||
// H5 调原生 nativeEcho,带特殊字符(测 MessageCodec 转义往返)
|
||||
var tricky = 'a"b\\c/中文\'x';
|
||||
bridge.callHandler('nativeEcho', tricky, function (resp) {
|
||||
if (resp === tricky) {
|
||||
log('✓ nativeEcho 转义往返一致: ' + resp, 'ok');
|
||||
} else {
|
||||
log('✗ nativeEcho 不一致! 发=' + tricky + ' 收=' + resp, 'err');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 ——
|
||||
|
||||
@@ -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<string>(this.jsEventId, (ev: emitter.GenericEventData<string>) => {
|
||||
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<string>(this.urlEventId, (ev: emitter.GenericEventData<string>) => {
|
||||
const u = ev.data;
|
||||
if (u !== undefined) {
|
||||
this.controller.loadUrl(u);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runJavaScript(script: string): void {
|
||||
const payload: Record<string, Object> = { 'script': script };
|
||||
const ev: emitter.EventData = { data: payload };
|
||||
emitter.emit(this.uiEventId, ev);
|
||||
const ev: emitter.GenericEventData<string> = { data: script };
|
||||
emitter.emit<string>(this.jsEventId, ev);
|
||||
}
|
||||
|
||||
loadUrl(url: string): void {
|
||||
const payload: Record<string, Object> = { 'loadUrl': url };
|
||||
const ev: emitter.EventData = { data: payload };
|
||||
emitter.emit(this.uiEventId, ev);
|
||||
const ev: emitter.GenericEventData<string> = { data: url };
|
||||
emitter.emit<string>(this.urlEventId, ev);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
emitter.off(this.uiEventId);
|
||||
emitter.off(this.jsEventId);
|
||||
emitter.off(this.urlEventId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user