review: M3+M4 成果审查后的质量加固
八角度审查 15 Provider+微信/七牛+双容器+导航。桥核心/契约骨架基本无误;修确凿问题:
正确性/健壮性
- ShakeProvider:stop()/onDestroy 取消未决 shakeEnd 计时并复位 pending(修停止后仍触发出站、重启后首摇被吞、计时器泄漏)
- VibrateProvider:startRepeat 先 stopRepeat(修重复 repeatvibrator('1') 叠加出无法取消的孤儿计时链)
- PhotoProvider:JSON.parse 后 Array.isArray 守卫(修 H5 传非数组致 .map 抛错、出站永不下发使 H5 挂死);去不可达 .catch
- GenericWebContainer:getWebdata 直调改用 JSON.stringify 生成 JS 字符串实参(修 data 含换行/行分隔符断行致注入语法错误)
- LocationProvider:单次请求前先停连续监听(修连续后切单次仍持续推送);去 push() 中 !continuousOn→stopContinuous 死代码
加固/清理
- BridgeGameContainer:抽 firstReadyPush() 幂等助手(appservice/setPostUrl/pendingWebdata);保持 100% 触发时序
(注明为何用 100% 而非更早 onPageEnd:确保 H5 已注册 handler);aboutToDisappear 置空 hostCtx/config/resource
研判驳回(与原 Android 一致/契约默认/自愈):getphonestate CallState 值映射、通用容器 orientation 默认横屏、setMuted prepared 自愈、exitDialog backgameData() 无参通知。
延期(随微信集成):WeChatApi 单例陈旧回调/instanceof(容器重建后回调打旧 bridge;微信运行期本就待开放平台注册)。
记录待重构:AbstractProvider 基类 + parseJson 助手(去 15 Provider 重复 bridge/ctx/解析样板)、AVPlayer 复用、openBrowser/orientation 抽公共。
devecocli build 通过;单测通过;模拟器复测桥往返/appservice/setPostUrl 无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
@@ -53,6 +53,27 @@ export struct BridgeGameContainer {
|
|||||||
this.registrar = undefined;
|
this.registrar = undefined;
|
||||||
this.bridge = undefined;
|
this.bridge = undefined;
|
||||||
this.adapter = undefined;
|
this.adapter = undefined;
|
||||||
|
this.hostCtx = undefined;
|
||||||
|
this.config = undefined;
|
||||||
|
this.resource = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面进度首次 100% 时下发 appservice/setPostUrl(+子游戏返回大厅待发的 getWebdata)。幂等。
|
||||||
|
* firstProgressDone 在子游戏切换/返回大厅时复位,使重载后再次下发。 */
|
||||||
|
private firstReadyPush(): void {
|
||||||
|
if (this.firstProgressDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.firstProgressDone = true;
|
||||||
|
const b = this.bridge;
|
||||||
|
if (b !== undefined) {
|
||||||
|
b.callHandler(OutboundHandlers.AppService, '1');
|
||||||
|
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
|
||||||
|
if (this.pendingWebdata !== undefined) {
|
||||||
|
b.callHandler(OutboundHandlers.GetWebData, this.pendingWebdata);
|
||||||
|
this.pendingWebdata = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1)。 */
|
/** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1)。 */
|
||||||
@@ -197,19 +218,11 @@ export struct BridgeGameContainer {
|
|||||||
this.bridge?.onPageEnd();
|
this.bridge?.onPageEnd();
|
||||||
})
|
})
|
||||||
.onProgressChange((event: OnProgressChangeEvent) => {
|
.onProgressChange((event: OnProgressChangeEvent) => {
|
||||||
if (event.newProgress === 100 && !this.firstProgressDone) {
|
// 首次/每次切换后页面到 100% → 推 appservice(前台)+setPostUrl(对齐 Android onProgressChanged==100)。
|
||||||
// 首次/每次切换后页面到 100% → 推 appservice(前台) + setPostUrl(对齐 Android onProgressChanged==100)
|
// 用 100%(而非更早的 onPageEnd):确保 H5 在 WebViewJavascriptBridgeReady 里已注册 appservice/setPostUrl
|
||||||
this.firstProgressDone = true;
|
// 等 handler,避免出站早于注册被丢。loadUrl 切子游戏/返回大厅会再次产生 100% 回调(已复位 firstProgressDone)。
|
||||||
const b = this.bridge;
|
if (event.newProgress === 100) {
|
||||||
if (b !== undefined) {
|
this.firstReadyPush();
|
||||||
b.callHandler(OutboundHandlers.AppService, '1');
|
|
||||||
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
|
|
||||||
}
|
|
||||||
// 子游戏返回大厅的回传数据,待大厅重载完成后下发 getWebdata
|
|
||||||
if (this.pendingWebdata !== undefined) {
|
|
||||||
b?.callHandler(OutboundHandlers.GetWebData, this.pendingWebdata);
|
|
||||||
this.pendingWebdata = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export struct GenericWebContainer {
|
|||||||
.catch((e: BusinessError) => Logger.tag('GenericWebContainer').w(`orientation failed: ${e.message}`));
|
.catch((e: BusinessError) => Logger.tag('GenericWebContainer').w(`orientation failed: ${e.message}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** JS 字符串字面量转义(用于 runJavaScript 直调)。 */
|
/** 生成安全的 JS 字符串实参字面量:JSON.stringify 完整转义引号/反斜杠/换行/行分隔符。 */
|
||||||
private esc(s: string): string {
|
private jsArg(s: string): string {
|
||||||
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
return JSON.stringify(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
@@ -87,7 +87,7 @@ export struct GenericWebContainer {
|
|||||||
// 首次 100% → 原生直调 getWebdata('<data>')(§11.3)
|
// 首次 100% → 原生直调 getWebdata('<data>')(§11.3)
|
||||||
if (event.newProgress === 100 && !this.loaded) {
|
if (event.newProgress === 100 && !this.loaded) {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.controller.runJavaScript(`getWebdata('${this.esc(this.params.data)}')`);
|
this.controller.runJavaScript(`getWebdata(${this.jsArg(this.params.data)})`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export class LocationProvider implements CapabilityProvider {
|
|||||||
if (continuous) {
|
if (continuous) {
|
||||||
this.startContinuous();
|
this.startContinuous();
|
||||||
} else {
|
} else {
|
||||||
|
// 单次请求前,若已有连续监听在跑则先停,避免单次后仍持续推送(H5 预期一次)
|
||||||
|
this.stopContinuous();
|
||||||
this.requestSingle();
|
this.requestSingle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,9 +165,7 @@ export class LocationProvider implements CapabilityProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private push(info: MaplocationInfo): void {
|
private push(info: MaplocationInfo): void {
|
||||||
|
// 单次定位用 getCurrentLocation(一次性,无监听可停);连续定位由 stopshake 等价的 stopContinuous 管理。
|
||||||
this.bridge?.callHandler(OutboundHandlers.GetLocationInfo, JSON.stringify(info));
|
this.bridge?.callHandler(OutboundHandlers.GetLocationInfo, JSON.stringify(info));
|
||||||
if (!this.continuousOn) {
|
|
||||||
this.stopContinuous();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,19 +31,25 @@ export class PhotoProvider implements CapabilityProvider {
|
|||||||
if (ctx === undefined) {
|
if (ctx === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let list: SavephotoURLBean[];
|
let parsed: Object;
|
||||||
try {
|
try {
|
||||||
list = JSON.parse(data) as SavephotoURLBean[];
|
parsed = JSON.parse(data) as Object;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log.w(`getphoto bad json: ${(e as Error).message}`);
|
this.log.w(`getphoto bad json: ${(e as Error).message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// H5 可能传非数组 → 守卫,避免 .map 抛错致出站永不下发(H5 等待挂死)
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
this.log.w('getphoto data is not an array');
|
||||||
|
this.bridge?.callHandler(OutboundHandlers.GetPhoto, '[]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list: SavephotoURLBean[] = parsed as SavephotoURLBean[];
|
||||||
const dir: string = `${ResourceManager.resourceRoot(ctx)}/photos`;
|
const dir: string = `${ResourceManager.resourceRoot(ctx)}/photos`;
|
||||||
|
// downloadOne 自身吞错并回空 photourl(不 reject),故 Promise.all 必定 resolve
|
||||||
const tasks: Promise<SavephotoURLBean>[] = list.map((item: SavephotoURLBean) => this.downloadOne(item, dir));
|
const tasks: Promise<SavephotoURLBean>[] = list.map((item: SavephotoURLBean) => this.downloadOne(item, dir));
|
||||||
Promise.all(tasks).then((results: SavephotoURLBean[]) => {
|
Promise.all(tasks).then((results: SavephotoURLBean[]) => {
|
||||||
this.bridge?.callHandler(OutboundHandlers.GetPhoto, JSON.stringify(results));
|
this.bridge?.callHandler(OutboundHandlers.GetPhoto, JSON.stringify(results));
|
||||||
}).catch((e: Error) => {
|
|
||||||
this.log.w(`getphoto failed: ${e.message}`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class ShakeProvider implements CapabilityProvider {
|
|||||||
private listening: boolean = false;
|
private listening: boolean = false;
|
||||||
private feedbackOn: boolean = false;
|
private feedbackOn: boolean = false;
|
||||||
private pending: boolean = false;
|
private pending: boolean = false;
|
||||||
|
private endTimer: number = -1;
|
||||||
private readonly onAccel: (data: sensor.AccelerometerResponse) => void;
|
private readonly onAccel: (data: sensor.AccelerometerResponse) => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -72,6 +73,12 @@ export class ShakeProvider implements CapabilityProvider {
|
|||||||
this.log.w(`sensor.off failed: ${(e as BusinessError).message}`);
|
this.log.w(`sensor.off failed: ${(e as BusinessError).message}`);
|
||||||
}
|
}
|
||||||
this.listening = false;
|
this.listening = false;
|
||||||
|
// 取消未决的 shakeEnd 计时并复位节流,避免停止后仍触发出站 / 重启后首次摇动被吞
|
||||||
|
if (this.endTimer !== -1) {
|
||||||
|
clearTimeout(this.endTimer);
|
||||||
|
this.endTimer = -1;
|
||||||
|
}
|
||||||
|
this.pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAccel(data: sensor.AccelerometerResponse): void {
|
private handleAccel(data: sensor.AccelerometerResponse): void {
|
||||||
@@ -88,7 +95,8 @@ export class ShakeProvider implements CapabilityProvider {
|
|||||||
vibrator.startVibration({ type: 'time', duration: 200 }, { usage: 'notification' })
|
vibrator.startVibration({ type: 'time', duration: 200 }, { usage: 'notification' })
|
||||||
.catch((e: BusinessError) => this.log.w(`shake vibrate failed: ${e.message}`));
|
.catch((e: BusinessError) => this.log.w(`shake vibrate failed: ${e.message}`));
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
this.endTimer = setTimeout(() => {
|
||||||
|
this.endTimer = -1;
|
||||||
this.bridge?.callHandler(OutboundHandlers.ShakeEnd, '');
|
this.bridge?.callHandler(OutboundHandlers.ShakeEnd, '');
|
||||||
this.pending = false;
|
this.pending = false;
|
||||||
}, SHAKE_END_DELAY);
|
}, SHAKE_END_DELAY);
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export class VibrateProvider implements CapabilityProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startRepeat(): void {
|
private startRepeat(): void {
|
||||||
|
// 先清旧链:避免重复 repeatvibrator('1') 叠加出无法取消的孤儿计时链
|
||||||
|
this.stopRepeat();
|
||||||
this.repeating = true;
|
this.repeating = true;
|
||||||
this.loop();
|
this.loop();
|
||||||
}
|
}
|
||||||
|
|||||||