HarmonyOS 7 ArkWeb M144 迁移排障:旧缓存白屏与摄像头授权竞态,两个可复现案例
HarmonyOS 7 ArkWeb M144 迁移排障:旧缓存白屏与摄像头授权竞态,两个可复现案例
内嵌网页出现白屏,不一定是新内核不兼容。比如 HTML 已经更新,缓存里的脚本还是旧版本:请求全部返回 200,页面却在执行脚本时出错。另一个容易忽略的问题是授权回调:页面已经跳转,之前申请摄像头的结果才返回。
下面用两个独立的代码案例分析这两种情况。缓存部分检查 HTML、脚本和缓存条目的版本是否一致;授权部分记录请求所属的页面,页面跳转后取消未完成的请求。

版本边界:升级内核不等于缓存和权限都自动修好
截至 2026-09-13,华为 ArkWeb 简介(更新于 2026-09-09)列出的 HarmonyOS 7.0 默认内核为 M144,也可选择 M132。迁移时需要检查缓存和授权逻辑,但这两类问题并非 M144 独有,旧内核下也可能发生。
示例使用 JavaScript 实现缓存选择和授权管理,可在 Node.js 中运行。接入 HarmonyOS 时,仍需通过 Web 组件加载资源,并分别处理系统权限和网页资源授权。本次验证范围是这两段逻辑;API 26 SDK 编译、真机音视频和内核联调尚未验证。
| 层次 | 核对内容 | 不应混淆的概念 |
|---|---|---|
| 系统与 SDK | HarmonyOS 7、API 26、实际内核 | 系统版本不是网页业务版本 |
| 网页资源 | HTML、JS、CSS 的 build 与摘要 | HTTP 缓存与应用自建缓存不同 |
| Web 容器 | 页面来源、当前主文档、Web 请求 | Web 请求不是系统授权结果 |
| 系统权限 | 摄像头、麦克风的实际授权 | 获得系统权限不代表任意网页可使用 |
案例一:HTML 已经是 b2,app.js 还是 b1
为了稳定复现,不需要等线上缓存“偶发”。先准备两个业务版本:b1 导出旧接口,b2 HTML 调用新接口,但让应用自建资源缓存继续返回 b1 的 app.js。入口可以显示标题,执行脚本后却出现接口缺失错误。
具体条件是资源缓存只按 pathname 索引,忽略 build 参数;于是 /app.js?build=b2 仍命中 /app.js 的旧内容。这是应用缓存设计缺陷,不应描述为 M144 自带的错误。若应用根本没有自建缓存,应先查 HTTP 响应头、CDN 和网页 Service Worker,而不是套用本例。
排查时记录 HTML 声明的 build、请求 URL 的 build,以及缓存条目的 build 与 hash。状态码 200 只说明请求成功,返回的脚本仍可能是旧版本。
export function selectAsset(requestUrl, manifest, cached, trustedOrigin = 'https://app.example') {
const url = new URL(requestUrl);
if (url.origin !== trustedOrigin) return { action: 'network', reason: 'outside_origin' };
const expected = manifest.get(url.pathname);
if (!expected) return { action: 'network', reason: 'outside_manifest' };
if (url.searchParams.get('build') !== expected.build) {
return { action: 'reload_document', reason: 'document_build_mismatch' };
}
if (cached && cached.build === expected.build && cached.hash === expected.hash) {
return { action: 'cache', reason: 'verified_asset' };
}
return { action: 'network', reason: 'missing_or_stale_asset' };
}
manifest 保存本次部署的资源清单,需要和当前 HTML 版本一致。trustedOrigin 配置为应用自己的源站,代码中的域名仅用于演示;不同源站即使路径相同,也不能共用缓存条目。hash 是资源内容摘要。这段函数只比较已有的版本和摘要,不负责下载或计算哈希;实际接入时,要先计算响应内容的摘要,校验通过后再写入缓存。
三种处理方式的区别
| 方案 | 能解决什么 | 代价与风险 |
|---|---|---|
| 每次进入禁用缓存 | 减少旧资源命中 | 弱网首屏变慢、流量增加 |
| 发布后清所有数据 | 可能暂时恢复 | 影响离线体验,甚至误清登录等无关状态 |
| 版本化清单与内容寻址 | 精确拒绝错配内容 | 需要部署与缓存层协作 |
推荐第三种。HTML 保持可重新验证,带内容指纹的静态文件可以长期缓存;应用缓存用 build、完整资源键和内容摘要区分版本。新包准备完成后再切换活动清单,不能先把 HTML 更新,再慢慢覆盖其依赖。
selectAsset 返回 reload_document 时,表示当前页面仍使用旧 build,而活动清单已经更新。容器应提示或安排受控刷新,不能直接在旧页面中混入新脚本。刷新失败保留明确错误页,最多一次受控重试;循环 reload 会把版本问题变成加载风暴。
旧缓存会如何处理
const manifest = new Map([['/app.js', { build: 'b2', hash: 'h2' }]]);
const url = 'https://app.example/app.js?build=b2';
console.log(selectAsset(url, manifest, { build: 'b1', hash: 'h1' }));
// { action: 'network', reason: 'missing_or_stale_asset' }
console.log(selectAsset(url, manifest, { build: 'b2', hash: 'h2' }));
// { action: 'cache', reason: 'verified_asset' }
返回 network 后,调用方需要重新获取资源。下载失败、资源 404 或摘要不符,应分别显示错误或重试,不要再次使用已经确认版本不一致的旧脚本。
把版本错配真的跑出一次错误
只检查返回值,还看不到旧脚本为什么会让页面出错。下面准备两段脚本:b1 没有 render,b2 增加了 render。入口按 b2 调用,旧缓存却只按路径返回 b1,于是直接抛出 TypeError。
把这段代码接在 selectAsset 定义后运行。Node.js 的 vm 只用来执行这两段受控脚本,不是浏览器,也不模拟 DOM;这个实验验证的是脚本接口混版,不是 M144 的渲染结果。
import { createHash } from 'node:crypto';
import { runInNewContext } from 'node:vm';
import assert from 'node:assert/strict';
const digest = text => createHash('sha256').update(text).digest('hex');
const oldScript = 'globalThis.app = { version: "b1" };';
const newScript = 'globalThis.app = { version: "b2", render: () => "ready:b2" };';
const entry = '\napp.render();';
const requestUrl = 'https://app.example/app.js?build=b2';
const legacyCache = new Map([['/app.js', oldScript]]);
const legacyScript = legacyCache.get(new URL(requestUrl).pathname);
assert.throws(() => runInNewContext(legacyScript + entry), /app.render is not a function/);
const activeManifest = new Map([['/app.js', { build: 'b2', hash: digest(newScript) }]]);
const oldItem = { build: 'b1', hash: digest(oldScript), body: oldScript };
const decision = selectAsset(requestUrl, activeManifest, oldItem);
assert.equal(decision.action, 'network');
const downloadedBody = newScript;
const expected = activeManifest.get('/app.js');
assert.equal(digest(downloadedBody), expected.hash);
const nextItem = { build: expected.build, hash: digest(downloadedBody), body: downloadedBody };
assert.equal(selectAsset(requestUrl, activeManifest, nextItem).action, 'cache');
assert.equal(runInNewContext(nextItem.body + entry), 'ready:b2');
const damagedItem = { ...nextItem, body: oldScript };
const checkedItem = { ...damagedItem, hash: digest(damagedItem.body) };
assert.equal(selectAsset(requestUrl, activeManifest, checkedItem).action, 'network');
console.log('cache: legacy=TypeError, fixed=ready:b2, damaged=network');
修复前,路径相同就取旧脚本,入口执行失败。修复后,旧条目被拒绝,下载内容的 SHA-256 与清单一致后才写入缓存,入口返回 ready:b2。最后再把缓存正文换回旧脚本,重新计算摘要,仍会被拒绝。这一步说明只保存 hash 字段不够,正文和摘要必须对应。
线上实现还要约束资源键:如果同一路径会随语言、账号或查询参数返回不同内容,需要完整缓存键或禁止缓存这类响应。这个例子只处理固定内容的 /app.js,不能直接用于接口响应。SHA-256 用于检查内容是否一致;如果资源清单本身被篡改,它不能替代可信传输和清单来源校验。
案例二:页面 A 的权限结果,在页面 B 才返回
复现过程可以固定为四步:页面 A 请求摄像头;系统授权流程未结束;主文档导航至页面 B;授权结果才返回。若回调只判断授权成功,不判断来源和页面身份,就可能对已经失效的请求执行后续操作。
更危险的写法是拿到系统权限后,批准 Web 请求中的全部资源。页面只请求摄像头时,不应顺便批准麦克风;受信网页授权不能被另一来源继承。来源必须从 Web 请求和当前文档上下文取得,不能相信网页自己传来的字符串。
Broker 检查请求来源、实际获准的资源和页面代次 epoch。video、audio 是示例内部使用的标识;接入 ArkWeb 时,需要映射到官方 ProtectedResourceType,未知类型直接拒绝。
export class PermissionBroker {
epoch = 0;
pending = new Map();
constructor(trustedOrigin, authorize, timeoutMs = 1000) {
this.trustedOrigin = trustedOrigin;
this.authorize = authorize;
this.timeoutMs = timeoutMs;
}
navigate() {
this.epoch += 1;
for (const finish of [...this.pending.values()]) finish([]);
}
request(id, origin, resources) {
if (this.pending.has(id)) throw new Error('duplicate_request_id');
const allowed = [...new Set(resources)];
if (origin !== this.trustedOrigin || allowed.length === 0 ||
allowed.some(v => v !== 'video' && v !== 'audio')) {
return Promise.resolve([]);
}
const epoch = this.epoch;
return new Promise(resolve => {
let settled = false;
let timer;
const finish = granted => {
if (settled) return;
settled = true;
clearTimeout(timer);
this.pending.delete(id);
resolve(granted);
};
this.pending.set(id, finish);
timer = setTimeout(() => finish([]), this.timeoutMs);
Promise.resolve().then(() => settled ? [] : this.authorize(allowed)).then(granted => {
finish(epoch === this.epoch ? allowed.filter(v => granted.includes(v)) : []);
}).catch(() => finish([]));
});
}
}
navigate 会立即结束旧请求,而不是等权限弹窗关闭;Promise 的迟到结果仍可能回来,但 settled 阻止第二次完成。timeoutMs 只是示例策略,产品应根据交互设置合理时限;授权弹窗停留多久并不受这个计时器控制。
id 是容器内部生成的请求序号,不使用网页可控ID。请求需要映射到原 Web PermissionRequest;Broker 返回允许的资源后,适配器只批准交集,否则拒绝。页面导航、Web 组件销毁都需要触发取消。缓存刷新只在页面 A 内重新渲染局部内容时,不应误当作主文档导航。
接到 HarmonyOS 时,分清两次授权
系统层用 Ability Kit 的 AtManager 查询或申请摄像头、麦克风权限;Web 层通过 onPermissionRequest 处理当前网页请求。系统层返回部分授权时,Broker 也只返回对应资源,不“全有或全无”误判,更不扩大请求范围。
权限检查失败、授权拒绝、页面离开、超时分别记录原因。这里不自动跳设置,不循环弹权限框,也不读取账号凭据。原始设备标识、页面查询参数和其他敏感内容不写入诊断日志。
| 事件 | Broker结果 | 容器动作 |
|---|---|---|
| 可信来源请求摄像头,系统允许 | video | 仅批准对应视频资源 |
| 摄像头允许、麦克风拒绝 | video | 只批准已授权子集 |
| 未知来源或未知资源 | 空数组 | 拒绝,不申请多余系统权限 |
| 主文档导航或组件销毁 | 空数组 | 结束旧请求 |
| 超时后授权才回来 | 仍为空数组 | 忽略迟到结果 |
对照实验:迟到的回调到底改了哪一页
错误写法在回调执行时读取 currentPage,没有记录请求发起时的页面。下面先让 A 发起请求,再切到 B,最后返回系统结果;回调就会错误地记录为 B 获准。这里用一个变量代替页面副作用,便于看到归属错误,不调用真正的摄像头。
把这段代码接在前面的 import、selectAsset 和 PermissionBroker 后运行。第二组实验先确认系统授权函数已经执行,再触发页面跳转,避免把“根本没开始申请”误当成“成功取消迟到回调”。
let resolveSystem;
const systemResult = new Promise(resolve => { resolveSystem = resolve; });
let currentPage = 'A';
let legacyApprovedPage;
const legacyCallback = systemResult.then(() => { legacyApprovedPage = currentPage; });
currentPage = 'B';
resolveSystem(['video']);
await legacyCallback;
assert.equal(legacyApprovedPage, 'B');
let resolveLater;
const delayedResult = new Promise(resolve => { resolveLater = resolve; });
let systemCalled = false;
const safeBroker = new PermissionBroker('https://app.example', () => {
systemCalled = true;
return delayedResult;
});
const oldRequest = safeBroker.request('request-A', 'https://app.example', ['video', 'audio']);
await Promise.resolve();
assert.equal(systemCalled, true);
safeBroker.navigate();
assert.deepEqual(await oldRequest, []);
resolveLater(['video']);
await Promise.resolve();
await Promise.resolve();
assert.equal(safeBroker.pending.size, 0);
const partialBroker = new PermissionBroker('https://app.example', async () => ['video']);
assert.deepEqual(await partialBroker.request('request-current', 'https://app.example', ['video', 'audio']), ['video']);
console.log('permission: legacy=approved on B, fixed=cancelled, partial=video only');
结果分别是:旧写法把结果用在 B;Broker 在跳转时结束 A 的请求,返回空数组;当前页面请求视频和音频、系统只允许视频时,仅返回 video。页面没有跳转的情况不需要取消,不能把一次局部刷新也算作离开页面。
这里更适合主动取消,而不是只在最后比较页面代次。只比较代次可以阻止结果回写,但调用方仍需一直等待授权结束;主动取消会立即结束旧 Promise,并清理 pending。settled 则保证超时、跳转和迟到结果同时出现时,请求只结束一次。它不会关闭系统权限弹窗,也不能撤销已经开始的系统授权流程。
测试范围与结果
前面两组对照实验另有 11 条断言,均在 Node.js 本地通过。缓存实验实际执行旧脚本和新脚本;授权实验实际等待受控 Promise 的结果。这些结果与下面 16 条策略测试分别记录,不等同于真机适配结果。
本地脚本使用 Node.js 内置 assert,覆盖六条缓存策略断言与十条授权策略断言。采用可控 Promise 模拟迟到结果,不需要在真机上反复等待偶发时序。运行命令为 node repair_164508898_arkweb_20260913.mjs --test。
let release;
const waiting = new Promise(resolve => { release = resolve; });
const broker = new PermissionBroker('https://app.example', () => waiting);
const result = broker.request('native-request-1', 'https://app.example', ['video']);
broker.navigate();
console.log(await result); // []
release(['video']); // 迟到授权不能恢复旧请求
Node.js 测试通过 16 条断言,覆盖旧资源拒绝、版本错配、跨源隔离、未知资源、重复 ID、部分授权、导航取消、异常拒绝、超时和待处理表清理。设备摄像头、系统权限弹窗和 M144 页面渲染仍需真机测试。
下面是完整的测试入口。把前面两个 export 实现放在同一文件顶部,再接上这段代码,保存成上述 mjs 文件即可运行;无需安装第三方包。
import assert from 'node:assert/strict';
let assertions = 0;
const eq = (a, b) => { assert.deepEqual(a, b); assertions++; };
const m = new Map([['/app.js', { build: 'b2', hash: 'h2' }]]);
const u = 'https://app.example/app.js?build=b2';
eq(selectAsset(u, m, { build: 'b1', hash: 'h1' }).action, 'network');
eq(selectAsset(u, m, { build: 'b2', hash: 'h2' }).action, 'cache');
eq(selectAsset('https://app.example/app.js?build=b1', m, null).action, 'reload_document');
eq(selectAsset(u, m, { build: 'b2', hash: 'wrong' }).action, 'network');
eq(selectAsset('https://app.example/api/state', m, null).reason, 'outside_manifest');
eq(selectAsset('https://evil.example/app.js?build=b2', m, { build: 'b2', hash: 'h2' }).reason, 'outside_origin');
const trusted = 'https://app.example';
const b = new PermissionBroker(trusted, async r => r);
eq(await b.request('1', 'https://evil.example', ['video']), []);
eq(await b.request('2', trusted, ['unknown']), []);
eq(await b.request('3', trusted, ['video', 'video']), ['video']);
const partial = new PermissionBroker(trusted, async () => ['video']);
eq(await partial.request('4', trusted, ['video', 'audio']), ['video']);
let release;
const wait = new Promise(r => { release = r; });
const stale = new PermissionBroker(trusted, () => wait);
const pending = stale.request('5', trusted, ['video']);
assert.throws(() => stale.request('5', trusted, ['video'])); assertions++;
stale.navigate();
eq(await pending, []);
release(['video']);
await Promise.resolve();
eq(stale.pending.size, 0);
const failure = new PermissionBroker(trusted, async () => { throw new Error('denied'); });
eq(await failure.request('6', trusted, ['audio']), []);
const timeout = new PermissionBroker(trusted, () => new Promise(() => {}), 10);
eq(await timeout.request('7', trusted, ['video']), []);
eq(timeout.pending.size, 0);
console.log('arkweb_policy_tests: ' + assertions + ' assertions passed');
| 待联调项 | 复现条件 | 验收信号 |
|---|---|---|
| M144资源升级 | b1缓存保留后更新b2 | 不出现HTML/JS混版 |
| 弱网首屏 | 请求超时、静态资源404 | 错误原因明确,无无限刷新 |
| 网页媒体权限 | 部分允许与全部拒绝 | 不批准未授权资源 |
| 导航竞态 | 弹窗未结束时离开主文档 | 旧请求只结束一次 |
| 生命周期 | 组件销毁后权限返回 | 无回写、pending归零 |
| 内核对照 | 同一网页在支持设备上测试M132/M144 | 业务与内核差异分别归类 |
接入后还要检查什么
先记录实际系统、SDK和内核,再看网络状态、HTTP错误与前端控制台。ERR_FAILED 不能直接判定内核Bug;还要区分资源404、跨域、安全策略与JS异常。确认缓存版本错配,修资源清单;确认迟到授权,修请求生命周期;只有业务侧证据排除以后,再整理最小页面与日志排查内核差异。
缓存选择和授权管理适合分别封装。前者接收资源清单与缓存条目,后者接收请求来源、资源集合和页面代次。分开实现后,缓存规则调整不会影响授权流程,也方便单独测试。
结论
迁移到 M144 后遇到白屏,先检查 HTML 和脚本是否来自同一次部署,再排查内核差异。遇到授权异常,重点检查回调对应的页面是否仍然有效、实际授予了哪些资源。版本校验和页面跳转取消分别处理这两个问题;后续修改缓存或授权逻辑时,可以先运行这些断言,再做真机回归。
官方资料与核对日期
- ArkWeb简介,更新时间2026-09-09,核对系统与Chromium版本对应关系:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/web-component-overview
- 定位网页加载问题,更新时间2026-09-09,核对错误定位与Web属性:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-page-loading
- Ability Kit权限管理参考,页面更新时间2025-05-20,核对AtManager授权接口;集成时仍需对照当前API26声明:https://developer.huawei.com/consumer/en/doc/harmonyos-references-V13/js-apis-abilityaccessctrl-V13
- API26版本说明入口:https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/overview-2600
资料核对日期:2026-09-13。缓存清单和 Broker 是应用侧示例实现,不是新增系统 API。
更多推荐



所有评论(0)