<

{{title}}

{{file.summary.functions.pct}}%

Functions
{{file.summary.functions.covered}}/{{file.summary.functions.total}}

{{file.summary.branches.pct}}%

Branches
{{file.summary.branches.covered}}/{{file.summary.branches.total}}

{{file.summary.lines.pct}}%

Lines
{{file.summary.lines.covered}}/{{file.summary.lines.total}}
          
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
                
import { BuilderNode, FrameNode, NodeController } from '@ohos.arkui.node'; 
import { UIContext } from '@ohos.arkui.UIContext'; 
import { WebHostModel } from './WebHostModel'; 
import { RenderRecovery } from '../RenderRecovery'; 
 
/** 渲染进程连续崩溃恢复次数上限,超过则判定不可恢复并关闭容器(对齐 Android)。 */ 
const MAX_RENDER_RECOVERY: number = 3; 
 
/** 
 * 崩溃恢复计数的冷却窗口(毫秒)。距上次崩溃超过此窗口视为页面已稳定运行过, 
 * 本次崩溃按全新问题处理并重置连续计数,避免长时间运行中的偶发(非连续)崩溃 
 * 累计触顶被误判为"不可恢复"。对齐 iOS renderRecoveryResetWindow。 
 */ 
const RENDER_RECOVERY_RESET_WINDOW: number = 30000; 
 
/** 
 * 承载 SDK Web 组件的**统一命令式 Builder + NodeController**(离线 Web 组件)。 
 * 
 * Page(open 模式)、Component(嵌入模式)、以及预热池都用这同一套 `@Builder` 构建, 
 * 从而保证「池预热态」的组件能被真实页 `NodeContainer` 绑定复用(事件绑定同构)。 
 * 
 * 所有 Web 事件回调转发到 `model.handlers`,联动状态写回 `model`(`@Observed`,容器响应式读取)。 
 */ 
 
/** 统一 Web 组件 Builder:所有链式属性/事件与原 Page/Component 声明等价。 */ 
@Builder 
function tencentQianWebBuilder(model: WebHostModel): void { 
  Web({ src: model.url, controller: model.controller }) 
    .javaScriptAccess(model.handlers!.webSettings.javaScriptAccess) 
    .domStorageAccess(model.handlers!.webSettings.domStorageAccess) 
    .mediaPlayGestureAccess(model.handlers!.webSettings.mediaPlaybackRequiresUserGesture) 
    .fileAccess(model.handlers!.webSettings.fileAccess) 
    .databaseAccess(model.handlers!.webSettings.databaseAccess) 
    .textZoomRatio(model.handlers!.webSettings.textZoomAtio) 
    .zoomAccess(model.handlers!.webSettings.zoomAccess) 
    .multiWindowAccess(model.handlers!.webSettings.multiWindowAccess) 
    .wideViewModeAccess(model.handlers!.webSettings.wideViewModeAccess) 
    .overviewModeAccess(model.handlers!.webSettings.overviewModeAccess) 
    .mixedMode(model.handlers!.mixedMode === 'All' ? MixedMode.All : MixedMode.None) 
    .javaScriptOnDocumentStart(model.handlers!.documentStartScripts as Array<ScriptItem>) 
    .onControllerAttached(() => { 
      model.handlers!.onControllerAttached(); 
    }) 
    .onLoadIntercept((event: ESObject) => model.handlers!.onLoadIntercept(event)) 
    .onPageBegin((event: ESObject) => { 
      model.isPageLoading = true; 
      model.progressVisible = true; 
      model.progress = 0; 
      if (model.onProgressChanged !== null) { 
        model.onProgressChanged(0, true); 
      } 
      model.lastVisitedUrl = RenderRecovery.captureLastUrl(event, model.lastVisitedUrl); 
      model.handlers!.onPageBegin(event); 
    }) 
    .onPageEnd((event: ESObject) => { 
      model.isPageLoading = false; 
      model.progress = 100; 
      // 页面成功加载完成 → 崩溃恢复计数清零,但**必须门控稳定运行窗口**: 
      // OOM 型必崩页面每次都是"加载完成 → 运行几秒 → 再崩",若无条件清零,则 onPageEnd 
      // 会把刚累加的计数抹掉,renderRecoveryCount 永远停在 1,MAX_RENDER_RECOVERY 上限形同 
      // 虚设、陷入无限 reload。仅当距上次崩溃超过 RENDER_RECOVERY_RESET_WINDOW(真正稳定运行过) 
      // 才清零;否则保留计数,让连续崩溃能累加到上限触发 onRenderUnrecoverable。 
      // 从未崩溃过的正常页面:lastRenderTerminateAtMs 初始 0,差值远超窗口,照常清零(本就是 0,no-op)。 
      if (Date.now() - model.lastRenderTerminateAtMs > RENDER_RECOVERY_RESET_WINDOW) { 
        model.renderRecoveryCount = 0; 
      } 
      // 恢复成功后 timer 已执行完毕,复位 id 保持状态干净(clearTimeout 已触发 id 为 no-op)。 
      model.pendingRecoverTimerId = -1; 
      if (model.onProgressChanged !== null) { 
        model.onProgressChanged(100, true); 
        // 短暂保留 100% 后淡出 
        setTimeout(() => { 
          if (model.onProgressChanged !== null) { 
            model.onProgressChanged(100, false); 
          } 
        }, 200); 
      } 
      try { 
        // 池复用(HEAVY 预热)后的首个页面加载完成:清掉预热残留历史(warmupUrl / about:blank), 
        // 使 signingUrl 成为唯一历史项,避免第一次点返回退回预热页。仅执行一次,不影响页内导航。 
        if (model.clearHistoryOnce) { 
          model.clearHistoryOnce = false; 
          try { 
            model.controller.clearHistory(); 
          } catch (_e) { 
            // ignore 
          } 
        } 
        model.canGoBack = model.controller.accessBackward(); 
        model.title = model.controller.getTitle(); 
        if (model.onTitleChanged !== null) { 
          model.onTitleChanged(model.title, model.canGoBack); 
        } 
      } catch (_e) { 
        // ignore 
      } 
      model.handlers!.onPageEnd(event); 
    }) 
    .onProgressChange((event: ESObject) => { 
      // 对齐 Android animateProgressTo:仅在整页加载周期(onPageBegin 后、onPageEnd 前) 
      // 驱动进度条。SPA 软导航(pushState/hashchange)不触发 onPageBegin,isPageLoading=false, 
      // 其加载子资源触发的 onProgressChange 一律不显示,避免切路由时进度条重现。 
      if (!model.isPageLoading) { 
        model.handlers!.onProgressChange(event); 
        return; 
      } 
      const np: number = (event as ESObject).newProgress as number; 
      model.progress = np; 
      model.progressVisible = np < 100; 
      if (model.onProgressChanged !== null) { 
        model.onProgressChanged(np, model.progressVisible); 
      } 
      model.handlers!.onProgressChange(event); 
    }) 
    .onPermissionRequest((event: ESObject) => model.handlers!.onPermissionRequest(event)) 
    .onShowFileSelector((event: ESObject) => model.handlers!.onShowFileSelector(event)) 
    .onAlert((event: ESObject) => model.handlers!.onAlert(event)) 
    .onConfirm((event: ESObject) => model.handlers!.onConfirm(event)) 
    .onPrompt((event: ESObject) => model.handlers!.onPrompt(event)) 
    .onDownloadStart((event: ESObject) => model.handlers!.onDownloadStart(event)) 
    .onErrorReceive((event: ESObject) => model.handlers!.onErrorReceive(event)) 
    .onHttpErrorReceive((event: ESObject) => model.handlers!.onHttpErrorReceive(event)) 
    .onSslErrorEventReceive((event: ESObject) => model.handlers!.onSslErrorEventReceive(event)) 
    .onRenderExited((event: ESObject) => { 
      model.handlers!.onRenderExited(event); 
      // 借出态实例崩溃:即便 reload 成功也已是"渲染进程死过的"僵尸,作废池避免归还后污染复用。 
      // 对齐 Android 借出态 invalidatePooledWebView(自建实例 fromPool=false,不触发)。 
      if (model.fromPool && model.onPoolInvalidateRequest !== null) { 
        try { 
          model.onPoolInvalidateRequest(); 
        } catch (_e) { 
          // ignore 
        } 
        model.fromPool = false; 
      } 
      // 冷却重置:距上次崩溃超过 RENDER_RECOVERY_RESET_WINDOW 视为页面已稳定运行过, 
      // 本次按全新问题处理,重置连续计数,避免偶发崩溃被误判为"不可恢复"。 
      const nowMs: number = Date.now(); 
      if (nowMs - model.lastRenderTerminateAtMs > RENDER_RECOVERY_RESET_WINDOW) { 
        model.renderRecoveryCount = 0; 
      } 
      model.lastRenderTerminateAtMs = nowMs; 
      // 连续崩溃达上限 → 判定不可恢复,停止 reload 并关闭容器,避免死循环 reload 风暴。 
      model.renderRecoveryCount++; 
      if (model.renderRecoveryCount > MAX_RENDER_RECOVERY) { 
        if (model.onRenderUnrecoverable !== null) { 
          model.onRenderUnrecoverable(); 
        } 
        return; 
      } 
      const fallback: string = model.lastVisitedUrl.length > 0 ? model.lastVisitedUrl : model.url; 
      // 取消上一次未执行的延迟恢复(连续崩溃场景),避免累积多个 timer。 
      model.cancelPendingRecover(); 
      // 恢复链 refresh → loadUrl 均失败(崩溃后 controller 常已失效)时,通知容器关闭, 
      // 消除持久白屏兜底缺口。对齐 Android 恢复失败 finish()。 
      // 保存 timer id:用户在 1s 延迟窗口内关闭页面时,容器 aboutToDisappear 会 clearTimeout 取消, 
      // 避免回调访问已销毁/已归还的 controller(野指针)。 
      model.pendingRecoverTimerId = RenderRecovery.recover(model.controller, fallback, () => { 
        model.pendingRecoverTimerId = -1; 
        if (model.onRenderUnrecoverable !== null) { 
          model.onRenderUnrecoverable(); 
        } 
      }); 
    }) 
    .width('100%') 
    .height('100%'); 
} 
 
const TENCENT_QIAN_WEB_WRAP: WrappedBuilder<[WebHostModel]> = wrapBuilder<[WebHostModel]>(tencentQianWebBuilder); 
 
/** 
 * 承载离线 Web 组件的 NodeController。容器把它交给 `NodeContainer(...)` 绑定即上树渲染。 
 */ 
export class WebNodeController extends NodeController { 
  private builderNode: BuilderNode<[WebHostModel]> | null = null; 
  private model: WebHostModel | null = null; 
 
  /** 命令式创建离线 Web 组件(触发渲染进程;未挂载时 Hidden/Inactive)。 */ 
  public init(uiContext: UIContext, model: WebHostModel): void { 
    this.model = model; 
    const node: BuilderNode<[WebHostModel]> = new BuilderNode<[WebHostModel]>(uiContext); 
    node.build(TENCENT_QIAN_WEB_WRAP, model); 
    this.builderNode = node; 
  } 
 
  makeNode(_uiContext: UIContext): FrameNode | null { 
    return this.builderNode !== null ? this.builderNode.getFrameNode() : null; 
  } 
 
  public getModel(): WebHostModel | null { 
    return this.model; 
  } 
 
  /** 销毁离线节点(调用前须确保已从 NodeContainer 解绑,避免白屏)。 */ 
  public disposeNode(): void { 
    try { 
      if (this.builderNode !== null) { 
        this.builderNode.dispose(); 
      } 
    } catch (_e) { 
      // ignore 
    } 
    this.builderNode = null; 
    this.model = null; 
  } 
} 

              
Code coverage generated by bjc at {{date}}