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() {}
}
|