Browser / Web APIs:宿主能力、生命周期与安全边界
最后核验:2026-06-30|适用版本:现代常青浏览器、Web Platform Baseline 2024–2026|前置知识:HTML、CSS、JavaScript、TypeScript
浏览器宿主能力与权限边界
ECMAScript 提供语言语义,浏览器提供 Window、Document、网络、存储、Worker、Observer、性能与安全模型。业务代码调用 fetch、history 或 localStorage 时,依赖的是宿主 API,不是 JavaScript 语言本身。
理解宿主边界可以避免三类错误:把 HTTP 404 当 Promise rejection、把 Worker 当可访问 DOM 的后台线程、把 CORS 当服务端鉴权。也能判断一个功能应由 URL、浏览器存储、服务器还是内存状态承担。
Web API 不应为了“原生”而滥用。41 条固定数据不需要 IndexedDB 或 Worker;CSS 容器查询已经解决的布局不需要 ResizeObserver;后端可靠性不能交给页面生命周期事件。先用最小宿主能力,只有数据规模、持久化或性能证据出现后再升级。
Realm、Window 与 Worker 生命周期
Window realm 拥有 document 和页面 UI;Dedicated Worker 有独立全局作用域、事件循环与模块图,没有 DOM;Service Worker 位于网络与页面之间,生命周期独立,适合离线与请求代理,不是常驻后台服务。不同 realm 拥有各自内建对象,跨 realm 的 instanceof 可能不符合直觉,应优先验证结构或使用标准品牌检查。
浏览上下文还包含 origin、active document、history entry 与生命周期状态。iframe 有独立 Window/Document;同源时父子可以访问 DOM,跨源时只能通过受限窗口能力和 postMessage 协作。页面进入 back/forward cache(bfcache)时不会完整销毁,恢复触发 pageshow;初始化和清理不能只假设 load/unload 各一次。
DOM 查询、更新与事件传播
DOM 是宿主对象树,不是 HTML 字符串。查询 API 返回静态或 live collection:querySelectorAll 是静态 NodeList,children/getElementsBy* 常是 live;遍历 live collection 时修改 DOM 会改变集合。创建与更新优先 createElement、textContent、replaceChildren,不可信文本不进入 innerHTML。
批量添加节点可使用 DocumentFragment,但现代 append(...nodes)/replaceChildren 已会高效搬移节点;不要为每次渲染先造通用虚拟 DOM。DOM 写入是否触发布局取决于随后是否读取需要最新几何的属性,关键是避免循环中读写交替。
事件按 capture、target、bubble 路径传播。event.target 是原始目标,currentTarget 是当前监听器所在节点;Shadow DOM 可能 retarget,完整路径用 composedPath()。stopPropagation 停止后续路径,preventDefault 取消可取消默认动作,两者不是同一件事。
const list = document.querySelector('#results');
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-entry-id]');
if (!button || !list.contains(button)) return;
const id = button.dataset.entryId;
if (!id) return;
console.log('选择条目', id);
});事件委托减少动态列表 listener,但不是越高越好:在最小稳定容器上注册,验证 closest 结果仍属于容器。addEventListener 选项中 once 自动移除一次监听,passive 承诺不 preventDefault(适合滚动相关事件),signal 可按生命周期批量取消。
const lifecycle = new AbortController();
addEventListener('resize', updateLayout, { signal: lifecycle.signal });
document.addEventListener('visibilitychange', pausePolling, { signal: lifecycle.signal });
// 组件/页面退出:
lifecycle.abort();自定义事件用 CustomEvent 传递同一页面内松耦合通知,但 detail 仍是共享对象,不是跨线程消息。业务命令更适合直接函数调用;只有确有多观察者和 DOM 边界时使用事件。
Fetch 请求生命周期与 AbortSignal
Fetch Standard 把 Request、Response 与 fetching 过程连接起来,并集成重定向、缓存、CORS、Service Worker 和流。fetch() 在收到 HTTP 404/500 时通常仍 fulfill 为 Response;只有网络层失败、策略阻止或取消等情况才 reject。应用必须检查 response.ok 或状态码。Fetch Standard
这条请求生命周期必须保留错误分类。用户能据此区分“离线”“服务端拒绝”“响应损坏”和“契约变化”;把所有异常显示成“加载失败”会丢掉可操作信息。
AbortController 提供协作取消。把 signal 传给 Fetch 和支持该协议的其他 API;新请求开始、页面离开或组件销毁时调用 abort()。取消不会回滚服务器已经执行的副作用,写请求仍需幂等键或事务语义。
Response body 是一次性可消费流。调用 json()、text() 或读取 reader 后,bodyUsed 会变为真;日志和业务都要读取时应谨慎 clone(),因为两个分支的背压与缓存仍有成本。
Request、Headers、凭据与缓存语义
Request 包含 URL、method、headers、body、mode、credentials、cache、redirect 和 signal 等。GET/HEAD 没有请求 body;写操作需设置服务器接受的 Content-Type,并在服务端做 CSRF、认证、授权、幂等和大小限制。浏览器会管理 forbidden headers,应用不能任意伪造 Host/Cookie。
credentials 决定 cookie/TLS client certificate/HTTP auth 凭据是否随请求以及跨源响应的凭据处理。跨源带凭据请求要求服务器返回具体 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials,不能用 *。CORS preflight 是浏览器在某些跨源请求前发送 OPTIONS 检查;它保护响应读取边界,不是防止服务器收到请求的权限系统。
HTTP 缓存由 Cache-Control、ETag、Last-Modified、Vary 等响应语义决定,Fetch cache 选项只调整请求如何与缓存交互。no-store、reload、no-cache 含义不同:no-cache 允许存储但要求复验,no-store 才要求不存储。业务缓存库不能忽略 HTTP 与 CDN 层,否则会出现多层陈旧和失效不一致。
| 失败层 | 观察证据 | 是否适合自动重试 |
|---|---|---|
| DNS/TLS/离线/策略拒绝 | fetch reject、Network failed | 幂等读取可带退避;先判断离线/取消 |
| 429/503 | status + Retry-After | 按服务端节流与幂等规则 |
| 401/403 | status 与认证上下文 | 不盲重试,刷新身份或提示权限 |
| 404 | status 与请求 URL | 通常是资源/路由问题,不重试 |
| JSON/结构错误 | body 与验证路径 | 版本/数据故障,不用网络重试掩盖 |
Streams、背压与增量解析
Response.body 是 ReadableStream。流允许边到达边处理并通过 reader/pipe 链传播背压,适合大文本、媒体和服务端流式响应;小 JSON 直接 response.json() 更短。流被锁定/消费后不能再次读取,tee()/clone 会让慢分支产生缓存压力。
async function readLines(response, onLine, signal) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.body) throw new Error('响应没有可读流');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = '';
try {
while (true) {
signal.throwIfAborted();
const { value = '', done } = await reader.read();
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) onLine(line);
if (done) break;
}
if (buffer) onLine(buffer);
} finally {
reader.releaseLock();
}
}这不是通用 JSON streaming parser;任意 JSON token 可跨 chunk,需 NDJSON 等明确分隔协议或成熟解析器。服务端若压缩、代理缓冲或 CDN 不 flush,客户端代码也无法保证逐条到达。
URL、History 与可分享状态
URL 是可分享状态边界。用 new URL() 和 URLSearchParams 解析、编码,不手拼查询字符串。history.replaceState 适合频繁同步筛选而不新增历史条目;pushState 适合用户可感知的新导航状态。popstate 时重新从 URL 派生状态,避免 URL 与内存双向漂移。
URL 由 scheme、authority、host、port、path、query、fragment 等组成,解析必须提供可信 base。验证外链时检查解析后的 protocol、origin 和 hostname allowlist,不用 startsWith('https://trusted.example'),后者会接受相似前缀。用户名密码、Unicode/punycode、默认端口和反斜杠都应交给标准解析器。
function readAllowedUrl(input, base = location.href) {
const url = new URL(input, base);
if (url.protocol !== 'https:') throw new TypeError('仅允许 HTTPS');
if (!['docs.example.com', 'api.example.com'].includes(url.hostname)) {
throw new TypeError('目标域名不在允许列表');
}
url.username = '';
url.password = '';
return url;
}History state 会 structured clone,但容量与持久性由浏览器决定,不放大型数据/密钥。pushState 不触发 hashchange,调用本身也不触发 popstate;应用路由必须处理直接导航、刷新、后退前进与服务器 fallback。只更新地址栏不能让服务器理解路由。
页面生命周期、权限与用户激活
页面至少经历 active、hidden、可能 frozen、pagehide/bfcache、discard 等状态。visibilitychange 是暂停轮询/动画的信号,不代表永久离开;pagehide 的 event.persisted 可提示进入 bfcache;pageshow 恢复时重新确认连接、权限和陈旧数据。避免 unload/beforeunload,只有确有未保存编辑时临时注册 beforeunload。
摄像头、麦克风、地理位置、通知、剪贴板、文件选择、全屏等能力受 secure context、Permissions Policy、用户许可和 transient user activation 共同约束。权限状态可能被用户或系统随时改变;拒绝是正常结果,核心流程必须有替代路径。
| 能力 | 触发要求 | 数据风险 | 降级 |
|---|---|---|---|
| Clipboard write/read | HTTPS,读取通常需用户激活/许可 | 读取可能暴露敏感剪贴板 | 可选中文本 + 原生复制 |
| Geolocation | HTTPS + 用户许可 | 精确位置与轨迹 | 手工选择地区/地址 |
| getUserMedia | HTTPS + 许可 + 设备 | 音视频隐私、track 生命周期 | 文件上传/文字输入 |
| Notifications | 明确用户意图后请求 | 打扰、跨会话标识 | 页面内状态/邮件设置 |
| File System/Picker | 用户操作 | 本地文件内容与持久 handle | <input type=file>/下载 |
请求权限不要在页面加载时批量弹窗。先解释用途,在用户执行对应任务时请求,捕获 NotAllowedError/NotFoundError/AbortError 等并停止已获得 MediaStreamTrack。Permissions API 只能查询部分能力,返回 prompt/granted/denied 也不保证下一次调用一定成功。
网络、HTTP、JSON 与契约错误分层
示例区分四类加载错误,使用 AbortSignal,安全写文本,并捕获存储异常。环境为现代浏览器和任意静态服务器。
class HttpError extends Error {
constructor(status) {
super(`HTTP ${status}`);
this.name = 'HttpError';
}
}
function parseEntries(value) {
if (!Array.isArray(value)) throw new TypeError('技术目录必须是数组');
return value.map((entry, index) => {
if (!entry || typeof entry !== 'object') throw new TypeError(`[${index}] 必须是对象`);
if (typeof entry.id !== 'string') throw new TypeError(`[${index}].id 必须是字符串`);
if (typeof entry.name !== 'string') throw new TypeError(`[${index}].name 必须是字符串`);
return { id: entry.id, name: entry.name };
});
}
async function loadEntries(url, signal) {
let response;
try {
response = await fetch(url, { signal });
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') throw error;
throw new TypeError('网络错误', { cause: error });
}
if (!response.ok) throw new HttpError(response.status);
let data;
try {
data = await response.json();
} catch (error) {
throw new SyntaxError('JSON 错误', { cause: error });
}
return parseEntries(data);
}
const controller = new AbortController();
const status = document.querySelector('[role="status"]');
const list = document.querySelector('ul');
try {
performance.mark('catalog-load-start');
const entries = await loadEntries('../../contracts/technologies.json', controller.signal);
list.replaceChildren(...entries.map(({ name }) => {
const item = document.createElement('li');
item.textContent = name;
return item;
}));
status.textContent = `已加载 ${entries.length} 项`;
performance.mark('catalog-load-end');
performance.measure('catalog-load', 'catalog-load-start', 'catalog-load-end');
} catch (error) {
status.textContent = error instanceof Error ? error.message : '未知错误';
}
try {
document.documentElement.dataset.theme = localStorage.getItem('theme') ?? 'light';
} catch {
document.documentElement.dataset.theme = 'light';
}
addEventListener('pagehide', () => controller.abort(), { once: true });把代码接入示例后,从仓库根目录运行:
pnpm --filter @roadmap/web-foundations test
python3 -m http.server 4173输入:先正常访问示例,再在 DevTools Network 中切换 Offline 后刷新。
预期结果:正常时状态区显示 41 / 41;离线时显示明确网络错误。把 JSON 临时改成非数组会显示数据验证错误;返回 404 会显示 HTTP 状态。无论错误类型如何,页面标题、筛选骨架与无脚本说明仍可阅读。
Storage、IndexedDB 与数据寿命
localStorage 是同步字符串键值存储,可能因隐私模式、配额、策略或无效源抛错;只适合小型偏好,不放令牌、敏感数据或大目录。IndexedDB 是异步、事务式对象数据库,适合较大结构化离线数据;它仍受同源与清理策略约束,也不替代服务器同步。IndexedDB 指南
IndexedDB 数据库由版本、object stores、indexes 和 transactions 组成。schema 只能在 upgradeneeded 的 versionchange transaction 修改;读写必须在事务 active 时排入请求。单个 request success 不代表事务已经提交,最终等待 complete/abort。
function requestToPromise(request) {
return new Promise((resolve, reject) => {
request.addEventListener('success', () => resolve(request.result), { once: true });
request.addEventListener('error', () => reject(request.error), { once: true });
});
}
function transactionDone(transaction) {
return new Promise((resolve, reject) => {
transaction.addEventListener('complete', resolve, { once: true });
transaction.addEventListener('abort', () => reject(transaction.error), { once: true });
transaction.addEventListener('error', () => reject(transaction.error), { once: true });
});
}
async function openCatalog() {
const request = indexedDB.open('catalog', 1);
request.addEventListener('upgradeneeded', () => {
const store = request.result.createObjectStore('entries', { keyPath: 'id' });
store.createIndex('column', 'column');
});
request.addEventListener('blocked', () => {
console.warn('请关闭仍在使用旧数据库版本的标签页');
});
const db = await requestToPromise(request);
db.addEventListener('versionchange', () => db.close());
return db;
}
async function saveEntries(db, entries) {
const transaction = db.transaction('entries', 'readwrite');
const store = transaction.objectStore('entries');
for (const entry of entries) store.put(entry);
await transactionDone(transaction);
}不要在事务请求之间 await 任意网络/计时 Promise,事务可能自动变为 inactive;先取得外部数据并验证,再开启短事务写入。大迁移采用可恢复版本策略,处理 blocked/versionchange,并在失败后保留旧事实源。
Service Worker、Cache API 与离线更新
Service Worker 运行在独立 worker context,可拦截其 scope 内请求、接收 push/sync 等事件。注册后经历 installing、waiting、activating、activated;新 worker 通常等待旧页面释放控制权。它可能随时终止,事件工作必须用 event.waitUntil() 延长生命周期,不能把内存当持久状态。
Cache API 保存 Request/Response 对,不遵循 HTTP 缓存的自动复验策略,应用自己决定匹配和更新。静态资源可 cache-first,实时 API 常 network-first 或 stale-while-revalidate;带用户身份的响应若错误缓存会跨会话泄露。
// service-worker.js:只缓存明确的同源静态壳
const CACHE = 'roadmap-shell-v1';
const SHELL = ['/', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)),
)),
);
});
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET' || new URL(event.request.url).origin !== location.origin) return;
event.respondWith(caches.match(event.request).then((cached) => cached ?? fetch(event.request)));
});这个最小例子没有离线回退页、更新通知、缓存容量和导航预加载,不能直接当生产策略。生产必须定义版本切换、失败回源、认证响应排除、清理与回滚;skipWaiting/clients.claim 会让新旧代码混用,只有协议兼容时使用。
Worker、Observer 与性能测量的选择
知识浏览器启动时创建一个 AbortController,Fetch 共享 JSON,把 response.json() 的结果保持为 unknown,随后交给 TypeScript 领域验证器。四类错误分别映射为中文状态,避免空列表与加载失败无法区分。
主题是设备偏好,只需一个字符串,使用 localStorage 并以 try/catch 保护;搜索和对比选择需要分享,写进 URL;技术目录是仓库共享事实源,通过 Fetch 读取;当前详情是短暂 UI 状态,只留在内存。不同寿命的数据不塞进同一个 store。
Worker 只在用户点击压力演示后启动。主线程复制 100 份目录,通过 structured clone 发送 { id, entries, query };Worker 复用纯搜索函数并返回结果。启动、脚本加载或执行失败时,终止 Worker 并同步搜索。真实大二进制数据可用 transferable objects 避免复制;普通小对象不需要预先复杂化。
Dedicated Worker 归创建者使用;SharedWorker 可让同源多个上下文经 MessagePort 连接,但生命周期与兼容性更复杂;Service Worker 面向请求代理/后台事件。Web Worker 不访问 DOM,CPU 工作也不会自动变快:启动、模块加载、structured clone 和消息排队都有固定成本。
ArrayBuffer 可作为 transferable 转移所有权,发送后原缓冲区 detached,适合大二进制避免复制。SharedArrayBuffer 允许 agent 共享内存,需要 cross-origin isolation(COOP/COEP)并用 Atomics 建立同步;普通 UI 数据不要承担共享内存竞态。
消息协议要像 API 一样验证类型、版本和 request id:
// 主线程
const worker = new Worker('./search.worker.js', { type: 'module' });
let nextId = 0;
function searchInWorker(entries, query) {
const id = ++nextId;
return new Promise((resolve, reject) => {
const onMessage = (event) => {
if (!event.data || event.data.id !== id) return;
worker.removeEventListener('message', onMessage);
event.data.error ? reject(new Error(event.data.error)) : resolve(event.data.entries);
};
worker.addEventListener('message', onMessage);
worker.postMessage({ type: 'search', id, entries, query });
});
}实际实现还要用 AbortSignal 移除 listener/发送 cancel、处理 worker error/messageerror,并在组件销毁终止 worker。只取最新结果时比较 request id,防止慢旧响应覆盖新查询。
跨 Window 通信的 postMessage 必须指定精确 targetOrigin(非敏感公开广播才考虑 *),接收端同时检查 event.origin、event.source 和消息 schema。origin 合法不代表用户有业务权限,服务端操作仍重新鉴权。
存储、Worker 与 Observer 选择:
| 需求 | 首选 API | 不选它的情况 |
|---|---|---|
| 可分享筛选与选择 | URL + History | 敏感值、体积大、仅设备偏好 |
| 小型设备偏好 | localStorage | 大对象、高频写、跨标签事务 |
| 大型离线结构化数据 | IndexedDB | 41 条随部署更新的只读 JSON |
| CPU 密集纯计算 | Dedicated Worker | 小列表、需要直接操作 DOM |
| 元素进入视口 | IntersectionObserver | CSS 能完成、需要精确每帧位置 |
| 元素尺寸改变 | ResizeObserver | 仅视口断点、容器查询已足够 |
| DOM 子树变化 | MutationObserver | 自己能在状态更新点直接调用逻辑 |
| 自定义性能条目 | Performance API / Observer | 无明确指标、只打印耗时日志 |
Observer 是异步通知,不是业务事件总线。IntersectionObserver 避免持续轮询几何信息,适合懒加载和曝光;ResizeObserver 观察盒尺寸,回调中再次改变被观察尺寸可能形成循环;MutationObserver 观察 DOM 变更,但如果 DOM 完全由当前应用控制,直接在写入点协调通常更简单。Intersection Observer、Resize Observer
Network、Performance 与安全检查清单
Network 面板先看请求 URL、method、status、redirect、CORS、缓存和 timing,再看代码。Promise reject 不包含 HTTP 状态是设计行为,不要用网络重试掩盖 401、404 或数据错误。Application 面板检查存储与 Service Worker;Performance 面板用 mark/measure 把业务阶段与主线程任务对齐。
Performance API 的时间戳来自高分辨率单调时钟,performance.mark() 与 measure() 适合业务片段;PerformanceObserver 适合异步消费 navigation、resource、long task 或 Web Vitals 所需条目。采集前定义指标、采样与隐私边界,避免把完整 URL、用户输入或设备指纹上传。
页面隐藏不等于即将永久关闭。visibilitychange 可暂停动画或轮询,pagehide 适合清理并兼容 back/forward cache;不要依赖 unload 可靠保存数据,它可能不执行且影响缓存。重要写入在用户操作时完成,遥测尾包按条件使用 sendBeacon 或 keepalive Fetch。
安全检查清单:
- 同源策略只隔离不同 origin 的读取,不替代身份认证与授权。
- CORS 是服务器声明哪些跨源前端可读取响应,不阻止非浏览器客户端发请求。
- CSP 限制资源与脚本执行来源,不能净化已拼接的不可信 HTML。
- 权限型 API 在用户动作后请求,拒绝时核心流程有降级。
- 外部数据保持
unknown,URL 限制允许协议,DOM 使用textContent。 -
localStorage、IndexedDB 与 Cache Storage 不保存长期敏感令牌。 - Worker 脚本来源、消息结构与 response id 都要验证。
- 跨窗口消息检查
event.origin和消息 schema,不使用*发送敏感数据。
常见误区:给所有 Fetch 自动重试(会重复非幂等写入);用 mode: 'no-cors' 解决 CORS(得到 opaque response,不能读取内容);把 localStorage 当数据库;每个条目创建一个 Worker;Observer 回调里同步读写大量布局;监听 unload 作为唯一保存机制。
Baseline、特性检测与隐私演进
Web API 由 WHATWG、W3C 与浏览器实现持续演进,应按具体功能和 Baseline 日期记录支持情况。Fetch、URL、History、Web Storage、Dedicated Worker 与主要 Observer 已是现代浏览器支持范围;较新的 Navigation API、Scheduler API、View Transitions 与部分存储能力仍需目标环境和特性检测。
平台方向是统一取消、流与异步迭代:更多 API 接受 AbortSignal,Fetch body 使用 Streams,Observer 批量异步交付。应用层应复用这些协议,而不是为每个模块设计不兼容的取消 token 和事件包装。
隐私保护会继续影响存储分区、第三方 cookie、精确时间和权限。任何依赖跨站身份或永久客户端存储的设计,都必须把浏览器清理、用户拒绝和分区视为正常状态,而非异常边角。
采用新 API 的最低路径:if ('Feature' in globalThis) 或方法级检测,保留可用基础版本,记录手动浏览器验证范围。不要根据 User-Agent 列表维护分支,也不要为一个装饰性增强下载大型 polyfill。
持久化事务、并发与恢复语义
选择存储不能只看容量。localStorage/sessionStorage 是同步字符串表,没有事务隔离;IndexedDB 事务把一组 object store 操作绑定为原子提交,但事务会在事件循环控制权和请求活动状态变化时自动结束;Cache API 保存 Request/Response 对,适合离线资源,不提供业务记录查询;服务器持久化才负责跨设备、权限、备份和一致性。
| 能力 | sessionStorage | localStorage | IndexedDB | Cache API | 服务端 |
|---|---|---|---|---|---|
| 生命周期 | 当前标签页会话 | 同源、可被用户清理 | 同源、可被清理 | 同源、可被清理 | 由产品策略决定 |
| 同步模型 | 同步 | 同步 | 异步请求 + 事务 | Promise | 网络协议 |
| 结构与查询 | 字符串键值 | 字符串键值 | object store、index、cursor | Request → Response | 数据库/服务定义 |
| 并发协调 | 标签页隔离 | 跨页通知但非事务锁 | 事务与版本升级阻塞 | 最后写入取决于应用 | 锁、事务或版本控制 |
| 失败恢复 | 重建短暂状态 | 解析失败时回退默认 | 处理 abort、quota、blocked、versionchange | 缓存未命中后回源 | 重试、幂等、备份与审计 |
| 安全边界 | 脚本可读 | 脚本可读 | 脚本可读 | 脚本/Service Worker 可读 | 认证、授权与加密策略 |
IndexedDB 版本升级是最容易被忽略的严重故障:其他标签页保持旧连接时,新连接会收到 blocked;旧连接应监听 versionchange 并关闭,否则发布后的用户可能永久卡在升级界面。写事务的 complete 才表示提交成功,单个 request 的 success 不等于整个事务已落盘。发生 QuotaExceededError、用户清理或隐私分区变化时,应能从服务端或随应用发布的数据重建,而不是把浏览器存储当唯一事实源。IndexedDB 3.0
跨标签并发若只需通知可用 BroadcastChannel;需要互斥可评估 Web Locks,但锁不保证进程崩溃后的业务一致性。支付、库存、权限和不可丢失编辑仍必须把最终事务放在服务端,并使用版本号、幂等键或冲突合并协议。
状态寿命与平台 API 决策表
| 状态或工作 | 内存 | URL | localStorage | IndexedDB | Worker |
|---|---|---|---|---|---|
| 当前对话框 | 首选 | 不必要 | 不必要 | 不必要 | 不适用 |
| 可分享查询 | 可作运行副本 | 首选 | 会产生双真值 | 过重 | 不适用 |
| 主题偏好 | 降级 | 不适合 | 首选 | 过重 | 不适用 |
| 大型离线目录 | 缓存当前视图 | 不适合 | 同步且容量有限 | 首选 | 可辅助计算 |
| 4,100 条纯搜索 | 保存结果 | 保存查询 | 不适合 | 可选数据源 | 经测量后可选 |
平台 API 与框架不是同层替代品。框架最终仍调用 Fetch、History、Storage 和 DOM;先理解宿主契约,才能判断框架封装是在减少重复还是隐藏关键错误和生命周期。
请求、存储与 Worker 验收练习
- 分别模拟网络失败、HTTP 500、无效 JSON 和字段错误,核对四种状态。
- 快速发起两次搜索,取消第一次并确认取消不显示为网络故障。
- 用 URL 恢复查询、专栏和四项选择,再用后退/前进验证状态同步。
- 禁用站点存储,确认主题回退内存且目录仍可用。
- 比较 41 条与 4,100 条搜索的主线程、Worker 耗时和输入响应。
- 为同一需求分别尝试容器查询与 ResizeObserver,只保留更小方案。
- 用 Performance mark/measure 记录加载和渲染,不上传敏感参数。
- 审查页面 CSP、CORS、外链和跨窗口消息边界。
验收标准:能从请求、策略、解析、验证到渲染逐层定位失败;能按状态寿命选择存储;能解释 Worker/Observer 的成本与降级,而不是把所有浏览器 API 都当优化开关。
Web 平台规范与测量资料
- Fetch Standard:Request、Response、CORS、取消与 fetching 算法;核验于 2026-06-30。
- HTML Web Application APIs:事件循环、Window 与脚本宿主集成;核验于 2026-06-30。
- HTML Web Workers:Worker 全局、消息与生命周期;核验于 2026-06-30。
- URL Standard:URL 解析与序列化;核验于 2026-06-30。
- Indexed Database API:事务式浏览器数据库;核验于 2026-06-30。
- Performance Timeline:性能条目与 Observer;核验于 2026-06-30。
- Content Security Policy Level 3:内容加载与脚本执行策略;核验于 2026-06-30。