<

{{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 connection from '@ohos.net.connection'; 
import webview from '@ohos.web.webview'; 
import { UIContext } from '@ohos.arkui.UIContext'; 
import common from '@ohos.app.ability.common'; 
import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 
import type EnvironmentCallback from '@ohos.app.ability.EnvironmentCallback'; 
import type { Configuration } from '@ohos.app.ability.Configuration'; 
import { PrewarmLevel } from './model/PrewarmLevel'; 
import { PrewarmEnvironment, manifestUrlOf, environmentOfHost } from './model/PrewarmEnvironment'; 
import { PrewarmConfig } from './model/PrewarmConfig'; 
import { WarmupManifest, WarmupBizType } from './internal/prewarm/WarmupManifest'; 
import { ManifestStore } from './internal/prewarm/ManifestStore'; 
import { PrewarmDedup } from './internal/prewarm/PrewarmDedup'; 
import { HarmonyWebViewPool } from './internal/prewarm/HarmonyWebViewPool'; 
 
/** 
 * **WebView 预热统一入口**(三端对齐)。 
 * 
 * 两个正交维度: 
 * - 网络/内核预热(DNS / 内核初始化 / 连接 / warmupUrl)——省网络与内核首次初始化时间。 
 * - 实例池——省 WebView 创建时间(HEAVY 离屏建组件,容器复用)。 
 * 
 * **档位语义**: 
 * - `MINIMAL`:仅 DNS 预解析(三端一致,纯地基)。 
 * - `LIGHT`:把内核首次初始化提前——`initializeWebEngine`(幂等/主线程)+ `prepareForPageLoad` 预连接;**不建池**。 
 * - `HEAVY`:离屏池(`NodeController`+`BuilderNode`)加载 `warmupUrl` 热网络(**需 `uiContext`**)。 
 * 
 * 档位叠加:高档位补做未做的低档动作(DNS → 内核预初始化 → warmupUrl),低档动作均幂等。 
 * 
 * **典型用法**: 
 * ```ts 
 * // App 启动 
 * TencentQianWebViewPrewarmer.configure(); 
 * TencentQianWebViewPrewarmer.prewarm(PrewarmLevel.MINIMAL); 
 * // 列表曝光 / 已知业务 
 * TencentQianWebViewPrewarmer.prewarm(PrewarmLevel.LIGHT, 'single-sign'); 
 * // 强意图(HEAVY 需传 UIContext 以建离屏池) 
 * TencentQianWebViewPrewarmer.prewarm(PrewarmLevel.HEAVY, 'single-sign', uiContext); 
 * ``` 
 */ 
export class TencentQianWebViewPrewarmer { 
  private static store: ManifestStore = new ManifestStore(); 
  private static dedup: PrewarmDedup = new PrewarmDedup(); 
  private static config: PrewarmConfig = new PrewarmConfig(); 
  private static configured: boolean = false; 
  /** Web 引擎是否已初始化(全局一次,幂等)。 */ 
  private static webEngineInitialized: boolean = false; 
  /** 内存回调是否已注册(幂等守卫,避免多次开池重复注册)。 */ 
  private static memoryCallbackRegistered: boolean = false; 
 
  /** 
   * 全局配置。App 启动时调用;触发 manifest 后台异步拉取(不阻塞)。 
   * 
   * 可省——未调用时首次 `prewarm` / `getMenuBizTypes` 会以默认配置(PROD)懒初始化。 
   * 
   * @param config 预热配置,可省 
   */ 
  public static configure(config?: PrewarmConfig): void { 
    const cfg: PrewarmConfig = config !== undefined && config !== null ? config : new PrewarmConfig(); 
    // poolSize clamp(v1 恒为 1) 
    if (cfg.poolSize > 1) { 
      console.warn('[TencentQianWebViewPrewarmer] poolSize > 1 not supported in v1, clamped to 1'); 
      cfg.poolSize = 1; 
    } 
    TencentQianWebViewPrewarmer.config = cfg; 
    TencentQianWebViewPrewarmer.configured = true; 
    HarmonyWebViewPool.setEnabled(cfg.enablePool); 
 
    // manifest URL:显式覆盖优先,否则按环境映射 
    const url: string = cfg.manifestUrl !== null && cfg.manifestUrl.length > 0 
      ? cfg.manifestUrl 
      : manifestUrlOf(cfg.environment); 
    TencentQianWebViewPrewarmer.store.setManifestUrl(url); 
    TencentQianWebViewPrewarmer.store.onVersionChanged = (_m: WarmupManifest): void => { 
      TencentQianWebViewPrewarmer.dedup.reset(); 
    }; 
 
    // 初始化 Web 引擎(幂等,主线程;提前到启动空闲,best-effort) 
    TencentQianWebViewPrewarmer.ensureWebEngineInitialized(); 
 
    // 后台异步拉取 manifest 
    TencentQianWebViewPrewarmer.store.ensureFresh(); 
  } 
 
  /** 
   * 按意图档位触发预热,可多次调用(内部单调去重)。 
   * 
   * @param level 预热档位 
   * @param bizType 业务类型;`MINIMAL` 可省,`LIGHT`/`HEAVY` 必填 
   * @param uiContext UI 上下文;**仅 `HEAVY` 建离屏池时必需**(`windowStage.getMainWindowSync().getUIContext()` / 页面内 `this.getUIContext()`) 
   */ 
  public static prewarm(level: PrewarmLevel, bizType?: string, uiContext?: UIContext): void { 
    // 懒初始化(configure 可省) 
    if (!TencentQianWebViewPrewarmer.configured) { 
      TencentQianWebViewPrewarmer.lazyInit(); 
    } 
    // bizType 允许为空:空时预热公共资源(warmup.html 加载公共部分),去重按空 key 记账 
    const bt: string = bizType !== undefined && bizType !== null ? bizType : ''; 
    // 先用缓存(可能略旧)立即预热;缺失则排队 replay;过期则后台重拉(SWR) 
    TencentQianWebViewPrewarmer.store.ensureFresh((manifest: WarmupManifest, _changed: boolean): void => { 
      if (!TencentQianWebViewPrewarmer.dedup.shouldRun(level, bt)) { 
        return; 
      } 
      TencentQianWebViewPrewarmer.execute(level, bt, manifest, uiContext); 
      TencentQianWebViewPrewarmer.dedup.markDone(level, bt); 
    }); 
  } 
 
  /** 释放预热资源(鸿蒙一次性池:销毁离屏实例)。 */ 
  public static release(): void { 
    HarmonyWebViewPool.release(); 
  } 
 
  /** 释放所有预热资源并重置状态。 */ 
  public static releaseAll(): void { 
    HarmonyWebViewPool.release(); 
    TencentQianWebViewPrewarmer.dedup.reset(); 
  } 
 
  /** 当前配置的目标环境(供 open() 环境错配检测)。 */ 
  public static currentEnvironment(): PrewarmEnvironment { 
    return TencentQianWebViewPrewarmer.config.environment; 
  } 
 
  /** 是否已配置(供 open() 判断是否需要做错配检测)。 */ 
  public static isConfigured(): boolean { 
    return TencentQianWebViewPrewarmer.configured; 
  } 
 
  /** 
   * 一次性获取 manifest 中的 bizType 列表。 
   * 
   * - manifest 已缓存 → 立即回调 
   * - manifest 拉取中 → 排队,拉取完成后回调一次 
   * - 拉取失败 → 不回调(调用方需自行兜底) 
   * 
   * 未调用 `configure` 时会以默认配置(PROD)懒初始化,确保 manifestUrl 已设置, 
   * 避免 callback 滞留在 pending 队列中。 
   * 
   * @param callback 回调,参数为 manifest 中的 bizType key 列表 
   */ 
  public static getMenuBizTypes(callback: (bizTypes: string[]) => void): void { 
    if (!TencentQianWebViewPrewarmer.configured) { 
      TencentQianWebViewPrewarmer.lazyInit(); 
    } 
    TencentQianWebViewPrewarmer.store.ensureFresh((manifest: WarmupManifest, _changed: boolean): void => { 
      const list: string[] = manifest.menuBizTypes !== undefined && manifest.menuBizTypes !== null 
        ? manifest.menuBizTypes 
        : Object.keys(manifest.bizTypes); 
      callback(list); 
    }); 
  } 
 
  /** 
   * `open(url)` 环境错配检测(best-effort,仅告警)。 
   * 
   * 仅当 url host 命中 3 个已知 `quick.*` 环境之一、且与配置环境不一致时才 log 警告; 
   * 非 `quick.*` 域名(客户自有/其他)不告警——`open()` 本就支持任意域名。 
   */ 
  public static warnIfEnvironmentMismatch(url: string): void { 
    if (!TencentQianWebViewPrewarmer.configured) { 
      return; 
    } 
    const host: string = TencentQianWebViewPrewarmer.hostOf(url); 
    const env: PrewarmEnvironment | null = environmentOfHost(host); 
    if (env === null) { 
      return; 
    } 
    if (env !== TencentQianWebViewPrewarmer.config.environment) { 
      console.warn('[TencentQianWebViewPrewarmer] environment mismatch: configured=' 
        + TencentQianWebViewPrewarmer.config.environment + ' but open() host indicates ' + env 
        + ' — prewarm cache may miss'); 
    } 
  } 
 
  // ────────── 内部 ────────── 
 
  private static lazyInit(): void { 
    TencentQianWebViewPrewarmer.configured = true; 
    const url: string = manifestUrlOf(TencentQianWebViewPrewarmer.config.environment); 
    TencentQianWebViewPrewarmer.store.setManifestUrl(url); 
    TencentQianWebViewPrewarmer.store.onVersionChanged = (_m: WarmupManifest): void => { 
      TencentQianWebViewPrewarmer.dedup.reset(); 
    }; 
  } 
 
  /** 
   * 执行某档位的实际预热动作(档位叠加:高档补做未做的低档动作,低档动作幂等)。 
   */ 
  private static execute(level: PrewarmLevel, bizType: string, manifest: WarmupManifest, uiContext?: UIContext): void { 
    // ── 全局层:DNS(所有档位地基)── 
    TencentQianWebViewPrewarmer.prefetchDns(manifest.hosts); 
    // 任意档位做完全局 DNS 即视为 MINIMAL 已达,避免后续 MINIMAL 重复跑 
    TencentQianWebViewPrewarmer.dedup.markDone(PrewarmLevel.MINIMAL, ''); 
 
    if (level === PrewarmLevel.MINIMAL) { 
      return; // MINIMAL:仅 DNS 
    } 
 
    // ── LIGHT 层:内核首次初始化 + 连接预热(HEAVY 叠加)── 
    TencentQianWebViewPrewarmer.ensureWebEngineInitialized(); 
    TencentQianWebViewPrewarmer.preconnectHosts(manifest.hosts); 
 
    if (level === PrewarmLevel.LIGHT) { 
      return; // LIGHT:不建池 
    } 
 
    // ── HEAVY 层:离屏池加载 warmupUrl(需 uiContext)── 
    const biz: WarmupBizType | undefined = manifest.bizTypes[bizType]; 
    if (biz === undefined) { 
      return; 
    } 
    TencentQianWebViewPrewarmer.warmPool(biz.warmupUrl, uiContext); 
  } 
 
  /** 
   * HEAVY:离屏池加载 `warmupUrl`(含语言拼接,与 warmup.html 委托语义一致)。 
   * 缺 `uiContext` 时无法建组件 → 降级(仅前面的 DNS/连接预热生效)+ log。 
   */ 
  private static warmPool(warmupUrl: string, uiContext?: UIContext): void { 
    if (warmupUrl.length === 0 || !TencentQianWebViewPrewarmer.config.enablePool) { 
      return; 
    } 
    if (uiContext === undefined || uiContext === null) { 
      console.warn('[TencentQianWebViewPrewarmer] HEAVY needs a uiContext to build the off-screen pool; ' 
        + 'degraded (DNS/preconnect still applied). Pass uiContext via prewarm(HEAVY, bizType, uiContext).'); 
      return; 
    } 
    const url: string = TencentQianWebViewPrewarmer.appendLang( 
      warmupUrl, 
      TencentQianWebViewPrewarmer.config.preferredLanguage 
    ); 
    try { 
      TencentQianWebViewPrewarmer.registerMemoryCallbackOnce(uiContext); 
      HarmonyWebViewPool.warm( 
        url, 
        uiContext, 
        TencentQianWebViewPrewarmer.config.injectUserAgent, 
        TencentQianWebViewPrewarmer.config.userAgentExtra 
      ); 
    } catch (_e) { 
      // best-effort 
    } 
  } 
 
  /** 
   * 幂等注册系统内存压力监听。经 `uiContext.getHostContext()` 拿到 `ApplicationContext` 
   * (应用级、非页面级,避免随页面销毁而失效)注册 `on('environment', EnvironmentCallback)`, 
   * 其 `onMemoryLevel` 即系统内存告警回调。 
   * 
   * 注册时机放在真正开池处({@link warmPool}),既保证只有持有池实例时才监听内存, 
   * 又天然覆盖 configure/lazyInit 两条路径。 
   * 
   * **阈值策略**:`MEMORY_LEVEL_MODERATE` 及以上(MODERATE/LOW/CRITICAL)即回收**空闲**池实例 
   * (单实例约 200MB,偏积极省内存);借出中的实例由 {@link HarmonyWebViewPool.releaseIfIdle} 跳过, 
   * 不打断签署。真正释放后重置去重状态,使后续 prewarm 可重新预热。 
   */ 
  private static registerMemoryCallbackOnce(uiContext: UIContext): void { 
    if (TencentQianWebViewPrewarmer.memoryCallbackRegistered) { 
      return; 
    } 
    try { 
      const hostContext: common.Context | undefined = uiContext.getHostContext() as common.Context | undefined; 
      if (hostContext === undefined || hostContext === null) { 
        return; 
      } 
      const appContext: common.ApplicationContext = hostContext.getApplicationContext(); 
      const callback: EnvironmentCallback = { 
        onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 
          if (level >= AbilityConstant.MemoryLevel.MEMORY_LEVEL_MODERATE) { 
            if (HarmonyWebViewPool.releaseIfIdle()) { 
              TencentQianWebViewPrewarmer.dedup.reset(); 
            } 
          } 
        }, 
        onConfigurationUpdated: (_config: Configuration): void => { 
          // no-op:池实例不随配置变更处理 
        } 
      }; 
      appContext.on('environment', callback); 
      TencentQianWebViewPrewarmer.memoryCallbackRegistered = true; 
    } catch (_e) { 
      // best-effort:注册失败不影响预热主流程 
      console.warn('[TencentQianWebViewPrewarmer] register environment(memoryLevel) callback failed'); 
    } 
  } 
 
  /** 
   * 语言拼接(与 Android/iOS L4 对齐):`preferredLanguage` 非空 → 拼 `&lang=`(warmup.html 内 `?lang` 优先级最高); 
   * 空 → 原样返回,完全委托 warmup.html 自动判定。 
   */ 
  private static appendLang(url: string, lang: string | null): string { 
    if (lang === null || lang.length === 0) { 
      return url; 
    } 
    const sep: string = url.indexOf('?') >= 0 ? '&' : '?'; 
    return url + sep + 'lang=' + encodeURIComponent(lang); 
  } 
 
  /** 初始化 Web 引擎(全局一次、主线程、幂等、best-effort)。 */ 
  private static ensureWebEngineInitialized(): void { 
    if (TencentQianWebViewPrewarmer.webEngineInitialized) { 
      return; 
    } 
    try { 
      webview.WebviewController.initializeWebEngine(); 
      TencentQianWebViewPrewarmer.webEngineInitialized = true; 
    } catch (_e) { 
      // best-effort:失败不阻塞,下次可重试 
    } 
  } 
 
  /** MINIMAL:DNS 预解析。 */ 
  private static prefetchDns(hosts: string[]): void { 
    hosts.forEach((h: string): void => { 
      const host: string = TencentQianWebViewPrewarmer.hostOf(h); 
      if (host.length === 0) { 
        return; 
      } 
      try { 
        connection.getAddressesByName(host).catch((_e: Object): void => { 
          // ignore 
        }); 
      } catch (_e) { 
        // ignore 
      } 
    }); 
  } 
 
  /** LIGHT:内核级预连接(TLS/TCP,自带 DNS)。 */ 
  private static preconnectHosts(hosts: string[]): void { 
    hosts.forEach((h: string): void => { 
      const url: string = TencentQianWebViewPrewarmer.rootUrlOf(h); 
      if (url.length === 0) { 
        return; 
      } 
      try { 
        // 通过 ESObject cast 调用静态方法,规避 .d.ts 版本差异 
        const ctrl: ESObject = webview.WebviewController as ESObject; 
        ctrl.prepareForPageLoad(url, true, 2); 
      } catch (_e) { 
        // ignore 
      } 
    }); 
  } 
 
  /** 提取纯 host(去 scheme / path / 端口)。 */ 
  private static hostOf(urlOrHost: string): string { 
    let s: string = urlOrHost.trim(); 
    const schemeIdx: number = s.indexOf('://'); 
    if (schemeIdx >= 0) { 
      s = s.substring(schemeIdx + 3); 
    } 
    const slash: number = s.indexOf('/'); 
    if (slash >= 0) { 
      s = s.substring(0, slash); 
    } 
    const colon: number = s.indexOf(':'); 
    if (colon >= 0) { 
      s = s.substring(0, colon); 
    } 
    return s; 
  } 
 
  /** 归一化为带 scheme 的根 URL(preconnect 需要完整 URL)。 */ 
  private static rootUrlOf(urlOrHost: string): string { 
    const s: string = urlOrHost.trim(); 
    if (s.length === 0) { 
      return ''; 
    } 
    if (s.indexOf('://') >= 0) { 
      return s; 
    } 
    return 'https://' + s; 
  } 
 
  // ArkTS 类禁止实例化 
  private constructor() {} 
} 

              
Code coverage generated by bjc at {{date}}