Uniapp H5端接入PWA完整指南:Service Worker离线缓存实战

发布时间:2026/10/2 15:21:13
Uniapp H5端接入PWA完整指南:Service Worker离线缓存实战 做 Uniapp 的人都知道一套代码能在多端跑H5 端却经常被忽略。发布完小程序、App 后H5 往往就是个“能访问的链接”弱网一刷白屏也没有桌面入口体验和小程序比差一截。其实 Uniapp 的 H5 端完全可以借 PWA 补上这个短板。PWA 不是一个新框架也不是要重写业务它由 Web App Manifest 和 Service Worker 两大块组成manifest 负责“看起来像 App”Service Worker 负责“跑得稳、离线能用”。这篇文章我按自己接过的真实项目从 Uniapp 的 manifest 配置到 Service Worker 注册、缓存策略、部署验证完整走一遍适合正在做 H5 端增强或者想给内部系统加离线能力的同学。1. 先搞清楚Uniapp、PWA、manifest 这三个“manifest”到底是不是一回事这里容易踩坑。Uniapp 项目根目录有一个 manifest.jsonPWA 里也有一个 manifest.json准确叫 Web App Manifest两个都是 JSON名字一样但作用完全不同。Uniapp 的 manifest.json 管的是应用标识、小程序 AppID、App 权限、SDK 配置、H5 路由模式等PWA 的 manifest 管的是网页作为“应用”安装后叫什么名字、用什么图标、以什么方式打开。把 PWA 字段直接塞进 Uniapp 的 manifest.json 顶层H5 端不会有任何变化。想在 Uniapp 里开 PWA 模式必须把 PWA 配置放在 Uniapp manifest.json 中 h5 节点的 pwa 块下面或者独立引用一个 manifest.webmanifest 文件。我刚开始做的时候也被这个命名绕了一下。当时在 Uniapp 的 manifest.json 里加了name, short_name, display, background_colorChrome 的 Application 面板里 Manifest 一直显示为空查了半天才发现放错位置了。所以第一步先把这个概念捋清楚后面才不会满世界找 bug。1.1 为什么 H5 端需要 PWAH5 和三端相比最大的问题就是“不可靠”。服务器一抖、网络一波动用户打开要么一直转圈要么直接白屏。内部管理系统尤其明显办公场景经常网络不稳定员工在地铁、电梯、会议室里打开页面结果统计全失。PWA 能解决三个具体问题第一安装入口通过 manifest 让页面能“添加到主屏幕”看起来像个独立应用有独立图标和启动页第二离线能力通过 Service Worker 预缓存静态资源页面在断网时也能打开至少骨架和样式不丢第三弱网加速缓存命中后不需要走完整网络链路开屏速度会快很多。对 Uniapp H5 来说业务代码本身就是静态资源加接口请求非常适合套 PWA。1.2 两个 manifest 的边界Uniapp 项目的跨端配置集中在 manifest.json里面的 h5 节点主要控制 H5 端的路由、模板、title、跨域配置等。PWA 的 Web App Manifest 则是一个独立的 JSON 描述文件浏览器通过link relmanifest引入。Uniapp 从某个版本开始已经把h5.pwa作为官方配置入口目的是把 PWA 配置和 Uniapp 工程配置放一起不用单独维护文件。但我还是建议你理解两者边界Uniapp 的 manifest 是“源项目配置”编译后不会直接暴露在浏览器里PWA 的 manifest 最终是一个可访问的 JSON URL两者靠构建过程联系。1.3 Uniapp 开启 PWA 的完整路径一句话Uniapp 源码里写好 PWA 配置和 Service Worker发行 H5 时构建产物带上这些内容部署到 HTTPS 服务器浏览器访问后提示“安装应用”桌面生成图标离线也能打开。别指望有什么开关一键搞定PWA 能力还是 Web 标准Uniapp 只是帮你组织配置和静态资源真正的运行逻辑在浏览器。对于使用 HBuilderX 创建的项目编译产物最终在unpackage/dist/build/h5部署时把该目录扔到服务器就行。Service Worker 文件能否在根路径直接访问决定了它的控制范围这一点后面会详细展开。2. manifest 配置决定“能不能被安装”的门面PWA 的 manifest 字段很多但日常用的就十来个。把关键字段配置对了页面就能被浏览器识别成“可安装应用”配错了安装菜单不出现一切后续都无从谈起。2.1 基础字段怎么填字段是否必填作用推荐值name推荐安装后显示的应用全称公司产品名short_name推荐桌面图标下方的短名称2~4个字description选填应用描述一句清楚的话start_url必填点击图标后打开哪个地址站点根路径或入口路径scope选填哪些路径在这个应用范围内与 start_url 保持一致display必填独立窗口还是浏览器标签页standaloneorientation选填屏幕方向portrait 或 anystart_url 我建议写成相对路径/前提是站点部署在域名根目录。如果部署在子目录比如https://example.com/h5/就得写/h5/scope 也写/h5/否则“添加到主屏幕”后打开访问范围不对甚至点击图标跑回首页。这个错位在子目录部署时几乎必踩。2.2 图标、主题色、启动方式manifest 里的 icons 数组要至少提供一个 192x192 和一个 512x512 的图标。Chrome 的安装条件非常直白缺少合适的 icon它就会认为这个站点“不够资格”成为应用Add to Home Screen 菜单会自动隐藏。注意图标不能是透明的 SVG 或 ICOPWA 安装场景推荐 PNG。另外 2021 年后的 Android Chrome 还支持 maskable 目的也就是说图标在圆形裁剪区域内也要完整识别制作时留出安全边距。theme_color 会影响地址栏、任务栏、浏览器 UI 的主题色background_color 是启动时白屏到首屏之间短暂显示的颜色。建议取应用主色调并和 Uniapp 页面里 CSS 变量尽量一致避免启动瞬间颜色断层。display 选择 standalone 是核心体验安装后会以独立窗口打开没有地址栏、没有浏览器按钮更像原生 App。minimal-ui 在 iOS 上也算可用但 Android 上 standalone 最省事。如果你不能接受 PWA 完全脱离浏览器上下文可以考虑 minimal-ui如果做内部工具直接 standalone。2.3 在 Uniapp 的 manifest.json 里写 PWA 配置假设我用 HBuilderX 创建一个 uni-app 项目在 manifest.json 的 h5 节点下增加 pwa 块h5: { router: { mode: hash }, pwa: { manifest: { name: 工单协作平台, short_name: 工单, description: 基于 Uniapp 的工单协作 PWA 应用, background_color: #1f2937, theme_color: #1f2937, display: standalone, orientation: portrait, start_url: /, scope: /, icons: [ { src: /static/pwa/icon-192.png, sizes: 192x192, type: image/png }, { src: /static/pwa/icon-512.png, sizes: 512x512, type: image/png, purpose: any maskable } ] } } }这里的 h5.router 我是举例你按自己项目已有配置保留即可不要覆盖。加好之后HBuilderX 的 PWA 模式会在构建 H5 时把这些字段生成对应的 manifest 信息并挂在页面 head 里。如果你的 HBuilderX 版本较老可视化界面里找不到 PWA 入口就直接在源码模式编辑这个 JSON效果是一样的。需要强调一点static 目录下必须真实存在对应的图标路径否则 manifest 可以解析但安装时会因图标加载失败被浏览器判为“不可安装”。3. Service Worker 注册离线和缓存能力的核心manifest 解决的是“像不像 App”Service Worker 解决的是“能不能离线、快不快”。它本质是一个跑在浏览器后台的 JavaScript 文件能拦截页面发起的网络请求并从 Cache Storage 里返回缓存。但 Service Worker 不是普通全局变量它的生命周期很明确配置时最容易出问题的就是对生命周期和 scope 的理解。3.1 注册的前提与位置Service Worker 必须运行在安全上下文也就是 HTTPS 或者 localhost。如果你是刚把 Uniapp H5 部署到内网服务器拿 HTTP 内网 IP 访问Service Worker 注册会直接失败这不是代码问题是浏览器安全策略。所以内网直接部署也可以但需要配置 HTTPS 证书调试阶段用127.0.0.1或localhost是可以绕过的。注册文件位置也有关浏览器默认把 sw.js 所在目录作为作用域。比如你访问https://example.com/static/sw.js默认只能控制/static/下的请求页面的/index.html、API 请求它都管不到。如果想控制整个站点就把 sw.js 放到根目录访问路径是https://example.com/sw.js或者通过响应头Service-Worker-Allowed: /扩大作用域。我在项目里最常遇到的就是这个把 sw.js 放进了 Uniapp 的 static 目录结果它变成/static/sw.js注册成功但离线不生效。3.2 生命周期和更新机制Service Worker 从注册到接管页面分为安装install和激活activate两步。install 通常用来预缓存静态资源activate 用来清理旧版本的缓存。页面刷新后新的 Service Worker 不会立刻接管要等所有打开的页面关闭再打开开发调试时勾选 DevTools 里的 Update on reload再配self.skipWaiting()和clients.claim()可以强制接管。更新机制需要注意浏览器会定期去服务器请求 sw.js 文件如果发现字节级变化就安装新版本。所以 sw.js 本身不能被 HTTP 缓存最好配合后端设置Cache-Control: no-cache。否则每次发布后客户端拿到的还是旧的 Service Worker 逻辑缓存策略不更新“改了没效果”的坑就是这么来的。3.3 缓存策略设计静态资源缓存优先接口网络优先Service Worker 里的 fetch 事件就是做请求路由的地方。我的项目采用混合策略页面导航请求网络优先确保入口能拿到最新 HTMLjs/css 图片这类带版本号的静态资源走缓存优先速度最快API 请求网络优先失败时再尝试返回一个缓存副本或者直接提示离线。请求类型策略原因navigate 页面跳转Network First要拿到最新入口和可能的更新static/js/css 静态资源Cache First带 hash缓存可无限期用图片、字体Cache First体积小稳定性高API GETNetwork First数据要新鲜跨域请求默认不缓存避免 CORS 缓存污染这里的核心思想静态资源带 hash当成不可变文件入口 HTML 和接口当成动态资源必须在网络正常时拿最新数据。盲目把全部请求都 cache-first虽然快但数据会一直过期用户看到旧列表、旧流程体验反而崩。4. 实操全过程从零接通 Uniapp PWA前面讲了理论和配置现在把完整落地过程过一遍。我是用 HBuilderX 创建的 uni-app Vue3 项目组件库选的是 uview-plus从插件市场导入后直接引入PWA 有没有它都能跑有了它界面更像原生。4.1 准备一个干净的 Uniapp H5 项目创建项目时选“默认模板 Vue3”或者用命令行创建也可以。项目跑起来后先用 Chrome 打开 H5 地址打开 DevTools 的 Application 面板确认当前是没有 Manifest 和 Service Worker 的清洁状态。这一步很重要能后续区别到底是谁帮我们加上的机制。如果你用 HBuilderX发行 H5 的入口在菜单“发行 - 网站-PC Web或手机H5”。这里会自动执行 uni build生成unpackage/dist/build/h5目录。正式部署时用这个目录直接丢给 Nginx 或对象存储。4.2 写 manifest 配置按照第 2 节把 PWA 配置放进 manifest.json 的 h5.pwa 块。图标文件放在src/static/pwa/或static/pwa/要注意实际路径。在 HBuilderX 工程中static 目录是静态资源目录最终会原样复制到构建产物根目录下所以图标引用写/static/pwa/icon-512.png是可以的。但 sw.js 不要放这里原因前面说了scope 会失控。sw.js 应该通过部署步骤直接放到网站根目录或者用构建工具复制过去。4.3 添加 sw.js 和注册代码我在项目根路径建立的 sw.js 完整示例const CACHE_NAME uniapp-pwa-v1.2.0; const STATIC_PREFIX [/static/, /js/, /css/, /fonts/]; self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) { return cache.addAll([/, /index.html]); }).then(() self.skipWaiting()) ); }); self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((keys) { return Promise.all(keys.filter((key) key ! CACHE_NAME).map((key) caches.delete(key))); }).then(() self.clients.claim()) ); }); self.addEventListener(fetch, (event) { const request event.request; if (request.method ! GET) return; const url new URL(request.url); if (url.origin ! location.origin) return; if (request.mode navigate) { event.respondWith( fetch(request).then((response) { const copy response.clone(); caches.open(CACHE_NAME).then((cache) cache.put(request, copy)); return response; }).catch(() caches.match(/index.html)) ); return; } if (STATIC_PREFIX.some((prefix) url.pathname.startsWith(prefix))) { event.respondWith( caches.match(request).then((cached) { if (cached) return cached; return fetch(request).then((response) { const copy response.clone(); caches.open(CACHE_NAME).then((cache) cache.put(request, copy)); return response; }); }) ); return; } });这段示例覆盖了页面、静态资源而 API 请求默认走浏览器原生网络不进入缓存。要注意cache.addAll([/, /index.html])在开发环境可能是/#/正式环境路径请确认站点的入口文件名和路径否则 install 可能因为其中一个请求 404 而全部失败。注册代码放在 Uniapp 的 main.js 里用条件编译保证只在 H5 端执行import App from ./App.vue; import { createSSRApp } from vue; export function createApp() { const app createSSRApp(App); // #ifdef H5 if (serviceWorker in navigator location.protocol https:) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js, { scope: / }).then((registration) { console.log([SW] registered, registration.scope); }).catch((error) { console.warn([SW] register failed, error); }); }); } // #endif return { app }; }注意我用location.protocol https:做了判断HTTP 环境下就不注册避免控制台一片报错。如果你的服务在 localhost 上调试协议判断会把它拦掉可以临时改成生产环境注册规则按团队需要定。4.4 构建部署与 DevTools 验证构建 H5 后把 sw.js 部署到站点根目录。比如用 Nginx 托管 dist 目录配置里要保证 sw.js 返回的 Content-Type 是application/javascript并且响应头Cache-Control: no-cache。可以加一条location /sw.js { add_header Cache-Control no-cache, no-store; add_header Service-Worker-Allowed /; }然后用 HTTPS 访问站点。打开 DevTools - Application依次看三块Manifest 里显示名称、图标、作用域是否正常Service Workers 里出现 root/sw.js 并且状态是 activated再切到 Network 面板离线模拟刷新页面如果页面还能打开Service Worker 就生效了。最后可以跑一下 Lighthouse选 PWA 分类看看各检查项得分。5. 常见问题与排查技巧这部分的坑我从实际项目里一个一个踩过来的每个都能查半天。5.1 Service Worker 注册了但离线不生效先看作用域。Application 面板里 Service Worker 条目会显示 Scope如果写成https://example.com/static/说明 sw.js 在 static 下只管静态目录。解决办法把 sw.js 放到根路径或者给响应头加Service-Worker-Allowed: /。其次看 Cache Storage 里有没有内容。install 阶段cache.addAll任何一个请求 404整个安装就会失败Service Worker 状态会一直 stuck 在 installing。这种情况先把作用域和入口路径配好再重新注册。还有个小细节Uniapp 的 H5 页面默认 URL 可能是/#/开头的 hash 路由如果 start_url 或预缓存写的是/index.html线上结构不一致也会导致 manifest 和 sw 找不到对应资源。部署前直接 curl 一下https://你的域名/index.html和/sw.js确认 200 再收工。5.2 更新后用户仍使用旧版本PWA 的更新不像 Web 页面那样刷新就生效。浏览器检查 sw.js 更新的节奏通常是 24 小时一次而且正在控制的页面要等释放后新版本才接管。如果想强制旧版本尽快失效一是 sw.js 本身不要被缓存二是在 install 后调用self.skipWaiting()三是在 fetch 拦截逻辑里使用self.clients.claim()。但即使这样已安装到桌面的 PWA 窗口也需要关闭重开一次才能切换所以发布新版本时不要指望秒级生效。如果只想调试DevTools 里勾选 Update on reload每次刷新都会强制检查并在 Network 面板里能看到脚本更新。养成习惯修改 sw.js 后先清一次旧 Service Worker否则容易误判“代码没问题”。5.3 站点部署在子目录时需要注意什么很多后台系统部署在https://内网域名/ops/这样的子路径。这时 PWA 的 start_url、scope、sw.js 的路径也要一起变。把 sw.js 放在/ops/sw.js注册写成/ops/sw.jsscope 默认就是/ops/这样没问题。同时 Uniapp 的 h5.router.base 或 vite 的 base 要改成/ops/否则 JS/CSS 路径会 404页面装了 PWA 也白搭。记住一个原则PWA 的所有路径设置都要和你线上 URL 的前缀保持一致。5.4 兼容性与安装入口Android 上的 Chrome、Edge 对 PWA 支持最好小米手机自带浏览器和 Chrome 差异不大一般在菜单里找“安装应用”或“添加到主屏幕”如果没有入口先确认 Manifest 字段是否合规、图标是否具备 192 和 512 两张。iOS 从 Safari 12 开始支持添加主屏幕但展示效果和 Android 有差别导航栏样式也控制不了。不要拿 PWA 去和安卓应用市场上架混淆——PWA 的“安装”只是快捷方式加离线壳不是 APK。想要上架安卓应用市场还是得走 Uniapp 的云打包或本地打包这是两条完全不同的路。6. 再做一点也不难体验优化和个人建议到这里核心链路已经通了但想让 PWA 真正好用还有几个小点值得做。6.1 添加更新提示Cache First 会把新版本缓存住用户看不到新页面。可以在注册成功后监听更新navigator.serviceWorker.addEventListener(controllerchange, () { window.location.reload(); });内部工具可以直接强制刷新面向用户的产品最好先弹出新版本已准备的提示点击后再刷新。实际体验中强制刷新虽然粗暴但减少了明明发版了却还是旧页面的投诉。6.2 给接口也做一层最小缓存如果某些列表数据变化不频繁比如部门列表、字典配置可以在 fetch 事件里对包含指定前缀的 GET 请求启用 Network First网络失败回退到缓存。但不能全局缓存 API否则会产生脏数据。我一般只对/api/dict/和/api/config/做半小时左右缓存其他接口一律网络优先核心交易数据绝不缓存。6.3 注意 iOS 的坑iOS Safari 对 PWA 的支持比较有限没有良好的更新提示桌面图标点击时启动动画不一致后台刷新也不可靠。更麻烦的是如果页面使用了定位或相机Service Worker 缓存可能导致权限弹窗行为异常。所以 iOS 用户使用前建议保留一个如果遇到问题强制刷新一下的提示入口或者在 Uniapp 条件编译里对 iOS 隐藏安装引导。PWA 是增强能力不是替代原生别把所有用户体验押在 Service Worker 上。我第一次给内部系统接入 PWA 时最反复的问题就是 scope后来养成了配置完先看 Application 面板、再 curl 一下文件、最后离线模拟的习惯几乎没再翻过车。PWA 的收益是实打实的静态资源秒开、弱网不再白屏、桌面入口能提升不少使用频率。Uniapp 这套代码编译成 H5 后本身就是标准 Web 应用PWA 相关的活儿最终还是落在浏览器标准上。希望这篇能让你少踩几个我已经踩平的坑。