2026/1/12 3:48:49
网站建设
项目流程
产品推广的网站怎么做,福州网站营销,asp.net企业网站后台管理系统,微网站开发难度流式SSR就是一种渐进式渲染#xff0c;在传统的页面加载流程是#xff1a;请求 → 等待 → 渲染。而渐进式渲染的思路是#xff1a;立即展示缓存的页面快照#xff08;即使是旧内容#xff09;后台请求最新的页面内容无缝替换为最新内容这样用户感知到的加载时间接近于零在传统的页面加载流程是请求 → 等待 → 渲染。而渐进式渲染的思路是立即展示缓存的页面快照即使是旧内容后台请求最新的页面内容无缝替换为最新内容这样用户感知到的加载时间接近于零体验类似于原生 App。前面笔者的文章中提到关于H5页面的快照是客户端做的。本篇文章讲述一种基于 Service Worker 的渐进式渲染方案的原理简单来讲就是将客户端的工作挪到了service worker中。通过给站点开启一个后台运行的service workerservice worker可以独立于webview运行在后台在service worker中劫持包括主文档在内的网络请求对文档内容进行存储并修改返回。技术方案设计整体架构┌─────────────┐ │ 用户访问 │ └──────┬──────┘ │ ▼ ┌─────────────────┐ │ Service Worker │ ◄─── 拦截请求 └────┬────────┬───┘ │ │ │ └─────────┐ ▼ ▼ ┌─────────┐ ┌──────────┐ │ 缓存快照 │ │ 网络请求 │ └────┬────┘ └─────┬────┘ │ │ └────────┬────────┘ ▼ ┌─────────────┐ │ 流式替换 │ └─────────────┘核心代码实现1. HTML 页面注册 Service Worker!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title渐进式渲染示例/title /head body h1Hello World/h1 script data-snapshot (function () { const swEnabled location.search.indexOf(x-swfalse) 0; // 注册 Service Worker swEnabled navigator.serviceWorker navigator.serviceWorker.register(/sw.js) .then(function (registration) { console.log(Service Worker 注册成功:, registration); }) .catch(function (error) { console.log(Service Worker 注册失败:, error); }); // 如果禁用则注销 Service Worker !swEnabled navigator.serviceWorker navigator.serviceWorker.getRegistration(location.href).then((r) { r r.unregister(); }); }()); /script /body /html关键点说明data-snapshot属性标记这是快照阶段需要保留的脚本支持通过?x-swfalse参数禁用 Service Worker禁用时会自动注销已注册的 Service Worker2. Service Worker 核心逻辑// sw.js self.addEventListener(fetch, (event) { // 只拦截主文档请求 if (event.request.destination ! document) { return; } // 支持禁用功能 if (event.request.url.indexOf(x-swfalse) 0) { event.waitUntil(caches.delete(my-cache)); return; } event.respondWith(handleFetch(event.request)); }); self.addEventListener(install, (event) { console.log(Service Worker 安装); self.skipWaiting(); // 立即激活 });3. 脚本过滤策略function replaceScripts(text, regularStream) { return text.replace(/script\b[^]*(?:(?!\/script)[^]*)*\/script/gi, (match) { // 快照阶段只保留 data-snapshot 脚本 // 正式阶段只保留普通脚本 if (match.indexOf(data-snapshot) 0) { return regularStream ? : match; } return regularStream ? match : ; }); }为什么要过滤脚本快照阶段避免执行业务逻辑脚本可能依赖未加载的资源正式阶段避免重复执行初始化脚本4. 流式渲染核心function withSnapshot(snapshot, request) { return new Response(new ReadableStream({ start(controller) { const encoder new TextEncoder(); const decoder new TextDecoder(); // 第一步立即输出快照 controller.enqueue(encoder.encode(snapshot)); let firstStream true; // 第二步请求最新内容 fetchAndStore(request).then((response) { const reader response.body.getReader(); function push() { reader.read().then(({ done, value }) { if (done) { controller.close(); return; } if (firstStream) { firstStream false; // 第三步清空页面 controller.enqueue(encoder.encode( scriptdocument.head.innerHTML ;document.body.innerHTML ;/script )); // 第四步注入最新内容 const text decoder.decode(value); const head text.match(/head([\s\S]*?)\/head/i); const body text.match(/body([\s\S]*?)\/body/i); if (head body) { controller.enqueue(encoder.encode( scriptdocument.head.innerHTML ${head[1].trim().replace(/\n/g, )}/script )); controller.enqueue(encoder.encode(replaceScripts(body[1], true))); } } else { controller.enqueue(value); } push(); }); } push(); }); }, })); }为什么要清空 DOM快照内容和最新内容可能结构不同直接追加会导致内容重复清空后重新注入确保页面状态一致为什么用 innerHTML 注入流式响应中我们无法直接操作 DOM只能通过推送script标签让浏览器执行 JavaScriptinnerHTML是最简单的 DOM 替换方式5. 缓存管理Service Worker 的缓存存储在 Cache Storage API 中这是浏览器提供的专门用于 Service Worker 的持久化存储空间。实际上不需要关心物理位置因为浏览器完全管理这些文件。function fetchAndStore(request) { return fetch(request) .then((networkResponse) { if (networkResponse.ok) { // 克隆响应用于缓存 const cacheResponse networkResponse.clone(); caches.open(my-cache).then((cache) { cache.put(request, cacheResponse); }); } return networkResponse; }); } function handleFetch(request) { return caches.match(request) .then((response) { if (response) { // 有缓存先展示快照再更新 return readResponseText(response).then((snapshot) { return withSnapshot(snapshot, request); }); } // 无缓存直接请求 return fetchAndStore(request); }); }为什么要 clone 响应Response 对象的 body 只能读取一次流的特性需要一份给缓存一份给浏览器clone()创建独立的副本工作流程详解首次访问无缓存用户访问 → Service Worker 拦截 → 无缓存 → 网络请求 → 返回内容 → 存入缓存二次访问有缓存用户访问 ↓ Service Worker 拦截 ↓ 读取缓存快照去除普通脚本 ↓ 立即返回快照内容 ← 用户看到页面 ↓ 后台发起网络请求 ↓ 清空 DOM ↓ 注入最新 head 和 body ↓ 更新缓存注意事项上述只讲述了该方案的基本原理实际应用要考虑更多的因素如App 环境兼容性、缓存策略、基础设施依赖等下面是方案对比维度客户端方案Service Worker 方案首次访问拦截✅ 可以拦截❌ 无法拦截跨平台能力❌ 需要各端适配✅ Web 标准通用更新速度⚠️ 需要发版✅ 实时生效开发成本⚠️ 需要端上开发⚠️ 需要 Web 开发维护成本❌ 多端维护✅ 单一维护灵活性⚠️ 受限于客户端版本✅ 完全可控降级能力⚠️ 需要发版回滚✅ 秒级降级总结如果你的业务是纯 Web 应用PWA) → Service Worker 是最佳选择如果你的业务在 App 内 → 优先考虑客户端方案