<

{{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 webview from '@ohos.web.webview'; 
import common from '@ohos.app.ability.common'; 
import { TencentQianWebViewConfig } from './model/TencentQianWebViewConfig'; 
import { 
  OnSignResult, 
  OnSdkEvent, 
  OnRenderUnrecoverable, 
  OnTitleChanged, 
  OnProgressChanged 
} from './model/SignCallback'; 
import { 
  TencentQianWebViewConfigurator, 
  TencentQianWebViewHandlers 
} from './TencentQianWebViewConfigurator'; 
import { TencentQianWebViewController } from './TencentQianWebViewController'; 
import { WebHostModel } from './internal/prewarm/WebHostModel'; 
import { WebNodeController } from './internal/prewarm/TencentQianWebNode'; 
import { HarmonyWebViewPool } from './internal/prewarm/HarmonyWebViewPool'; 
 
/** 
 * 容器组件形态(灵活嵌入)。 
 * 
 * 封装一个完整配置的 Web 组件,内部已挂载 SDK 的全部能力: 
 * UA 注入、scheme 拦截、权限处理、文件选择、JS 弹窗、下载、错误事件回调等, 
 * 客户可在自己的页面任意位置嵌入,与自有 UI 自由组合。 
 * 
 * Web 组件改由离线组件(`BuilderNode` + `NodeContainer`)承载, 
 * 与 Page / 预热池共用同一 `@Builder`(`TencentQianWebNode`)。嵌入模式为客户自持场景, 
 * **默认自建自管**、随生命周期 `dispose`(不接预热池,池由 `open()` 容器模式复用)。 
 * 
 * **可选池复用(`reusePrewarmPool`,默认 false)**:客户显式开启后,本组件也会尝试复用 
 * `prewarm(HEAVY)` 预热的池实例(UA 配置匹配时),关闭时按借还池归还(不销毁)。 
 * ⚠️ 仅当组件是「一次性、非缓存」的主签署入口时才建议开启——嵌入模式生命周期由客户 
 * UI 树掌控,若页面被 tab 切换 / `@Reusable` / 路由缓存,`aboutToDisappear` 不等于 
 * 「签署结束」,归还后实例可能被他处借走导致状态错乱。默认关闭以保证边界清晰。 
 * 
 * **适用场景**:客户需要自定义页面布局(顶栏 / 底栏 / 侧边栏等)。 
 * 
 * **不包含**:顶栏 / 进度条 / 关闭确认对话框(由客户自行实现); 
 * 如需开箱即用的完整签署页,改用 `TencentQianWebView.open()` 容器模式。 
 * 
 * **典型用法**: 
 * ```ts 
 * @Component 
 * struct MyPage { 
 *   build() { 
 *     Column() { 
 *       Row() { Text('我的签署') }.height(56) 
 * 
 *       TencentQianWebViewComponent({ 
 *         url: this.signUrl, 
 *         context: getContext(this) as common.UIAbilityContext, 
 *         onSignResult: (result) => { console.log(result); } 
 *       }) 
 *         .layoutWeight(1) 
 *     } 
 *   } 
 * } 
 * ``` 
 */ 
@Component 
export struct TencentQianWebViewComponent { 
  /** 目标 H5 URL。必填。 */ 
  @Prop url: string = ''; 
  /** UIAbility/页面 context。必填。 */ 
  context: common.UIAbilityContext | null = null; 
  /** 容器配置。可省,使用默认值。 */ 
  config: TencentQianWebViewConfig = new TencentQianWebViewConfig(); 
  /** 签署结果回调。必填。 */ 
  onSignResult: OnSignResult = (_r): void => { 
    // no-op default 
  }; 
  /** SDK 事件回调。可选。 */ 
  onSdkEvent?: OnSdkEvent = undefined; 
  /** 
   * 渲染进程连续崩溃且 SDK 放弃自动恢复(永久白屏终态)时触发。可选。 
   * 
   * 宿主据此显示错误页 / 返回上级 / 提示重试(配合 `controller.reload()`)。 
   * 与 `onSdkEvent` 的 `RenderProcessGoneEvent`(每次崩溃)语义互补。 
   */ 
  onRenderUnrecoverable?: OnRenderUnrecoverable = undefined; 
  /** 
   * H5 标题 / 可后退状态变化(`onPageEnd` 快照)。可选。 
   * 
   * 供宿主自绘顶栏更新标题与返回按钮禁用态。实时可后退值请用 `controller.canGoBack()`。 
   */ 
  onTitleChanged?: OnTitleChanged = undefined; 
  /** 
   * 加载进度变化(`progress` 0~100;`visible` 是否应显示)。可选。供宿主自绘进度条。 
   */ 
  onProgressChanged?: OnProgressChanged = undefined; 
  /** 
   * 嵌入模式 Web 控制器。可选。 
   * 
   * 宿主创建 `TencentQianWebViewController` 实例传入后,可在自绘的返回按钮 / 重试按钮里主动 
   * 操控内部 WebView(`canGoBack` / `goBack` / `handleBack` / `reload`)。不传则无法主动控制。 
   */ 
  controller?: TencentQianWebViewController = undefined; 
  /** 
   * 是否复用 `prewarm(HEAVY)` 预热的池实例。默认 false(自建自管)。 
   * 
   * ⚠️ 仅建议在「一次性、非缓存」的主签署入口场景开启(见类文档风险说明)。 
   */ 
  reusePrewarmPool: boolean = false; 
 
  // —— internal state —— 
  private webviewController: webview.WebviewController = new webview.WebviewController(); 
  @State private handlers: TencentQianWebViewHandlers | null = null; 
  /** 承载离线 Web 组件的 NodeController(null 表示尚未初始化 / 缺 context)。 */ 
  @State private nodeController: WebNodeController | null = null; 
  /** 本组件 Web 实例是否借自预热池(决定关闭时归还还是销毁)。 */ 
  private fromPool: boolean = false; 
 
  aboutToAppear(): void { 
    if (this.context === null) { 
      console.error('[TencentQianWebViewComponent] context is null, please pass the UIAbilityContext'); 
      return; 
    } 
 
    // 仅当客户显式开启 reusePrewarmPool 时尝试复用池实例(UA 配置匹配);否则自建自管 
    if (this.reusePrewarmPool) { 
      const pooled: WebNodeController | null = 
        HarmonyWebViewPool.obtain(this.config.injectUserAgent, this.config.userAgentExtra); 
      if (pooled !== null && pooled.getModel() !== null) { 
        this.reusePooled(pooled); 
        return; 
      } 
    } 
    this.buildOwnNode(); 
  } 
 
  aboutToDisappear(): void { 
    // 复位宿主控制器为 no-op,防止组件销毁后宿主误调用命令触及已销毁/已归还的 controller。 
    if (this.controller !== undefined && this.controller !== null) { 
      this.controller.detachInternal(); 
    } 
    if (this.handlers !== null) { 
      try { 
        this.handlers.networkStateInjector.detach(); 
      } catch (_e) { 
        // ignore 
      } 
    } 
    // 断开 model 对本组件的联动回调,防止池 model 长期持有已销毁组件引用(内存泄漏)。 
    if (this.nodeController !== null) { 
      const model: WebHostModel | null = this.nodeController.getModel(); 
      if (model !== null) { 
        model.clearContainerListeners(); 
      } 
    } 
    if (this.fromPool) { 
      // 借还池(同 Page/Android):先解绑 NodeContainer,再归还(about:blank + clearHistory,不销毁) 
      this.nodeController = null; 
      HarmonyWebViewPool.recycle(); 
    } else { 
      // 自建:销毁离线节点(一次性,不归还池) 
      if (this.nodeController !== null) { 
        this.nodeController.disposeNode(); 
        this.nodeController = null; 
      } 
    } 
  } 
 
  /** 复用预热池实例:用池 controller 生成真实 handlers,热替换 model.handlers/url 并重新激活。 */ 
  private reusePooled(pooled: WebNodeController): void { 
    const model: WebHostModel = pooled.getModel()!; 
    this.webviewController = model.controller; 
    // aboutToAppear 已保证 context 非 null 才会走到复用/自建分支 
    this.handlers = TencentQianWebViewConfigurator.apply( 
      this.webviewController, 
      this.context!, 
      this.config, 
      this.onSignResult, 
      this.onSdkEvent 
    ); 
    // detach 预热 handlers 的网络监听,切到真实 handlers 
    const old: TencentQianWebViewHandlers | null = model.handlers; 
    if (old !== null) { 
      try { 
        old.networkStateInjector.detach(); 
      } catch (_e) { 
        // ignore 
      } 
    } 
    model.handlers = this.handlers; 
    model.url = this.url; 
    // 池实例已在预热时加载过 warmupUrl(历史栈残留);标记首个 onPageEnd 清历史, 
    // 使 signingUrl 成为唯一历史项,避免第一次返回退回预热页。 
    model.clearHistoryOnce = true; 
    // 标记来自池 + 绑定作废回调:借出态渲染进程崩溃时作废池实例(避免僵尸复用), 
    // 并同步组件 fromPool=false(后续销毁不再走归还)。对齐 Android。 
    model.fromPool = true; 
    model.onPoolInvalidateRequest = (): void => { 
      // 借出态崩溃:只脱池(不再被 obtain 复用 / 不再被 recycle 收回),**不销毁正在显示的活节点**。 
      // controller 崩溃后仍有效,容器在同一 controller 上 refresh() 原地恢复(鸿蒙无需分离节点/重建); 
      // 活节点由容器继续持有,关闭时(fromPool=false → disposeNode)再销毁收口。对齐 Android invalidateIfPooled。 
      HarmonyWebViewPool.unpoolBorrowed(); 
      this.fromPool = false; 
    }; 
    // 绑定进度 / 标题 / 不可恢复联动回调到 model(转发给宿主 props),并把命令注入宿主控制器。 
    this.bindModelListeners(model); 
    this.nodeController = pooled; 
    this.fromPool = true; 
    // controller 已 attach(预热时),手动执行真实 handlers 的 attach(UA + 网络监听) 
    try { 
      this.handlers.onControllerAttached(); 
    } catch (_e) { 
      // ignore 
    } 
    this.attachController(); 
    // 加载真实签署页(命中预热热身的连接/内核缓存) 
    try { 
      this.webviewController.loadUrl(this.url); 
    } catch (_e) { 
      // ignore 
    } 
  } 
 
  /** 自建离线组件(无池可复用 / 未开启复用时)。 */ 
  private buildOwnNode(): void { 
    // aboutToAppear 已保证 context 非 null 才会走到复用/自建分支 
    this.handlers = TencentQianWebViewConfigurator.apply( 
      this.webviewController, 
      this.context!, 
      this.config, 
      this.onSignResult, 
      this.onSdkEvent 
    ); 
    const model: WebHostModel = new WebHostModel(this.webviewController); 
    model.url = this.url; 
    model.handlers = this.handlers; 
    // 绑定进度 / 标题 / 不可恢复联动回调到 model(转发给宿主 props)。 
    this.bindModelListeners(model); 
    const ctrl: WebNodeController = new WebNodeController(); 
    ctrl.init(this.getUIContext(), model); 
    this.nodeController = ctrl; 
    this.fromPool = false; 
    // 把命令注入宿主控制器(controller 已就绪)。 
    this.attachController(); 
  } 
 
  /** 
   * 绑定进度 / 标题 / 不可恢复联动回调到 model,转发给宿主对应 props。 
   * 
   * 与 Page 模式 `bindModelListeners` 同构,区别在于嵌入模式无内置顶栏 / 进度条, 
   * 联动信号直接透传给宿主自绘 UI(宿主未传对应 prop 时静默忽略)。 
   */ 
  private bindModelListeners(model: WebHostModel): void { 
    model.onProgressChanged = (progress: number, visible: boolean): void => { 
      if (this.onProgressChanged !== undefined) { 
        this.onProgressChanged(progress, visible); 
      } 
    }; 
    model.onTitleChanged = (title: string, canGoBack: boolean): void => { 
      if (this.onTitleChanged !== undefined) { 
        this.onTitleChanged(title, canGoBack); 
      } 
    }; 
    model.onRenderUnrecoverable = (): void => { 
      if (this.onRenderUnrecoverable !== undefined) { 
        this.onRenderUnrecoverable(); 
      } 
    }; 
  } 
 
  /** 
   * 把内部 WebView 命令(后退查询 / 后退 / 重载)注入宿主控制器。 
   * 
   * 宿主未传 `controller` 时为 no-op。命令实时走 `webviewController`:`canGoBack` 用 
   * `accessBackward()` 覆盖 SPA 软路由的历史深度;`goBack` 前置判断避免首屏无历史时报错。 
   */ 
  private attachController(): void { 
    if (this.controller === undefined || this.controller === null) { 
      return; 
    } 
    this.controller.attachInternal( 
      (): boolean => { 
        try { 
          return this.webviewController.accessBackward(); 
        } catch (_e) { 
          return false; 
        } 
      }, 
      (): void => { 
        try { 
          if (this.webviewController.accessBackward()) { 
            this.webviewController.backward(); 
          } 
        } catch (_e) { 
          // ignore 
        } 
      }, 
      (): void => { 
        try { 
          this.webviewController.refresh(); 
        } catch (_e) { 
          // ignore 
        } 
      } 
    ); 
  } 
 
  build() { 
    Column() { 
      if (this.nodeController === null) { 
        Text('TencentQianWebView: missing context') 
          .fontSize(14) 
          .fontColor('#ff0000') 
          .width('100%') 
          .height('100%') 
          .textAlign(TextAlign.Center); 
      } else { 
        NodeContainer(this.nodeController) 
          .width('100%') 
          .height('100%'); 
      } 
    } 
    .width('100%') 
    .height('100%'); 
  } 
} 

              
Code coverage generated by bjc at {{date}}