真理社游记:用Service Worker实现离线优先的社团页面
之前看到“真理社游记”这个题材时我第一反应不是动画剧情本身而是那个非常有意思的设定冲突一群擅长信息技术、日常离不开网络的角色突然陷入没有 WiFi 的一天会发生什么站在 Web 开发的角度这个场景其实是一个很经典的工程问题当用户的网络不可用时我们能不能保证页面依然可用本文就从“真理社游记”这个灵感出发带大家实现一个离线优先Offline First的社团主题网页。页面会模拟“网瘾少女断网的一天”通过 Service Worker 和 Cache API 做静态资源缓存让大家在完全没有 WiFi、没有任何网络的环境下依然能打开页面并看到完整内容。这篇文章适合以下读者刚接触 PWA渐进式 Web 应用和离线缓存的前端初学者。想搞清楚 Service Worker 到底怎么注册、怎么缓存、怎么更新的同学。想做个人站点、社团站点、校园项目又担心弱网环境下体验太差的新手。想了解离线优先工程实践但不想看一堆抽象文档的开发者。读完本文后你能掌握Service Worker 的基本生命周期、Cache API 的核心用法、Manifest 配置方式以及如何通过 Chrome DevTools 模拟离线环境完成验证。整个项目不依赖任何框架纯 HTML CSS JavaScript 就能跑起来。1. 背景与核心概念1.1 从“真理社游记”到离线网页项目“真理社”给人一种技术社团的印象成员擅长信息处理、代码调试、网络攻防。这样一群人如果遇到没有 WiFi 的断网日按理说会陷入“无事可做”的状态。但从开发者角度来看真正优秀的 Web 应用不应该把命运完全交给网络。围绕“网瘾少女与没有 WiFi 的一天”我们可以做一个非常贴近场景的小项目一个社团游记页面第一次从网络打开后将所有的 HTML、CSS、JavaScript、图片资源缓存到浏览器本地。之后哪怕断网页面也能从本地缓存中读取资源继续正常渲染。这个思路再往前一步就是 PWA 的核心能力之一离线可访问。1.2 什么是离线优先离线优先并不是说“不要网络”而是指在开发时优先考虑离线场景确保网络断开后应用核心功能不失效。传统网页的加载过程是浏览器向服务器发起请求。服务器返回 HTML。浏览器解析 HTML再请求 CSS、JS、图片等资源。所有资源加载完成后页面才能完整显示。如果第二步或第三步断网页面就会白屏或报错。离线优先的加载过程则是浏览器首次访问时Service Worker 在后台缓存核心资源。后续每次请求Service Worker 拦截请求先看缓存中是否已有资源。有缓存就直接返回缓存没有缓存才请求网络。网络断开时依然可以返回已缓存的资源。这个模式非常适合社团介绍页、个人博客、文档站点、活动宣传页等内容更新频率不高、但需要稳定访问的网站。1.3 为什么选择 Service WorkerService Worker 是浏览器提供的独立于网页主线程的 JavaScript 运行环境。它相当于“浏览器和服务器之间的中间层”可以拦截页面发出的所有网络请求并决定如何处理。选择 Service Worker 的原因有三点网络请求可控我们可以把请求导向缓存也可以导向网络还可以在网络失败时回退到缓存。支持后台更新Service Worker 可以在页面不刷新时预取和更新资源为下一次访问做好准备。离线兜底能力强配合 Cache API能实现精细的缓存策略而不只是依赖浏览器默认的内存缓存或磁盘缓存。与传统的 AppCache已被废弃相比Service Worker 更灵活、更安全是当前实现离线功能的推荐方案。1.4 “网瘾少女没有 WiFi 的一天”场景解读我们做这个页面其实是在还原一个场景故事早上页面正常联网打开资源被缓存。中午网络断开网瘾少女打开同一个页面。页面没有白屏没有加载失败所有布局、文字、图片依然正常展示。页面顶部出现“当前网络状态离线”的提示但她依然可以浏览社团游记。这就是一个标准的“离线可用”演示。如果你要把它当作课程设计、社团官网或前端练习项目也非常合适。2. 环境准备与版本说明2.1 开发环境清单这个项目不需要复杂环境只要有一个现代浏览器和顺手代码编辑器即可。工具说明操作系统Windows / macOS / Linux 均可代码编辑器VS Code、WebStorm、Sublime Text 均可浏览器Chrome / Edge 最新版推荐 Chrome本地服务器VS Code Live Server 插件或 Node.js 手动启动Node.js可选用于启动静态服务器版本建议 14 以上本文示例以常见环境为例重点演示配置思路。不同版本带来的差异不大核心 API 在主流现代浏览器中已经稳定。2.2 为什么需要本地服务器Service Worker 有一个限制只能在安全上下文HTTPS 或 localhost下运行。我们不可能用file://协议直接双击 HTML 打开那样 Service Worker 不会生效。所以我们需要启动一个本地静态服务器。最简单的做法是安装 VS Code 的 Live Server 插件鼠标右键点击index.html选择 “Open with Live Server”。也可以使用 Node.js 写一个极简静态服务器例如npx serve .然后在浏览器中访问http://localhost:3000具体端口以终端输出为准。2.3 版本注意点navigator.serviceWorker.register()方法所有现代浏览器都支持。Cache API 是 Promise 风格注意异步处理的错误回调。不同浏览器的缓存容量、缓存淘汰策略略有差异但本文用到的基础功能完全兼容。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。3. 核心原理拆解3.1 Service Worker 生命周期Service Worker 的生命周期可以分成四个阶段注册、安装、激活、拦截。注册Register页面加载完成后通过navigator.serviceWorker.register(./sw.js)注册 Service Worker。安装Install浏览器下载sw.js并触发install事件。通常在这里做预缓存把核心资源提前放入 Cache。激活Activate安装成功后进入激活阶段触发activate事件。通常在这里清理旧版本缓存。拦截Fetch激活完成后Service Worker 开始拦截页面请求在fetch事件中决定返回缓存还是请求网络。第一次访问页面时Service Worker 可能需要等到第二次刷新才会完全控制页面。这是因为首次加载时页面已经走完请求流程Service Worker 还没来得及拦截。在调试时建议注册成功后刷新一次。3.2 Cache API 与缓存策略Cache API 是浏览器提供的缓存接口可以将Request和Response对象缓存到浏览器中。核心方法如下方法作用caches.open(name)打开一个命名缓存不存在则创建cache.addAll(urls)批量请求并缓存资源cache.add(url)请求并缓存单个资源cache.put(request, response)手动将请求和响应写入缓存cache.match(request)从缓存中匹配请求caches.match(request)在所有缓存中匹配请求cache.delete(request)删除指定缓存项常用的缓存策略有三种Cache First缓存优先直接返回缓存缓存中没有才请求网络。适合不经常变化的静态资源例如 CSS、JS、图片。Network First网络优先先请求网络网络失败再回退缓存。适合需要保证内容最新的页面例如文章详情页。Stale While Revalidate过期缓存同时后台更新先返回缓存同时在后台请求网络并更新缓存。适合内容实时性要求不高的场景。本文中的示例会采用“缓存优先 网络兜底”的组合方式。3.3 Web App Manifest 基础manifest.json是 PWA 的配置文件它可以让网页“像 App 一样”被添加到手机桌面并控制启动方式、图标、主题色等。{ name: 真理社游记, short_name: 真理社游记, start_url: ./index.html, display: standalone, background_color: #1a1a2e, theme_color: #1a1a2e }关键字段含义name应用全名。short_name桌面图标下方显示的短名。start_url启动时打开的页面地址。display展示模式standalone表示独立窗口隐藏浏览器地址栏。theme_color浏览器工具栏和任务栏颜色。background_color启动时的背景色。Manifest 本身不负责离线缓存但它能让网页的“安装体验”更完整是 PWA 的重要组成部分。如果要做到真正的“没有 WiFi 也能像 App 一样打开”还需要同时配置好 Service Worker 和 Manifest。3.4 离线页面显示逻辑离线显示并不是“完全不用网络”而是通过 Service Worker 在请求发出前截获并返回缓存。浏览器层面的流程是页面发起请求 ↓ Service Worker 拦截 ↓ 点击判断是否命中缓存 ├── 是 → 返回缓存资源 └── 否 → 发起真实网络请求 ├── 成功 → 写入缓存并返回 └── 失败 → 返回兜底页面 index.html这样即使在断网状态下已经缓存的资源也能正常返回用户看到的效果和在线时几乎一致。4. 完整实战建设“真理社游记”离线网页4.1 创建项目目录结构为了方便管理我们按下面的结构组织项目veritas-travel/ ├── index.html ├── manifest.json ├── sw.js ├── css/ │ └── style.css ├── js/ │ └── app.js └── images/ ├── icon-192.png └── icon-512.pngimages目录下的图标文件可以用任意 192x192 和 512x512 的 PNG 图片代替如果暂时没有图标可以先不引用或直接删除 manifest 中的icons字段。4.2 编写 index.html文件路径index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title真理社游记 · 没有WiFi的一天/title meta namedescription content一个受《真理社游记》启发的离线优先社团页面演示 Service Worker 离线缓存。 link relmanifest hrefmanifest.json meta nametheme-color content#1a1a2e link relstylesheet hrefcss/style.css /head body header classhero div classhero-overlay/div div classhero-content p classtaglineVeritas 真理社/p h1没有 WiFi 的一天/h1 p classsubtitle网瘾少女的离线生存手册 · 离线页面也能正常打开/p /div /header main classcontainer section classcard intro h2今天的游记主题/h2 p网络断开后原本需要在线访问的内容是不是就完全不可用了其实如果我们在开发时预先把静态资源放入浏览器缓存即使没有 WiFi也能正常浏览已经加载过的页面。这就是本文要实现的“离线优先”能力。/p p idnetwork-status classstatus-tip正在检测网络状态…/p /section section classcard gallery h2社团相册/h2 div classimg-grid div classimg-item图片 1/div div classimg-item图片 2/div div classimg-item图片 3/div /div /section section classcard article h2网瘾少女的离线日志/h2 p早上九点教室里的 WiFi 突然断连。对于随时需要查资料、同步代码、上传素材的社团来说这本来应该是一场灾难。但因为项目已经完成 Service Worker 离线缓存所有静态页面和图片都能继续访问工作流几乎没有中断。/p /section /main footer p真理社游记 · 离线网页示例 · powered by Service Worker/p /footer script srcjs/app.js/script /body /html页面结构说明header classhero是首屏横幅模拟动画或海报风格的视觉焦点。main里面放了三个卡片区域分别是游记介绍、社团相册和离线日志。底部的#network-status元素会由app.js动态更新网络状态。尾部的script用于加载我们的逻辑脚本。这个页面本身没有写死任何网络请求所有资源都是相对路径方便 Service Worker 进行精确匹配。4.3 编写样式文件路径css/style.css/* 全局基础 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Noto Sans SC, Microsoft YaHei, sans-serif; background: #f4f6fb; color: #222; line-height: 1.7; } /* 首屏横幅 */ .hero { position: relative; height: 320px; display: flex; align-items: center; justify-content: center; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); color: #fff; overflow: hidden; } .hero-overlay { position: absolute; inset: 0; background: radial-gradient(circle at 70% 50%, rgba(64, 156, 255, 0.25), transparent 60%); } .hero-content { position: relative; z-index: 1; } .tagline { letter-spacing: 4px; font-size: 14px; opacity: 0.8; } .hero h1 { font-size: 42px; margin: 8px 0; } .subtitle { opacity: 0.85; } /* 内容容器 */ .container { max-width: 960px; margin: 24px auto; padding: 0 16px; display: grid; gap: 20px; } .card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); } .card h2 { font-size: 20px; margin-bottom: 12px; border-left: 4px solid #409cff; padding-left: 10px; } /* 网络状态提示 */ .status-tip { margin-top: 12px; padding: 10px 14px; border-radius: 8px; font-weight: 600; } .status-tip.online { background: #e7f7ec; color: #1e7a3c; } .status-tip.offline { background: #fff3e0; color: #b26a00; } /* 相册 */ .img-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } .img-item { aspect-ratio: 4 / 3; background: linear-gradient(145deg, #d9e8ff, #b3d4ff); border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #2a5caa; font-weight: 600; } /* 页脚 */ footer { text-align: center; padding: 20px; color: #777; font-size: 14px; }这段样式重点做了三件事构建了一个学园风格的首屏横幅。用卡片结构组织游记内容。定义在线、离线两种状态下#network-status的不同颜色。aspect-ratio可以让图片区域在保持比例的响应式布局中依然整齐不需要额外写高度。4.4 添加交互 JavaScript文件路径js/app.js// 文件路径js/app.js window.addEventListener(load, () { // 注册 Service Worker if (serviceWorker in navigator) { navigator.serviceWorker.register(./sw.js) .then(reg { console.log(Service Worker 注册成功, reg.scope); }) .catch(err { console.warn(Service Worker 注册失败, err); }); } else { console.warn(当前浏览器不支持 Service Worker); } // 检测网络状态并显示提示 function updateNetworkStatus() { const statusEl document.getElementById(network-status); if (navigator.onLine) { statusEl.textContent 当前网络状态在线; statusEl.classList.remove(offline); statusEl.classList.add(online); } else { statusEl.textContent 当前网络状态离线 (你已进入“没有 WiFi 的一天”); statusEl.classList.remove(online); statusEl.classList.add(offline); } } window.addEventListener(online, updateNetworkStatus); window.addEventListener(offline, updateNetworkStatus); updateNetworkStatus(); });这里每一段逻辑都对应一个明确目标window.addEventListener(load, ...)保证在页面资源加载完成后注册 Service Worker避免阻塞首次渲染。if (serviceWorker in navigator)做能力判断老浏览器或特殊环境会优雅降级不影响页面正常展示。navigator.serviceWorker.register(./sw.js)是注册入口。navigator.onLine是浏览器自带的网络状态属性结合online和offline事件可以实时更新页面顶部提示。4.5 编写 Service Worker文件路径sw.js// 文件路径sw.js const CACHE_NAME veritas-travel-v1; const CACHE_ASSETS [ ./, ./index.html, ./css/style.css, ./js/app.js, ./manifest.json ]; // 安装阶段预缓存核心资源 self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME) .then(cache cache.addAll(CACHE_ASSETS)) .then(() self.skipWaiting()) ); }); // 激活阶段清理旧缓存 self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then(cacheNames { return Promise.all( cacheNames .filter(name name ! CACHE_NAME) .map(name caches.delete(name)) ); }).then(() self.clients.claim()) ); }); // 请求阶段优先缓存离线兜底 self.addEventListener(fetch, (event) { if (event.request.method ! GET) return; event.respondWith( caches.match(event.request).then(cachedResponse { if (cachedResponse) { return cachedResponse; } return fetch(event.request) .then(response { // 只缓存同源地址避免缓存跨域资源引发安全问题 const url new URL(event.request.url); if (self.location.origin url.origin) { const clone response.clone(); caches.open(CACHE_NAME).then(cache cache.put(event.request, clone)); } return response; }) .catch(() caches.match(./index.html)); }) ); });这段逻辑包含三个重要细节安装阶段用addAll批量缓存addAll只要有一个资源请求失败整个安装过程就会失败。所以CACHE_ASSETS里的路径必须真实存在不能写错。激活阶段清理旧缓存当CACHE_NAME从v1升级到v2时旧缓存会被自动清理避免缓存越来越多。fetch 阶段做“缓存优先”对于图片、CSS、JS 等以静态资源为主的页面缓存优先能显著提升加载速度。当请求失败且没有缓存时最后回退到index.html保证单页应用或详情页在离线时也能显示框架。需要注意的是fetch事件中的self.location.origin url.origin判断非常关键。如果不加这个判断Service Worker 可能会缓存跨域资源存在引入不可信数据的风险。实际项目中也可以在fetch请求时检查response.type只缓存basic类型响应。4.6 配置 manifest.json文件路径manifest.json{ name: 真理社游记 · 没有WiFi的一天, short_name: 真理社游记, start_url: ./index.html, display: standalone, background_color: #1a1a2e, theme_color: #1a1a2e, icons: [ { src: images/icon-192.png, sizes: 192x192, type: image/png }, { src: images/icon-512.png, sizes: 512x512, type: image/png } ] }如果没有准备好图标文件可以先移除icons字段manifest.json依然可以发挥作用。安装图标只是增强体验不影响离线缓存逻辑。4.7 运行与验证完成上面所有文件后通过 Live Server 或其他本地服务器打开页面。预期的正常运行顺序是浏览器请求index.html。页面加载后注册sw.js。Service Worker 安装自动缓存首页、CSS、JS 和 Manifest。控制台输出Service Worker 注册成功。刷新一次页面让 Service Worker 完全控制当前页面。接下来验证离线能力打开 Chrome DevToolsF12。切到 Network 面板。在 Throttling 下拉框中选择Offline。刷新页面。观察页面是否还能正常显示。如果一切正常你会看到页面依然完整渲染并且控制台不会出现资源加载失败的提示。顶部状态栏会显示“当前网络状态离线”。为了更直观也可以在断网前打开页面然后断开电脑的 WiFi 或拔掉网线再切换标签页效果一样。除了手动模拟还可以在 Chrome 的 Application 面板中查看Application → Service Workers可以看到 Service Worker 的注册状态和激活状态。Application → Cache Storage可以看到名为veritas-travel-v1的缓存及内部资源列表。Application → Manifest可以预览 PWA 安装信息。4.8 结果说明完成验证后我们需要明确一个结论离线可用的前提是资源已经在上一次在线访问时被成功缓存。如果用户在完全离线的情况下第一次打开页面那么缓存中没有任何资源Service Worker 也还没来得及安装页面依然无法加载。所以离线优先方案更适合“先在线访问过再离线使用”的场景。为了进一步改善首次访问体验可以考虑在项目上线前用脚本预缓存核心资源但这属于进阶优化本文不展开。5. 常见问题与排查思路在实现离线缓存时新手最容易遇到下面这些问题。问题现象常见原因解决思路navigator.serviceWorker是 undefined当前环境不是 HTTPS 或 localhost改用本地服务器启动项目并确认地址是 localhostService Worker 注册成功但页面不缓存首次加载时 Service Worker 还未控制当前页面注册成功后刷新一次页面修改了 CSS/JS 但刷新后依然是旧内容缓存策略是 Cache First缓存未失效更新CACHE_NAME版本号或清理 Cache StorageaddAll安装失败列表中有资源路径错误或资源不存在在 Network 面板中逐个检查资源请求状态离线时页面能打开但样式丢失CSS 没有被缓存或路径与注册地址不一致确认CACHE_ASSETS中css/style.css路径正确控制台出现“Unexpected token”sw.js中使用了当前浏览器不支持的语法检查浏览器版本或避免使用太新的 JS 特性部署到服务器后离线无效静态服务器未正确返回Content-Type或sw.js不在项目根目录将sw.js放在站点根目录并检查服务器 MIME 配置缓存无限增长版本号未更新旧缓存没有被清理在activate事件中清理非当前CACHE_NAME的缓存这里单独强调一个最容易忽略的问题Service Worker 的更新并不像普通脚本那样即时生效。即使你修改了sw.js并刷新页面浏览器通常会先让新版本 Service Worker 进入 waiting 状态等旧版本控制的页面全部关闭后新版本才会接管。如果开发时需要强制更新可以这样操作打开 Chrome DevTools → Application → Service Workers。勾选 “Update on reload”。点击 “Unregister”清除旧注册信息。再次刷新页面。这样做可以避开调试时“改了代码不生效”的怪圈。6. 最佳实践与工程建议6.1 明确缓存边界不是所有资源都适合缓存。以下资源一般不建议缓存用户私密数据例如个人设置、订单信息。动态接口返回的实时数据。带敏感信息的跨域资源。在sw.js中建议通过请求 URL、请求方法、响应内容类型做精细判断。例如if (event.request.url.includes(/api/)) { return; // 不拦截接口请求 }这样可以避免把用户敏感数据缓存到本地也能降低调试接口时出现“缓存导致数据不更新”的概率。6.2 制定缓存版本策略CACHE_NAME是缓存更新最重要的手段。每当你更新了 CSS、JS 或页面结构建议同步修改版本号。例如const CACHE_NAME veritas-travel-v2;这样激活阶段就会清理v1的旧缓存避免新老资源混用。如果项目更新频繁可以把版本号和构建时间结合const CACHE_NAME veritas-travel-v2-20241201;命名越明确排错越轻松。6.3 优雅降级与报错提示Service Worker 本身是渐进增强能力不支持的时候页面应该照常工作。因此注册代码一定要做能力判断并且不要因为注册失败而报全局错误。在线状态提示也应该友好。例如在离线时提供“你正在查看离线缓存内容”的提示让用户知道当前内容可能不是最新版本。if (!navigator.onLine) { statusEl.textContent 当前为离线状态正在展示缓存内容; }6.4 性能优化离线缓存本身就能提升二次访问速度。在此基础上还有几个优化方向把公共样式、公共脚本单独拆分方便复用缓存。图片资源使用合适的尺寸避免缓存大尺寸原图。对需要使用网络但又不是核心内容的模块采用延迟加载。在install阶段只预缓存首屏资源其余资源在请求命中后再缓存。6.5 安全边界与最小权限Service Worker 拥有拦截所有同源请求的能力因此需要注意只缓存同源或信任的第三方 CDN 资源。不将包含 Cookie、Token 的接口响应写入 Cache。对fetch事件中的请求做基本校验。生产环境建议通过 HTTPS 部署避免中间人篡改缓存内容。此外多项目部署时CACHE_NAME最好带上项目前缀避免同域下多个缓存互相干扰。6.6 开发调试流程建议在实际开发中推荐按照下面流程维护离线功能先在 Network 面板关闭缓存Disable cache验证在线模式功能正常。再开启 Service Worker 离线缓存验证二次加载速度。切换到 Offline 模式验证离线兜底页面。修改代码后先更新CACHE_NAME再刷新验证。部署到测试环境后至少模拟一次从“在线访问”到“断网使用”的完整流程。7. 总结与学习路线这个“真理社游记”离线网页项目主要让大家掌握了一套完整的离线优先实现思路。我们完成了搭建一个语义明确的 HTML 社团页面。通过 Service Worker 注册、安装、激活、拦截四个阶段管理缓存。通过 Cache API 将静态资源缓存到本地。通过 manifest.json 让页面具备 PWA 基础能力。通过 Chrome DevTools 离线模拟完成验证。对新手来说下一步可以继续学习PWA 的安装与桌面快捷方式配置。更复杂的缓存策略例如 Network First 和 Stale While Revalidate。IndexedDB用于离线场景下的结构化数据存储。前端构建工具中自动生成 SW 的插件例如 Workbox。发送离线通知结合 Service Worker 的后台能力。实际项目应用时优先关注三件事缓存版本控制、敏感数据隔离、离线场景下的用户提示。这三件事处理好了离线体验就不会出大问题。最后提一个可以动手扩展的小玩法在页面中加入一个“WiFi 断开倒计时”的小组件当检测到离线时显示“网瘾少女的离线生存时间”用setInterval每秒更新恢复在线后自动清零。这样就能把“没有 WiFi 的一天”这个故事场景做得更有代入感同时加深对online/offline事件的理解。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →