资讯动态

Uniapp + PWA 离线文档工具实现:Service Worker 缓存与更新策略

发布时间:2026/10/8 14:43:54 来源:尧图企业网站定制
1. 场景复盘为什么文档类工具是离线化的最佳试验田Uniapp PWA 这个组合最打动我的地方是它能让离线文档工具在断网环境下依旧打开同时把缓存策略和内容同步这两件事落到具体代码里。我最初做这个项目是给一套企业内部产品写在线帮助文档。产品本身的 Web 端、小程序端都已经用 Uniapp 维护文档模块自然也想复用同一套代码。但文档工具有一个非常硬的诉求用户可能在电梯里、高铁上、客户现场打开帮助页网络状况完全不可控。要是文档页面一断网就白屏那这个工具基本等于摆设。1.1 现场演示翻车与“离线优先”需求我真正被刺激到是一次现场演示。客户那边会议室 WiFi 时好时坏我点开一个 API 参考页面等了十几秒浏览器弹出一个“无法连接到服务器”的默认错误页。当时只能尴尬地切到手机热点页面才刷出来。演示结束以后我就想文档这种内容命中率其实非常集中用户翻来翻去就是首页、目录、最近更新的几篇、搜索命中那几篇。真正需要的是“第一次打开过之后第二次即使断网也能看”。离线文档工具的价值就在这不是要求所有内容都塞进本地而是把用户实际会反复看的那些内容用可靠策略缓存下来让阅读行为不依赖网络。和视频、直播这类强实时业务不同文档是静态内容为主更新频率低新鲜度要求也没那么敏感天然适合做离线。产品手册、API 参考、帮助中心、内部知识库都属于这一类。1.2 为什么选 Uniapp PWA 而不是纯 Web 或原生 App很多人会问直接用纯 Web PWA 不就行了为什么还要套一层 Uniapp这个问题我实际操作下来感受很深。如果项目只需要 H5纯 Web 确实更轻。但我们团队已经有 Uniapp 的小程序和 App 端文档模块如果单独用纯 Web 写一遍以后维护成本就是三套。Uniapp 统一了 Vue 语法和路由H5 端编译出来本身就是一个标准 SPA完全有能力挂上 Service Worker这是它能做 PWA 的前提。另外原生 App 离线存储方案能力更强比如把文档写入 SQLite 或者本地文件但上架、打包、审核的链路比 PWA 重得多。PWA 的优势是你还没有把它当成一个 App 去分发用户只要用 HTTPS 访问过一次浏览器就能把它变成可安装、可离线的应用。对工具型产品来说这个转化成本极低。这里还要提一个很容易混的点Uniapp 和 Uniapp X。如果你是新项目要搞清楚这两者差异。Uniapp 目前仍然是 Vue 3 Vite 这套生态H5 端可以用成熟的前端 PWA 方案Uniapp X 是另一条基于 UTS 的跨端技术栈它强调原生渲染性能但 H5 端的兼容特性不能默认等价。我这个项目记录的是 Uniapp Vue 3 Vite 的落地链路如果你用的是 Uniapp X关键能力必须单独在目标浏览器里验证。1.3 离线范围控制不是把整个站点都塞进浏览器缓存做离线工具最忌讳的就是“离线即全量”。文档站点可能几百篇甚至几千篇文章把全部 Markdown、图片、附件都预缓存首屏加载会变得非常慢用户第一次访问体验直接崩。我建议把离线范围分三层来控制。第一层是应用外壳也就是 JS、CSS、HTML、LOGO 这类站点运行必需的资源必须离线可用。第二层是核心文档内容只缓存用户实际访问过的文档正文和目录索引没访问过的内容等用户点开时再按策略加入缓存。第三层是做容量上限缓存超过一定条目或大小就淘汰最久未用的数据。这个思路和控制浏览器缓存有点像都是空间换时间但要有一个明确的边界。2. Service Worker 上岗前必须想清楚的几个问题Service Worker 是 PWA 离线能力的核心但很多从普通 Web 开发转过来的同学容易拿它和浏览器 HTTP 缓存做对比。这里要强调一个认知Service Worker 不是替代浏览器缓存它是一个位于页面和网络之间的独立线程可以拦截页面发出的请求再由我们自己决定请求是走网络还是走本地缓存。2.1 Service Worker 的生命周期和角色Service Worker 主要有三个事件install、activate、fetch。install 发生在第一次注册安装适合把基础资源写入缓存比如应用外壳activate 在安装完成后触发适合清理旧版本缓存fetch 则是在任何页面请求发出时触发我们在这里写缓存策略。理解生命周期特别重要因为很多“改了代码不生效”的坑其实都出在生命周期上。安装了一个新版本 Service Worker 后它不会立刻控制当前页面而是进入 waiting 状态只有当前页面刷新并且旧的 Service Worker 释放后新的才会接管。如果不处理用户可能连续刷新几次都还在跑旧逻辑感觉就像线上没发布一样。我通常这样给团队解释Service Worker 就像一个快递柜管理员。用户请求内容时管理员可以决定这个包裹是刚从仓库取的还是直接从柜子里拿给你的。管理员换班时老管理员要把手里正在处理的活儿交代清楚新管理员才能正式接手。这个“交接”过程就是 activate 和 waiting 的关系。2.2 在 Uniapp H5 中注册 Service Worker 的正确姿势Uniapp 的页面只会编译成 H5、小程序、App 中对应的产物。Service Worker 只有 H5 端存在小程序和 App 里没有 navigator 这个概念所以注册代码必须用条件编译包起来。以 Vue 3 Vite 的 Uniapp 项目为例通常放在 main.ts 里// #ifdef H5 if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js).then((reg) { console.log(ServiceWorker registered:, reg.scope); }).catch((err) { console.warn(ServiceWorker registration failed:, err); }); }); } // #endif这里要注意两个细节。第一注册时机放在 window.load 事件里不阻塞首屏渲染。文档工具的场景允许页面先加载Service Worker 慢慢就位。第二sw.js 的路径决定了它的作用域放在站点根目录就默认控制整个站点。如果放在 /docs/sw.js那只能控制 /docs 下面的请求。2.3 HTTPS 不是可选项是硬前提Service Worker 只能在安全上下文中运行也就是 HTTPS 页面或者 localhost。所以本地调试还算方便Chrome 会默认把 localhost 当成安全上下文。但一旦上线必须保证网站有有效的 HTTPS 证书。我之前踩过一个小坑测试环境用 IP 地址加端口访问Service Worker 始终注册失败浏览器控制台只提示“不支持的上下文”换成域名加证书后一切正常。另外manifest 文件也一样。PWA 的可安装性依赖 Web App Manifest虽然这个文件本身不一定强制 HTTPS但整个页面必须是安全上下文否则后面所有能力都白搭。Uniapp 项目通过 manifest.json 配置应用信息但 H5 端真正使用的 PWA manifest是部署到静态服务器上的 webmanifest 文件这个我在第 5 部分再详细展开。3. 缓存策略分层静态资源、文档正文、接口数据各回各家缓存策略是整个离线文档工具里最容易被低估的部分。很多初版实现就一句话把所有 GET 请求都 Cache First文档倒是能离线打开了但内容更新后用户永远看到旧版本。更合理的做法不是一概而论而是按照资源的性质分层处理。3.1 分层决策和三种基础策略我习惯把文档工具的请求分成三类静态资源、文档正文、接口数据。静态资源是 JS、CSS、图片、字体文档正文是 Markdown 转出来的 HTML 或 JSON接口数据是文档目录树、搜索建议、文章列表这类动态请求。它们适合的缓存策略完全不同核心区别在于“内容更新的紧迫程度”。常用的三种基础策略分别是 Cache First、Stale-While-Revalidate、Network First。我用一个表格说明各自适用场景。策略成功路径适用场景风险点Cache First命中缓存直接返回不请求网络文件名带 hash 的 JS/CSS、图片如果文件名不变内容会一直旧Stale-While-Revalidate先返回缓存同时后台请求网络更新缓存文档正文、文章列表有轻微延迟但用户无感知Network First先请求网络失败再回退缓存目录树、搜索接口、版本检查弱网下延迟较高需要超时控制Cache First 看起来简单但它要求资源 URL 必须与内容版本绑定。静态资源打包工具会生成 hash 文件名所以适合。文档正文如果也用 Cache First那发布新文档后只要 URL 不变用户就永远读不到更新这是很多 PWA 项目翻车的根源。3.2 静态资源文件名 Hash Cache First 的组合静态资源缓存策略的核心逻辑是“让 URL 变化代替手工清缓存”。Vite 在构建时会把 JS、CSS 文件名变成index-a1b2c3.js这种带内容 hash 的名字文件内容一变URL 就变Service Worker 自然会把旧缓存和新资源区分开。针对这类资源我会在 Service Worker 的 fetch 事件里这样处理async function cacheFirst(request) { const cache await caches.open(docs-static-v1); const cached await cache.match(request); if (cached) return cached; const response await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; }有一个例外是 index.html。入口 HTML 文件不能走 Cache First因为它的内容决定了你加载哪些带 hash 的静态资源如果 HTML 被旧缓存卡住JS 更新再快也没用。入口文件建议走 Network First或者说每次请求都先问网络网络不可用才用缓存。3.3 文档正文Stale-While-Revalidate 是最稳妥的选择文档正文对应的请求数据是“读多写少”用户访问频率高但内容更新不频繁。如果完全 Network First离线和弱网场景下体验不好如果完全 Cache First一篇文档更新后要等老用户重启浏览器才生效。Stale-While-Revalidate 是折中中很稳的方案。它的逻辑是命中缓存就先把缓存内容返回给页面页面马上能展示同时后台发一个真实网络请求拉取最新内容并更新缓存。下次访问时拿到的就是新内容。这个策略能保证首次访问速度也不会让旧内容在本地赖着不走。核心代码大致如下async function staleWhileRevalidate(request) { const cache await caches.open(docs-content-v1); const cached await cache.match(request); const network fetch(request) .then((response) { if (response.ok) { cache.put(request, response.clone()); } return response; }) .catch(() cached); return cached || network; }需要注意如果文档正文不是 HTML而是类似 JSON 的数据文件那决策逻辑一样只是缓存命名空间要区分开避免不同类型的数据混在一个 Cache Storage 里后续清理困难。3.4 接口数据Network First 加超时兜底文档工具里还有一类接口数据不能盲目走缓存比如当前用户是否有某篇高级文档的权限、搜索接口的实时结果、服务端最新版本号。这些数据如果离线展示过期内容可能给用户造成误导。但纯 Network First 在弱网环境又容易让页面一直转圈。我的做法是给网络请求加一个超时比如 3 秒内没返回就回退到上一次缓存。async function networkFirstWithTimeout(request, timeoutMs 3000) { const cache await caches.open(docs-api-v1); const timer new Promise((resolve) setTimeout(resolve, timeoutMs)); try { const response await Promise.race([fetch(request), timer]); if (response response.ok) { cache.put(request, response.clone()); return response; } const cached await cache.match(request); if (cached) return cached; // 都没有则返回一个预置的兜底数据 return Response.json({ ok: false, offline: true }); } catch (error) { const cached await cache.match(request); return cached || Response.json({ ok: false, offline: true }); } }这里的“兜底”不一定是空数据。对文档目录树这种基础信息我会在构建时生成一个默认目录快照作为离线时的兜底数据保证用户至少能浏览到历史目录。这个预置快照文件也可以放到预缓存列表里页面断网时体验会好很多。3.5 手写 Service Worker 还是直接用 Workbox手写 Service Worker 最大的好处是你能看到每一行逻辑适合文档工具这种不复杂的场景。但等到资源种类变多需要处理预缓存清单、运行时缓存、路由匹配、过期清理时手写容易漏。Workbox 是 Google 提供的 Service Worker 工具库把缓存策略封装成现成方法配合 Vite 生态时可以直接用 vite-plugin-pwa它会基于 Workbox 自动生成 Service Worker。我的建议是如果项目是长期维护的正式产品直接用 vite-plugin-pwa把精力放在策略参数上如果只是想验证 PWA 离线的可行性先从手写一个只有二十几行的 sw.js 开始跑通了再换工具会更容易理解底层原理。4. 内容同步新文档上线后旧缓存如何优雅让位缓存策略解决的是“离线能不能读”的问题内容同步解决的是“在线了新的内容能不能覆盖旧内容”的问题。这一点比缓存更让运维同事头疼。文档站点发布一篇新文章后用户本地可能还留着旧版本如果同步机制设计得不好新内容发布一周都触达不到部分用户。4.1 更新检查的三种触发方式Service Worker 会在 install 事件触发时重新下载一遍 sw.js / 预缓存清单判断是否有新版本。但浏览器不会频繁自动检查所以页面端要主动触发检查。我在实际项目里用了三种方式。第一种是应用启动时主动检查。Uniapp 的 App.vue 的 onLaunch 里调用一次registration.update()如果用户是当天第一次打开页面马上就能检查到新版本。第二种是页面重新可见时检查。用户从后台切回页面触发visibilitychange事件时再调用一次 update。这个场景很关键因为文档工具经常被用户挂在后台过几个小时切回来如果没有这个机制用户会以为一直是最新版本。第三种是定时检查。对于长期打开不关闭的 PC 端站点我习惯设置 30 分钟一次定时器。文档内容更新不频繁30 分钟已经足够不需要更密集。// #ifdef H5 if (serviceWorker in navigator) { const regPromise navigator.serviceWorker.getRegistration(); regPromise.then((reg) { if (reg) { setInterval(() reg.update(), 30 * 60 * 1000); } }); } // #endif4.2 用户可见的“新版已就绪”提示流程更新流程不能悄无声息也不能强制打断用户。我的方案是当检测到新的 Service Worker 进入 waiting 状态时弹一个确认框告诉用户“文档内容已更新”用户确认后再刷新页面。Uniapp 的 H5 端可以直接用 uni.showModal代码路径上是浏览器原生的确认框但在 Uniapp 语法下写起来一致if (reg.waiting) { uni.showModal({ title: 发现新版本, content: 文档内容已更新是否立即刷新, success: (res) { if (res.confirm) { reg.waiting.postMessage({ type: SKIP_WAITING }); } } }); }Service Worker 里面需要监听这个 message调用 skipWaiting让新版本立即激活。self.addEventListener(message, (event) { if (event.data event.data.type SKIP_WAITING) { self.skipWaiting(); } });还要在页面里监听 controllerchange 事件。这个事件表示 Service Worker 已经切换成功此时刷新页面才能加载新版本navigator.serviceWorker.addEventListener(controllerchange, () { window.location.reload(); });如果你想让更新无缝一点可以跳过弹窗直接在新版本激活后自动刷新。但对文档工具体验来说用户可能正在读一篇重要文档突然刷新会打断阅读我最终还是选择了显式确认的方式。4.3 增量同步的边界什么时候才需要做 diff很多团队一听到“内容同步”第一反应就是做增量同步给每篇文档算 diff只传输变更部分。但对 PWA 来说浏览器层面已经做了一层增量预缓存清单里记录的是带 hash 的资源 URL文档文件变了hash 变了Service Worker 更新的那一刻浏览器只会下载变更后的新文件未变更的文件会继续沿用缓存。这种机制在静态资源层面已经天然完成增量同步。真正需要自己设计增量逻辑的场景是文档数据存在 IndexedDB 里以前端数据表形式维护比如用户笔记、批注、阅读进度。这类数据跨端同步才需要自定义版本号、时间戳或分段 hash。我做离线文档工具的经验是第一版不要一上来就设计复杂的 diff 协议先用“版本号 全量覆盖更新”跑通流程等数据量真的到了需要省流量的程度再引入增量机制那时候你会更清楚该对哪些字段做 diff而不是为了做 diff 而 diff。5. 从创建项目到打包部署的完整链路前面四部分聊的是原理和策略这一部分把代码链路完整串一遍。我默认你用的是 Uniapp CLI 创建的 Vue 3 Vite 项目因为 Uniapp 的 H5 构建链路和 Vite 深度绑定很多配置改起来比 HBuilderX 图形界面更可控。5.1 用 Vite TypeScript 初始化 Uniapp 项目命令行创建带 TypeScript 支持的 Uniapp 项目很简单直接用官方模板npx degit dcloudio/uni-preset-vue#vite-ts offline-docs cd offline-docs npm install npm run dev:h5这个模板默认是 Vue 3 Vite TypeScript。如果你以前用 HBuilderX 新建项目类型支持和 CLI 构建习惯会有点差异。我推荐用 CLI 方式因为后面要改 vite.config.ts、引入 vite-plugin-pwaCLI 项目改起来更顺手也方便进 CI/CD 流程。5.2 manifest.json 与 H5 平台配置Uniapp 的 manifest.json 是全局配置文件很多新手容易忽略 h5 节点下的配置。它决定 H5 端路由模式、页面标题、开发服务器 HTTPS 等。下面是一个精简示例{ name: 离线文档工具, appid: __UNI__OFFLINEDOCS, versionName: 1.0.0, versionCode: 100, h5: { title: 离线文档工具, router: { mode: hash }, devServer: { https: true } } }需要注意的是manifest.json 里的 versionName 主要影响 App 端和小程序端H5 端并不直接在运行时暴露这个字段。如果你想要 H5 端显示版本号需要额外注入这个下面会讲到。另外PWA 可安装性还需要一个标准的 Web App Manifest 文件放在项目 public 目录下比如 public/manifest.webmanifest{ name: 离线文档工具, short_name: 离线文档, start_url: /, display: standalone, background_color: #ffffff, theme_color: #4FC08D, icons: [ { src: /icons/icon-192.png, sizes: 192x192, type: image/png } ] }然后在 index.html 里引入link relmanifest href/manifest.webmanifest / meta nametheme-color content#4FC08D /这里有个很容易忽略的点Uniapp H5 构建时index.html 会作为模板所以可以直接改这个文件不用额外配置 HtmlWebpackPlugin。5.3 接入 vite-plugin-pwa 或手写 Service Worker如果生产项目想省事我更推荐用 vite-plugin-pwa。先安装npm install -D vite-plugin-pwa然后在 vite.config.ts 里配置import { defineConfig } from vite; import uni from dcloudio/vite-plugin-uni; import { VitePWA } from vite-plugin-pwa; export default defineConfig({ plugins: [ uni(), VitePWA({ registerType: autoUpdate, manifest: false, workbox: { globPatterns: [**/*.{js,css,html,svg,png,json}], runtimeCaching: [ { urlPattern: /\/api\/doc\/.*/i, handler: NetworkFirst, options: { cacheName: docs-api, expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 7 } } } ] } }) ] });需要提醒的是vite-plugin-pwa 默认会往页面注入 Service Worker 注册代码但 Uniapp 的 H5 端编译流程有时会有兼容问题。如果发现注册没有生效就把injectRegister设成 null自己在 main.ts 里写前面那套注册逻辑然后把 registerType 改成 autoUpdate 或者配合手动提示流程使用。5.4 H5 端版本号获取与更新参数很多人问 Uniapp H5 端怎样获取版本号。如果你在 H5 里调用uni.getSystemInfoSync().appVersion得到的通常是当前浏览器的版本而不是你产品的发布版本。正确做法是在构建时把 package.json 的 version 注入成全局变量。在 vite.config.ts 里加上import { defineConfig } from vite; import { version } from ./package.json; export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(version) } });然后在 TypeScript 项目里声明全局变量避免报错declare const __APP_VERSION__: string;页面里就可以用了console.log(当前文档版本:, __APP_VERSION__);版本号不只是拿来显示还可以用来辅助更新检查。每次发布后版本号变了配合定时执行registration.update()能让“新版就绪”的提示更可靠。5.5 打包产物、日志清理与各端边界H5 端打包命令是npm run build:h5产物默认在dist/build/h5目录。部署到 nginx 时静态资源缓存头建议按下面方式区分location / { root /opt/offline-docs; try_files $uri $uri/ /index.html; } location /sw.js { add_header Cache-Control no-cache, no-store, must-revalidate; } location /index.html { add_header Cache-Control no-cache; } location /assets/ { add_header Cache-Control public, max-age31536000, immutable; }sw.js 绝对不能长缓存否则 Service Worker 永远更新不了。index.html 也不能长缓存否则加载的静态资源 hash 可能和本地缓存不匹配。生产环境经常会遇到“uniapp 不打印日志信息”的需求。Uniapp 开发阶段 console 日志很多但发布时要清理。如果用的是 Vite 构建最直接的办法是在 esbuild 配置里把 console 和 debugger 去掉export default defineConfig({ esbuild: { drop: process.env.NODE_ENV production ? [console, debugger] : [] } });这比在代码里写一堆条件编译干净得多因为它是构建期行为开发环境不受影响。再补充一下“uniapp 怎么打包”的边界。如果你的目标是 H5 PWAnpm run build:h5之后部署静态文件就可以了如果目标是上架安卓应用市场那要走云打包或本地打包生成 apk/aab和本文的 PWA 链路不是一回事。Uniapp 的打包能力确实是多端的但 PWA 只在 H5 端生效不要混淆。6. 线上 PWA 清单与真实踩坑记录策略配好了构建能出包不代表上线就一定没问题。PWA 的很多坑只有在真实网络环境、真实浏览器、真实发布流程里才会暴露。我在项目上线过程中踩了不少整理成下面几个部分。6.1 用 Lighthouse 自查哪些指标最常用的检查工具是 Chrome DevTools 里的 Lighthouse。跑之前先把构建产物部署到一台测试服务器上并且一定用 HTTPS 访问然后切到无痕窗口再跑避免之前访问残留的 Service Worker 干扰结果。Lighthouse 的 PWA 类别主要看页面是否可安装、是否启用 Service Worker、是否支持离线启动、是否响应移动端。文档工具最容易出问题的地方是离线启动因为 Service Worker 必须在离线情况下能返回一个可用的页面壳子而不是直接报网络错误。我用表格总结一下最常见的几个失败点表现常见原因处理方式离线时页面白屏index.html 没有预缓存把入口 HTML 加入预缓存清单配置了 SW 但 Lighthouse 检测不到sw.js 路径和作用域不一致务必放在构建根目录scope 覆盖全站更新后用户一直是旧版本没有处理 skipWaiting监听 waiting 状态并提示刷新HTTPS 正常仍报 insecure context使用了 IP 地址访问换成有效域名和证书6.2 Safari、微信内置浏览器与国产浏览器差异Service Worker 的兼容性不能只看 Chrome。iOS Safari 从 11.3 开始支持 Service Worker但部分 API 行为和桌面 Chrome 不一致。还有一点iOS 的“添加到主屏幕”是否完全走 PWA 模式取决于 web app manifest 和 apple-touch-icon 是否配置完整。只做 Chrome 调试真机一测容易翻车。微信内置浏览器对 Service Worker 的支持并不可靠很多版本里注册会失败。如果你主要把文档链接分享在微信里一定要做一个降级方案检测到 npx 不支持 Service Worker 时至少保证普通 HTTP 访问能用最多是离线能力失效不能让页面直接打不开。这也是为什么我的策略始终强调“回退”而不是把离线当成唯一路径。国产 Android 浏览器的内核差异同样需要真机验证尤其是 X5 内核和厂商自带浏览器。处理办法是把 Service Worker 的注册代码包在能力检测里不支持的环境自动走普通网络加载至少不影响基本使用。6.3 各端差异化实现自定义分享、开发者工具插件与应用商店的边界Uniapp 的项目最终往往会同时发布 H5、小程序、App 三端离线能力只是 H5 端的方案。很多人会拿“uniapp自定义分享好友”这类小程序能力来对比 H5。小程序端实现自定义分享一般是在按钮上配置 open-type或者调用 uni.shareH5 端则用 Web Share API 或者复制链接。两者不在一个技术体系里但是同一套业务逻辑。如果要在微信开发者工具里调试小程序需要在 HBuilderX 里配置“uniapp 微信小程序开发者工具插件”路径运行后自动打开微信开发者工具。这个插件只是调试链路的配置和 PWA 没有关系别在排查 H5 问题时被它干扰。至于“uniapp上架安卓应用市场”那是另一个流程。云打包会生成原生安装包离线能力可以靠本地存储方案实现但它和 PWA 的浏览器缓存完全是两码事。真正想要浏览器层面的安装体验还是只有 PWA 这条路。这个边界想清楚才不会在技术选型上走偏。6.4 复盘后我建议盯紧的五个动作项目复盘时我总结出五个最值得盯的动作分享给做同类项目的同学。第一用真实域名加 HTTPS 部署测试环境不要用 IP。IP 环境下 Service Worker 注册会被拒很多排查时间都浪费在这。第二每次发布都手动更新一次版本号并且把版本号显示到文档页脚或设置页。这样用户反馈“看不到新内容”时你可以立刻判断他本地是哪个版本。第三离线检查不要只看 Chrome DevTools 的离线开关要真机开飞行模式验证一次。Service Worker 在移动浏览器上的行为往往比桌面端更严格。第四缓存策略别追求一步到位先让离线能跑再根据真实访问数据调整哪些资源走 Cache First哪些走 Network First。过度设计会拖慢上线节奏。第五给不支持 Service Worker 的环境留一条普通网络访问的退路。PWA 对文档工具来说是增强体验不应该成为所有用户访问文档的唯一入口。我自己在项目里最深刻的体会是离线文档工具的技术难点不是怎么把页面缓存下来而是怎么在内容更新时让用户端的状态始终保持一致。把静态资源、文档正文、接口数据三条缓存路径分清楚把版本号变成更新机制里的一等成员整个项目后半段的维护会轻松非常多。这套链路跑通之后后续想扩展搜索离线索引、阅读记录同步都会顺手很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑