review: M0/M1 成果审查后的质量加固
三路独立审查:桥引擎(对照 lzyzsd 逐字)与契约 SSOT(对照原 Android 44 handler) 两个命门维度【零问题】;以下为架构/质量改进项的修复: - [安全红线] BridgeGameContainer setWebDebuggingAccess 加 AppEnv.isDebug() 守卫 (common 新增 AppEnv,用 bundleManager.appInfo.debug 运行时判断,release 自动关闭, 消除 CLAUDE.md 附录B 红线违反) - [生命周期] BridgeController 加 dispose() 清回执表/注册表/启动队列;HandlerRegistry 加 clear();容器 aboutToDisappear 先 dispose 桥再 dispose adapter 并置空,防残留命中/泄漏 - [事件总线] EventBus.on/once 返回取消函数支持 per-subscriber 精确退订;off→offAll 并 标注谨慎(避免 M3 多容器订阅 appservice 时误注销他人回调) - [单测] 加强 escape 断言覆盖内层 " 加倍(命门);新增 dispose 用例;16 用例全通过, 桥核心覆盖率 89.0% - [记录] Index.pageMap param as 运行期不校验,M4 外部 scheme 唤起时需补校验(已注释) 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
74b302e131
commit
bdb920f61e
@@ -6,3 +6,4 @@ export { BridgeTracer } from './src/main/ets/log/BridgeTracer';
|
|||||||
export { EventBus, EventPayload } from './src/main/ets/event/EventBus';
|
export { EventBus, EventPayload } from './src/main/ets/event/EventBus';
|
||||||
export { DIContainer, Factory } from './src/main/ets/di/DIContainer';
|
export { DIContainer, Factory } from './src/main/ets/di/DIContainer';
|
||||||
export { ErrorCenter, ErrorStrategy, ErrorSink } from './src/main/ets/error/ErrorCenter';
|
export { ErrorCenter, ErrorStrategy, ErrorSink } from './src/main/ets/error/ErrorCenter';
|
||||||
|
export { AppEnv } from './src/main/ets/env/AppEnv';
|
||||||
|
|||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 运行环境判断(生产加固,对应 CLAUDE.md 附录 B 红线)。
|
||||||
|
*
|
||||||
|
* isDebug():是否为 debug 包(debug 签名)。用于守卫仅 debug 可开的能力,
|
||||||
|
* 如 setWebDebuggingAccess——release 包 appInfo.debug 为 false,自动关闭远程调试。
|
||||||
|
* 等价 BuildProfile.DEBUG 的意图,但用运行时签名信息判断、不依赖构建期字段注入。
|
||||||
|
*/
|
||||||
|
import { bundleManager } from '@kit.AbilityKit';
|
||||||
|
|
||||||
|
export class AppEnv {
|
||||||
|
private static debugCache: boolean | undefined = undefined;
|
||||||
|
|
||||||
|
static isDebug(): boolean {
|
||||||
|
if (AppEnv.debugCache === undefined) {
|
||||||
|
try {
|
||||||
|
const info = bundleManager.getBundleInfoForSelfSync(
|
||||||
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||||
|
AppEnv.debugCache = info.appInfo.debug;
|
||||||
|
} catch (e) {
|
||||||
|
AppEnv.debugCache = false; // 取不到时保守按 release(不开调试)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AppEnv.debugCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,18 +11,27 @@ import { emitter } from '@kit.BasicServicesKit';
|
|||||||
export type EventPayload = Record<string, Object>;
|
export type EventPayload = Record<string, Object>;
|
||||||
|
|
||||||
export class EventBus {
|
export class EventBus {
|
||||||
/** 持续订阅。 */
|
/** 持续订阅。返回取消函数——调用它仅注销本次订阅(per-subscriber,不影响其他订阅者)。
|
||||||
static on(eventId: string, callback: (payload?: EventPayload) => void): void {
|
* 多容器/多订阅者共享同一 eventId(如 appservice 前后台)时务必用返回值退订,勿用 off(eventId)。 */
|
||||||
emitter.on(eventId, (ev: emitter.EventData) => {
|
static on(eventId: string, callback: (payload?: EventPayload) => void): () => void {
|
||||||
|
const wrapper = (ev: emitter.EventData) => {
|
||||||
callback(ev.data);
|
callback(ev.data);
|
||||||
});
|
};
|
||||||
|
emitter.on(eventId, wrapper);
|
||||||
|
return () => {
|
||||||
|
emitter.off(eventId, wrapper);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单次订阅,触发后自动取消。 */
|
/** 单次订阅,触发后自动取消。返回取消函数(触发前可主动退订)。 */
|
||||||
static once(eventId: string, callback: (payload?: EventPayload) => void): void {
|
static once(eventId: string, callback: (payload?: EventPayload) => void): () => void {
|
||||||
emitter.once(eventId, (ev: emitter.EventData) => {
|
const wrapper = (ev: emitter.EventData) => {
|
||||||
callback(ev.data);
|
callback(ev.data);
|
||||||
});
|
};
|
||||||
|
emitter.once(eventId, wrapper);
|
||||||
|
return () => {
|
||||||
|
emitter.off(eventId, wrapper);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发布事件。 */
|
/** 发布事件。 */
|
||||||
@@ -31,8 +40,8 @@ export class EventBus {
|
|||||||
emitter.emit(eventId, data);
|
emitter.emit(eventId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消该事件的全部订阅。 */
|
/** ⚠ 取消该事件的【全部】订阅(粗粒度)。多订阅者场景请改用 on() 返回的取消函数。 */
|
||||||
static off(eventId: string): void {
|
static offAll(eventId: string): void {
|
||||||
emitter.off(eventId);
|
emitter.off(eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { webview } from '@kit.ArkWeb';
|
import { webview } from '@kit.ArkWeb';
|
||||||
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
||||||
|
import { AppEnv } from 'common';
|
||||||
import { BridgeGameParams } from '../routes/AppRoutes';
|
import { BridgeGameParams } from '../routes/AppRoutes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,15 +18,24 @@ export struct BridgeGameContainer {
|
|||||||
private bridge: BridgeController | undefined = undefined;
|
private bridge: BridgeController | undefined = undefined;
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
// 开发期开启远程调试;M5 加固时用 BuildProfile.DEBUG 守卫、release 必关(附录 B)
|
// 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线)
|
||||||
webview.WebviewController.setWebDebuggingAccess(true);
|
if (AppEnv.isDebug()) {
|
||||||
|
webview.WebviewController.setWebDebuggingAccess(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToDisappear(): void {
|
aboutToDisappear(): void {
|
||||||
|
// 释放:先清桥(回执表/注册表/启动队列),再断 UI 线程通道,最后置空防残留命中
|
||||||
|
const b = this.bridge;
|
||||||
|
if (b !== undefined) {
|
||||||
|
b.dispose();
|
||||||
|
}
|
||||||
const a = this.adapter;
|
const a = this.adapter;
|
||||||
if (a !== undefined) {
|
if (a !== undefined) {
|
||||||
a.dispose();
|
a.dispose();
|
||||||
}
|
}
|
||||||
|
this.bridge = undefined;
|
||||||
|
this.adapter = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupBridge(): void {
|
private setupBridge(): void {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct Index {
|
|||||||
this.pathStack.pushPathByName(RouteName.SPLASH, '', false);
|
this.pathStack.pushPathByName(RouteName.SPLASH, '', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注:param as T 为编译期断言、运行期不校验。本期由 Splash 固定传对象,安全;
|
||||||
|
// M4 接入外部 scheme 唤起(gamepaywelcome 等深链)时,需在此对 param 做运行期类型校验后再下发。
|
||||||
@Builder
|
@Builder
|
||||||
pageMap(name: string, param: object) {
|
pageMap(name: string, param: object) {
|
||||||
if (name === RouteName.SPLASH) {
|
if (name === RouteName.SPLASH) {
|
||||||
|
|||||||
@@ -34,12 +34,17 @@ export default function bridgeTest() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('escape_quotes_backslash', 0, () => {
|
it('escape_quotes_backslash', 0, () => {
|
||||||
// JSON {"data":"a\"b"} → 结构引号转义、内部 \" 加倍
|
// data='a"b' → toJson 得 {"data":"a\"b"}(内部引号被 JSON 转义为 \")
|
||||||
const mm = new Message();
|
const mm = new Message();
|
||||||
mm.data = 'a"b';
|
mm.data = 'a"b';
|
||||||
const escaped = MessageCodec.escape(MessageCodec.toJson(mm));
|
const json = MessageCodec.toJson(mm);
|
||||||
// 结构引号被转义为 \"(前面非反斜杠)
|
// toJson 内部:a 与 b 之间是 反斜杠+引号
|
||||||
|
expect(json.includes('a\\"b')).assertTrue();
|
||||||
|
const escaped = MessageCodec.escape(json);
|
||||||
|
// 第二步:结构引号(前面非反斜杠)被转义为 \"
|
||||||
expect(escaped.includes('\\"data\\"')).assertTrue();
|
expect(escaped.includes('\\"data\\"')).assertTrue();
|
||||||
|
// 第一步(命门):内部已转义的 \" 的反斜杠被加倍 → a 与 b 之间出现 \\\"(反斜杠×2+引号)
|
||||||
|
expect(escaped.includes('a\\\\\\"b')).assertTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toArray_parses_queue', 0, () => {
|
it('toArray_parses_queue', 0, () => {
|
||||||
@@ -173,6 +178,16 @@ export default function bridgeTest() {
|
|||||||
expect(resp).assertEqual('1700');
|
expect(resp).assertEqual('1700');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dispose_clears_registry_and_callbacks', 0, () => {
|
||||||
|
const fake = new FakeWebController();
|
||||||
|
const bridge = new BridgeController(fake);
|
||||||
|
bridge.registerHandler('a', (_d, cb) => cb('A'));
|
||||||
|
expect(bridge.getRegistry().has('a')).assertTrue();
|
||||||
|
bridge.dispose();
|
||||||
|
// dispose 清空注册表(默认兜底保留)
|
||||||
|
expect(bridge.getRegistry().has('a')).assertFalse();
|
||||||
|
});
|
||||||
|
|
||||||
it('default_handler_unregistered_no_throw', 0, () => {
|
it('default_handler_unregistered_no_throw', 0, () => {
|
||||||
const fake = new FakeWebController();
|
const fake = new FakeWebController();
|
||||||
const bridge = new BridgeController(fake);
|
const bridge = new BridgeController(fake);
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ export class BridgeController {
|
|||||||
this.bridgeJs = js;
|
this.bridgeJs = js;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 释放:清空回执表/注册表/启动队列,断开与已注册能力闭包的引用,防泄漏。
|
||||||
|
* 容器 aboutToDisappear 调用(注意:IWebController 由容器另行 dispose)。 */
|
||||||
|
dispose(): void {
|
||||||
|
this.responseCallbacks.clear();
|
||||||
|
this.registry.clear();
|
||||||
|
this.startupMessages = null;
|
||||||
|
}
|
||||||
|
|
||||||
// —— H5 → 原生:在 Web().onLoadIntercept 调用,返回 true 表示拦截、阻断真实导航 ——
|
// —— H5 → 原生:在 Web().onLoadIntercept 调用,返回 true 表示拦截、阻断真实导航 ——
|
||||||
onLoadIntercept(rawUrl: string): boolean {
|
onLoadIntercept(rawUrl: string): boolean {
|
||||||
let url: string;
|
let url: string;
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ export class HandlerRegistry {
|
|||||||
this.handlers.delete(name);
|
this.handlers.delete(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 清空全部已注册 handler(容器销毁时防泄漏;默认兜底 handler 保留)。 */
|
||||||
|
clear(): void {
|
||||||
|
this.handlers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
has(name: string): boolean {
|
has(name: string): boolean {
|
||||||
return this.handlers.has(name);
|
return this.handlers.has(name);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user