
各位用HBuilderX做Uniapp的朋友如果只是把项目打包成H5扔到服务器上那可真浪费了Uniapp的跨端能力。最近我在给一个资讯类应用做Web端体验升级目标很明确用户在浏览器里打开链接能像打开一个原生App一样——有桌面图标、有启动屏、全屏运行、离线还能看缓存过的内容。这正好就是PWA的看家本领而Uniapp在H5端的底层是标准Web技术栈天然可以叠加PWA能力。上一篇我讲了Uniapp PWA的整体思路和开发环境的初步准备这一篇直接把核心难点拆开揉碎重点聚焦两件事manifest配置和Service Worker注册。这两块是PWA的任督二脉打通了你的Uniapp项目在移动端和PC端的体验会上一个台阶打不通页面永远只是个“能在浏览器里打开的网页”。我会把配置面板里的每个选项、Service Worker的每个生命周期事件、常见的坑全部按实操顺序过一遍保证你跟着做完能出效果。1. 为什么Uniapp项目值得做PWA化1.1 PWA到底是什么解决了什么问题PWAProgressive Web App不是一个新框架也不是一门新语言它是Google在2015年前后提出的一套Web应用增强标准。核心思路是把现代浏览器提供的能力组合起来让网页达到接近原生App的体验。这里的“接近”不是空喊口号它有实实在在的衡量标准可安装、离线可用、后台同步、桌面入口、全屏沉浸式运行。我打个比方。普通网页像一张传单用户看完就扔下次还得重新找PWA像一个小型工具箱用户第一次访问时浏览器会问“要不要把这个工具放到桌面”放上去之后它就有了自己的图标、独立窗口、甚至能在弱网环境下继续使用。传统网页是“用户主动找你”PWA是“你住在用户的设备里”。这种体验对内容型、工具型、营销型产品特别有效尤其是那些不想为了一个简单需求专门开发原生App的场景。从技术构成上说PWA主要由三块支撑HTTPS安全环境、manifest清单文件、Service Worker脚本。HTTPS是地基没有安全连接浏览器不会把高级能力开放给网页manifest解决“身份展示”问题让系统认识你这个应用Service Worker解决“能力执行”问题负责拦截网络请求、管理缓存、实现离线策略。这三块各司其职缺一个都不叫完整的PWA。1.2 Uniapp和PWA的组合优势Uniapp本身是一套跨端框架它通过编译手段把一套Vue代码输出到小程序、App、H5。其中H5端输出的是一份标准Web项目说白了就是一堆HTML、CSS、JavaScript文件。这份标准Web项目天然运行在浏览器里所以PWA需要的所有能力它都支持前提是别在Uniapp里用掉那些只属于小程序或App的API。这个组合的好处在于你依然用Vue语法、Uniapp的组件体系和生命周期管理业务代码可以复用同时还能享受到PWA带来的Web体验升级。比如我在Uniapp里写一个新闻列表页跑在H5端时用户可以“安装”到手机桌面下次打开直接从缓存读取列表数据弱网环境也不至于白屏。这些东西不需要Objective-C也不需要Java/Kotlin纯粹是配置文件和脚本逻辑就能搞定。另一个容易被忽略的优势是全球化部署成本低。PWA应用就是一个静态站点随便找个Nginx、OSS静态托管、Vercel都能跑不需要应用商店审核不需要签名证书也不依赖第三方推送服务商微信推送等除外。如果你还在纠结“公司预算不够做原生App但网页版体验又太简陋”Uniapp PWA是个极其务实的方案。1.3 开始前的环境清单电脑上装好HBuilderX我用的3.99版本建议保持最新稳定版某些PWA相关的H5编译配置在旧版本上有差异。准备一个HTTPS可用的域名或IP。生产环境必须有HTTPS证书这一点后面会单独展开。如果只在本地调试localhost历史上属于安全上下文或者用127.0.0.1也可以但有些移动端调试场景需要真实域名加证书后面排查章节会细说。浏览器推荐Chrome或Edge因为PWA调试工具最成熟。Firefox和Safari也在逐步支持PWA但很多能力优先在Chrome上验证比较省心。如果你要用Vue3 Vite方式编译H5需要稍微了解下npm run build:h5产物目录结构这会直接关系到你部署时把哪些文件扔上服务器。2. 工程侧准备从HBuilderX创建项目到H5部署前置2.1 在HBuilderX中创建一个Uniapp项目打开HBuilderX在菜单栏选择“文件 - 新建 - 项目”弹出窗口里选择“uni-app”模板。注意这里有两个基础模板Vue2和Vue3。如果你是新项目无条件选Vue3版本因为Vite编译链路更快而且Vue3对PWA相关的现代Web API兼容性更好。填好项目名称比如uniapp-pwa-demo和保存路径后点击创建。创建完成后你会看到标准的Uniapp目录结构pages目录放页面static目录放静态资源根目录有manifest.json、pages.json、main.js、App.vue。这里需要说明下manifest.json在Uniapp里是一个整体配置文件它不只有PWA那点东西还包括应用名称、App图标、APP模块配置、小程序配置、H5配置等。PWA的所有身份信息就藏在H5这一区块中。2.2 细看manifest.json的H5配置面板回到HBuilderX双击manifest.json默认打开的是可视化配置界面。顶部有“基础配置”“App配置”“Web配置”“小程序配置”等多个Tab。点击“Web配置”你会看到一堆和H5运行相关的设置项。这里先看几个和后续开发直接相关的路由模式默认是hash改成history可以去掉URL里的#PWA的start_url和分享链接会更干净。但history模式需要服务端配合做history fallback否则刷新二级页面会404这个在后端部署时要注意。运行端口默认8080如果多个项目同时调试建议改成不冲突的端口。H5页面图标这里的favicon.ico只是浏览器标签页图标不是PWA桌面图标。别指望在这里加一张大图就能当桌面图标用。页面标题对应浏览器标签和安装时显示的名称但安装后桌面显示的名称由manifest单独控制。可视化界面虽然方便但它并不能覆盖全部PWA字段很多关键的manifest配置项比如short_name、start_url、display、theme_color在Uniapp的默认界面里并不会直接出现。这就引出一个重要知识点你完全可以在HBuilderX的manifest源码视图手动添加自定义字段。具体操作在manifest.json的可视化编辑窗口右上角有个“源码视图”切换按钮。切到源码视图后找到h5节点你会发现类似下面这样的结构{ h5: { title: 我的资讯站, router: { mode: hash }, template: , sdkConfigs: {} } }而我们要做的是在h5节点里塞入一个pwa子节点专门放manifest关联字段。这算是Uniapp里头有点“隐藏”的用法。具体配置会在第三章完整展示。2.3 本地启动与基本访问验证在HBuilderX选择项目点击工具栏上的运行按钮选择“运行到浏览器 - Chrome”。正常启动后浏览器会打开http://localhost:8080端口视你的配置而定。能看到你的页面正常渲染说明Uniapp的H5链路没问题了。如果你想在手机上调试PWA效果不要直接访问localhost手机没法访问你电脑的localhost。你需要让电脑和手机处于同一局域网然后用电脑的局域网IP加端口访问比如http://192.168.1.101:8080。但这里的坑是PWA的Service Worker在非HTTPS环境下只有localhost和127.0.0.1算安全上下文局域网IP并不属于安全上下文浏览器往往会拒绝注册SW。所以这一步也先别太纠结真正给自己的IP配一张证书或者用一些内网穿透工具映射成HTTPS域名才有完整的PWA调试体验。具体的调试思路我在第五章会讲清楚。3. manifest配置详解让页面变成“可安装应用”3.1 PWA核心配置逐项拆解先说结论Uniapp在编译H5时会读取manifest.json里的某些字段来生成网页里的link relmanifest指向的manifest文件。但这个自动生成过程并不会包含所有PWA高级配置。我这里给出一个经过真实项目验证的完整配置模板你直接照着改就行。在manifest.json的源码视图里把h5节点扩展成下面这样{ h5: { title: 我的资讯站, router: { mode: history, base: / }, pwa: { name: 我的资讯站, short_name: 资讯站, description: 一个基于Uniapp构建的PWA资讯应用, lang: zh-CN, display: standalone, orientation: portrait, theme_color: #42b983, background_color: #ffffff, start_url: /, scope: /, icons: [ { src: /static/pwa/icon-192x192.png, sizes: 192x192, type: image/png }, { src: /static/pwa/icon-512x512.png, sizes: 512x512, type: image/png }, { src: /static/pwa/icon-512x512.png, sizes: 512x512, type: image/png, purpose: maskable } ] }, template: , sdkConfigs: {} } }逐项解释一下为什么这么配name这是应用安装后显示在桌面下方的完整名称也是系统应用列表里看到的名称。一般不超过30个中文字符再长就会被系统截断。short_name在桌面图标空间不够时显示的短名称。建议控制在6~8个汉字以内比如“资讯站”。有些人氮忽略它导致安装后桌面显示一个被截断的长名字观感很糟。description应用描述部分浏览器在安装确认弹窗里会展示类似于应用商店里的产品简介对用户是否点击“安装”有一定影响。lang应用语言zh-CN是简体中文。这项影响浏览器对应用语言的识别也影响一些无障碍阅读器朗读。display显示模式。standalone意味着打开应用时不显示浏览器地址栏和工具栏是一个独立的窗口这是PWA最接近原生App的体验模式之一。可选值还有fullscreen全屏、minimal-ui保留最小浏览器控件、browser普通标签页。个人建议用standalone除非你要做游戏类全屏应用。orientation屏幕方向锁定。我一般设成portrait竖屏内容型产品以竖屏为主。如果业务不需要锁定方向可以不填或设any避免在平板或PC上体验受限。theme_color主题色影响浏览器地址栏颜色和系统任务栏背景色。这里设成和你网站主题一致的品牌色视觉统一性会好很多。background_color启动屏背景色。PWA启动时先展示这个颜色然后才加载页面。如果你的页面首屏背景是白色的这里就设白色避免启动瞬间色块跳变。start_url应用从桌面图标启动后加载的第一个页面路径。注意这里必须是相对scope的有效URL我设置为/根路径。如果你希望用户每次打开都进入首页就用/如果希望进入某个特定落地页比如/pages/index/index但这里需要注意Uniapp history模式下的真实URL路径通常是带/h5/前缀或者直接/要根据实际部署路径来配。scope应用的作用域。通常和start_url同级目录或者更靠根。如果设置成/意味着这个域下面的所有页面都被视为该应用的一部分。别把scope缩小到某个子目录后又希望全站都有PWA能力那会冲突。需要注意上面的pwa节点只是我个人的组织习惯Uniapp官方在H5编译时并不一定会直接读取这些字段生成一个独立的manifest.json文件。如果你在项目根目录里找不到实际的manifest.json但浏览器调试面板又显示有manifest那大概率是编译器做了处理。为了保证最终产物里真的有标准manifest文件我的做法是在static目录下手工放置一个manifest.json并在index.html里手动引入后面会讲。3.2 icons图标的正确使用方法PWA图标的坑比大多数人想象的要多。不是随便拿一张512px的图放上去就完事。浏览器和系统对图标有多重需求最小尺寸Chrome要求至少192x192和512x512两档。格式推荐PNG不支持ICO和GIF。maskable图标新版Android系统的自适应图标要求图标有安全边距图标主体内容位于中心80%的圆形区域内。如果你不提供purpose: maskable的图标某些系统会把你的方图强行裁剪成一个圆结果边缘被切秃非常丑。我的实践是准备三张图文件名尺寸用途icon-192x192.png192x192低分辨率设备图标icon-512x512.png512x512高分辨率图标icon-512-maskable.png512x512Android自适应图标主体内容居中偏小放到static/pwa/目录。开发期间可以先用占位图但上线前一定要找人做一套正式品牌图标否则一个糊图标会毁掉用户对产品的第一印象。有些读者会问我用png路径是相对路径/static/pwa/icon-192x192.png但部署到二级路径下比如https://example.com/h5/不会挂吗会。所以如果你部署到子路径需要把start_url、scope、图标路径全部改成子路径前缀或者在打包后统一替换为绝对路径。这个问题在第五章部署部分我还会再提醒一次。3.3 如何在页面中正确引入manifestUniapp和纯前端项目有一个重要差异编译器生成的index.html里不一定默认包含link relmanifest。所以我现在都采用“显式宣言”的做法不依赖编译器的隐含行为。在项目根目录的index.html里如果HBuilderX的Vue3项目模板没有index.html文件你可以手动创建编译时会以它作为H5模板加上这样一行link relmanifest href/manifest.json然后把manifest.json放到项目的static目录下或者放根目录也可以取决于你要部署的绝对路径。我习惯放static因为Uniapp编译H5时会把static下的文件原样拷贝到输出目录不会改名不会加hash这样部署时不容易漏。有人会担心手动维护一份manifest.json会不会和Uniapp本身的manifest.json冲突不会。Uniapp的manifest.json是编译期配置文件给编译器看你放到static里的manifest.json是运行期文件给浏览器看。名字虽然一样但各司其职。3.4 验证manifest是否解析正确配置完不要急着往下走先验证。Chrome DevTools打开后切到“Application”应用程序面板。在左侧菜单里找到“Manifest”栏目点击后你会看到完整的解析结果。如果配置正确它会显示应用名称、图标、主题色、启动地址等。如果某些字段没解析出来那里会直接标红提示。另外地址栏右侧如果出现一个小加号图标安装按钮说明你的站点已经被浏览器认定为“可安装的PWA”。点击它浏览器会弹出一个安装确认框这就是PWA安装入口。如果这个加号没出现大概率是manifest缺失、图标尺寸不够、没有Service Worker或者HTTPS不满足这几个方向逐个排查。4. Service Worker注册全攻略4.1 Service Worker运行机制速通如果说manifest决定了PWA“长什么样”那Service Worker就决定了PWA“能不能干重活”。它是一个独立于页面主线程的脚本本质上是浏览器在后台维护的一个代理层可以拦截页面发出的网络请求。它运行在Worker上下文中无法直接操作DOM但可以监听install、activate、fetch、push、sync等事件。我习惯把它类比成一个快递中转站。页面要什么资源先经过中转站中转站有存货就直接给没有存货就看策略去仓库取。这中间的“存货”就是Service Worker管理的缓存。注册Service Worker的基本姿势虽然在各篇文章里都快写烂了但在Uniapp环境里有几个细节不太一样。核心问题是你的Service Worker文件应该放在哪里路径怎么填。4.2 在Uniapp中注册Service Worker先决定放哪我建议把sw.jsService Worker脚本文件放到static目录下因为前面说过static下的文件会被原样拷贝到构建产物根目录。编译后它通常会出现在/static/sw.js或者取决于你的部署路径。然后在Uniapp项目的入口文件main.js里写注册逻辑。这里有个关键选择直接写注册代码还是封装成一个模块我建议在main.js里搞一个单独函数控制只在生产环境开启避免本地开发时Service Worker缓存干扰调试。参考代码// main.js import App from ./App.vue import { createSSRApp } from vue export function createApp() { const app createSSRApp(App) // 注册 Service Worker仅在生产环境执行 if (process.env.NODE_ENV production serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/static/sw.js).then(registration { console.log(SW registered:, registration.scope) }).catch(err { console.error(SW registration failed:, err) }) }) } return { app } }这里要注意几个细节。第一serviceWorker in navigator是能力检测防止老旧浏览器报错。第二用window.addEventListener(load)先等页面资源加载完成避免SW注册请求和页面首屏资源抢带宽。第三我这里写的是/static/sw.js如果你的产品部署在子路径下请改成实际部署路径或者更稳妥的方式是用一个相对路径加上scope参数的组合。为什么我强调在生产环境才注册因为开发环境下页面一改动SW缓存还留着旧资源你会一遍一遍地被“缓存住了修改不生效”的问题折磨。开发时根本不该启用SW等调完UI和业务逻辑再开生产模式验证PWA功能这样效率最高。4.3 缓存策略设计install、activate、fetch三大件有了注册还不够。如果sw.js里什么都不写注册成功什么都做不了。一个最基础的可用版本至少要实现install、activate、fetch三个事件。我给一个可以直接上手的版本然后逐段解释每段代码在干嘛。// static/sw.js const CACHE_NAME uniapp-pwa-v1 const PRECACHE_URLS [ /, /index.html, /static/js/index.js, /static/css/index.css ] // 安装阶段预缓存核心资源 self.addEventListener(install, event { event.waitUntil( caches.open(CACHE_NAME).then(cache { return cache.addAll(PRECACHE_URLS) }) ) 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)) ) }) ) self.clients.claim() }) // 请求阶段缓存优先回退网络 self.addEventListener(fetch, event { if (event.request.method ! GET) return event.respondWith( caches.match(event.request).then(cached { if (cached) return cached return fetch(event.request).then(response { if (response response.status 200 event.request.url.startsWith(self.location.origin)) { const clone response.clone() caches.open(CACHE_NAME).then(cache cache.put(event.request, clone)) } return response }) }).catch(() { // 离线兜底如果页面缓存了返回缓存首页 if (event.request.mode navigation) { return caches.match(/index.html) } }) ) })逐段拆解install阶段预缓存列出的核心资源。这里的资源列表不能瞎写它应该和你的实际构建输出对应。Uniapp H5构建后/static/js/和/static/css/目录下会生成带hash的JS和CSS文件。问题来了你不可能每次构建都手工把hash文件名写进列表里。解决方式有两个方案A只缓存/和/index.html其他资源靠fetch阶段动态缓存。这样做简单但首次访问后第二次访问才会把所有资源缓存全。方案B写一个小脚本在构建后读取H5产物目录里的文件名自动生成预缓存列表。这更专业但属于进阶优化入门阶段先用方案A把缓存策略跑通再优化。我个人推荐的入门方案是跳过PRECACHE_URLS的完整列出只保证/和/index.html实际资源靠fetch阶段“访问即缓存”。activate阶段做旧缓存清理cacheNames.filter(name name ! CACHE_NAME)这里用了严格匹配。注意如果你修改了缓存版本号但忘了清理逻辑老缓存会一直躺在浏览器里占空间也会导致加载旧文件。这一步的重要性平时看不出来等你发布一个新版本后用户怎么都加载不到新内容时你就懂了。fetch阶段这里的策略我用了“缓存优先Cache First”。命中缓存直接返回没命中就去网络请求拿到成功应答后写入缓存。这种策略适合图片、CSS、JS这类不太会变动的静态资源。但有个大坑它会缓存HTML页面吗会。如果你把/index.html也按“缓存优先”处理那就意味着每次用户访问首页都直读缓存永远不会更新页面结构。那怎么打破这个问题最实用的做法是握手HTML走网络优先静态资源走缓存优先。可以用一个简单的判断event.request.mode navigation时走网络优先策略。参考演示// 页面导航请求网络优先失败后回退缓存 if (event.request.mode navigation) { event.respondWith( fetch(event.request).then(response { const clone response.clone() caches.open(CACHE_NAME).then(cache cache.put(/index.html, clone)) return response }).catch(() caches.match(/index.html)) ) return }把这个逻辑放在fetch事件的起始位置优先处理导航请求。这样一来用户每次打开首页都会尝试从服务器拿最新HTML即使HTML里引用的资源URL发生了hash变化也能从新HTML里发现新资源。而离线状态下则回退到上次缓存的HTML保证基本可用。4.4 版本更新与缓存清理机制PWA上线后最烦的事情就是更新。你改了一行代码打包部署到服务器但用户浏览器里的Service Worker和缓存还停留在旧版本刷新都没有用。解决这个问题的核心机制是修改CACHE_NAME版本号。比如你发布了新代码把CACHE_NAME从uniapp-pwa-v1改成uniapp-pwa-v2然后重新部署。用户的Service Worker在后台检测到新SW文件字节内容变化后会触发install事件此时新的CACHE_NAME里没有预缓存资源会重新拉取。等到activate阶段旧版本缓存被清理删除新缓存生效。但这里有个必须说的体验陷阱Service Worker默认不会立即接管页面需要页面刷新两次。第一次访问时旧的SW还在控制页面但浏览器已在后台装好了新SW等旧页面关闭第二次访问才启用新的SW。为了避免用户看到“刷新两次才更新”的诡异现象我在install事件里加了self.skipWaiting()activate里加了self.clients.claim()。这两个API的意思是新SW安装完成后立刻替换旧SW并且马上获得页面控制权。代价是如果新旧资源版本差异太大可能一瞬间页面请求还没完全切换但绝大多数场景下利大于弊。另一个坑每次修改SW代码版本号不一定要变因为浏览器是按字节比对SW文件的。你把代码改了一个字符并部署后新文件跟旧文件字节不同浏览器就会认为这是新SW。但如果业务更新只是Vue页面、接口数据SW代码本身没变那新SW就不会安装旧缓存不会更新。所以一个规范做法是业务发版时手动改一下CACHE_NAME就算SW代码没变也要主动触发一次缓存整体换新。这表面看起来有点笨但这是最可控、最不容易出幺蛾子的方式。4.5 如何调试Service Worker状态不踩坑打开Chrome DevTools切到Application面板左侧找到“Service Workers”你能看到当前页面控制的SW状态、作用域、版本号。这里注意几个按钮的用途Update强制检查并更新SW相当于手动触发浏览器的后台更新流程。Unregister注销SW之后页面恢复普通Web模式缓存的数据还在但不会再被SW劫持请求。Start/Stop调试用可以手动启停SW。还有一个非常有用的面板是“Cache Storage”里面可以看到所有缓存条目你可以手动删除某个缓存模拟用户离线或缓存损坏的场景。调试时强烈建议勾上DevTools里的“Network”面板中的“Offline”选项再刷新页面观察离线模式下页面是否依然可用。在真实项目中我调试SW时最常遇到的问题不是代码写错而是浏览器缓存了旧的SW文件。DevTools里每次看到的都是上一个版本的SW看起来代码改了没生效。这时候点一下“Update on reload”选项再勾选Network面板的“Disable cache”刷新页面后能绕过很多坑。5. Uniapp PWA 实战中的坑与心得5.1 HTTPS是硬性要求没有条件就创造条件这一点再强调都不为过。PWA的manifest和Service Worker都只有在安全上下文中才能运行。所谓安全上下文主要指HTTPS协议。localhost是个例外被视为安全上下文这也是本地调试能跑起来的原因。但你换到局域网IP、或者用HTTP域名地址访问时浏览器会直接拒绝Service Worker的注册控制台会报类似“Cannot register service worker: 只允许安全来源”的错误。生产环境如果公司没有SSL证书用certbot申请免费证书或者用云厂商的免费DV证书都能快速搞定。但如果你只是临时测试某些跨端能力不想为了验证一个PWA功能就折腾域名可以考虑用内网穿透工具把你本地服务映射成一个HTTPS地址。这个方法日常调试挺方便但千万别在生产环境依赖这类工具的免费通道稳定性没有保障。5.2 路径前缀和子目录部署的坑很多团队会把Uniapp的H5产物放到服务器子目录里比如https://example.com/web/h5/。这时候你的index.html里如果引用了/static/sw.js浏览器会请求https://example.com/static/sw.js结果404。所以所有以根路径/开头的资源引用包括start_url、scope、图标、SW路径都要按实际部署的子路径修正。最省心的做法如果条件允许把H5产物部署到域名的根路径或者一个独立子域名。比如https://app.example.com/。这样所有配置都不用改路径逻辑最简单。如果做不到你就要仔细逐一替换为带前缀的绝对路径并且确认服务端正确配置了history路由fallback否则二级路由刷新404。5.3 iOS Safari的兼容性限制iOS的PWA支持程度比Android弱不少。有些年份的系统版本里Safari虽然可以安装PWA到桌面但Service Worker后台运行和推送受限明显。你在Chrome上测得好好的功能在iOS上可能安装按钮都不出现。假如目标用户群里有大量iPhone用户建议至少用一台真机做Safari验证密切关注这几个问题display: standalone不一定生效打开后可能依然显示Safari工具栏。图标要包含apple-touch-icon否则iOS添加到主屏幕时用的是页面截图。iOS上Service Worker的缓存淘汰策略相对激进大缓存可能被系统自动清理。针对iOS还有一个老生常谈但又不得不做的补充在index.html里加apple-touch-icon指向一张180x180的PNG图标这能让iOS用户手动“添加到主屏幕”时获得相对正常的图标体验。5.4 缓存更新策略不当导致的“页面永远旧”我见过不少团队上线PWA后收到用户反馈“页面不更新”。查到最后都是SW缓存策略写成了“缓存优先且缓存了HTML”。解决思路前面已提过navigation请求走网络优先。这里再补充一个细节如果你的页面调用接口返回的数据也需要实时性请千万别把接口响应也塞进Cache Storage。在fetch事件里通常只缓存静态资源对/api/开头的请求直接放行不要拦截更不要缓存否则用户会看到旧数据而且很难排查。// 静态资源才缓存API 请求直接返回网络 if (event.request.url.includes(/api/)) return5.5 Uniapp特有的坑编译后JS路径与预缓存列表如果你想在install阶段用cache.addAll预缓存全部静态资源就必须知道Uniapp H5构建产物里到底有什么。打开dist/build/h5目录你能看到index.html、static/js/下有一堆带hash的JS文件static/css/下有带hash的CSS文件。这些hash文件名每个版本都不同。我之前试过手工把文件名写进预缓存列表结果每次发版都要更新SW非常痛苦。后来我改用“fetch时动态缓存”配合“navigation请求网络优先”完美避开这个问题。如果你还是想追求首屏秒开的极致体验那就要引入构建钩子在npm run build:h5之后自动分析index.html里的JS/CSS引用生成预缓存列表再把列表注入到sw.js。这个方案工程量稍大但效果确实最好适合资源体积大、首屏体验要求高的业务。5.6 关于“一键安装”交互的细节PWA安装按钮不是随时都会出现的。Chrome的安装条件比较严格需要用户已访问站点超过一定时长以前是30秒现在更灵活、需要有有效的manifest和SW、站点需要HTTPS。这些都满足后地址栏右侧才会出现安装图标。即便出现了用户不点也白搭。如果你想主动引导用户安装可以监听beforeinstallprompt事件在页面上做一个自定义“安装应用”按钮。这个事件在条件达成时触发你可以把它保存起来在用户点击按钮时调用prompt()弹窗。这个API在Uniapp里用起来并不复杂写在某个页面的onMounted里即可。但有个使用禁忌beforeinstallprompt事件只能使用一次触发过了就不能无限次弹窗打扰用户。所以产品设计上要给用户一个明显的安装入口但也不要疯狂弹骚扰用户。6. 适用场景判断与Nginx部署补充6.1 什么时候该用PWA什么时候该劝退PWA不是银弹。如果你的产品是强账号体系、强实时交互、强推送业务比如IM、社交信息流PWA能帮你补齐安装体验和离线缓存但推送能力和原生App的推送生态仍然有差距。反过来如果你的产品是内容展示、工具计算、报价查询这类弱交互场景PWA几乎是为你们量身定做的。还有一点如果你的目标用户大量使用旧版iOS或老旧Android设备PWA的兼容成本可能比预期高。这种情况建议做一次小范围灰度测试统计目标设备的支持率再决定是否把PWA作为核心形态推给所有用户。6.2 Nginx部署时的关键配置部署Uniapp的H5产物到Nginx核心是保证history路由模式能用、SW文件路径正确、缓存头配置合理。参考配置片段server { listen 443 ssl; server_name app.example.com; root /var/www/h5; index index.html; # history 路由 fallback location / { try_files $uri $uri/ /index.html; } # Service Worker 不能被强缓存 location /static/sw.js { add_header Cache-Control no-cache; } # 静态资源可以长缓存 location /static/ { expires 30d; add_header Cache-Control public, max-age2592000; } }重点SW文件本身必须设置no-cache否则浏览器对SW的更新检查会受HTTP缓存策略干扰可能迟迟检测不到新SW。静态资源长缓存没问题因为文件名里带hash内容变了会生成新文件名。HTML页面也建议设置no-cache让导航请求每次去服务端验证。6.3 后续还能继续扩展的PWA能力入门篇写到这你已经能跑通“安装到桌面 离线缓存”这两条主干。接着还可以往两个方向升级Web Push推送通过Service Worker监听push事件配合第三方推送通道在用户离线时也能发送消息到桌面上。这个能力在Android生态下已经很成熟iOS目前支持有限。后台同步监听sync事件在弱网环境把用户提交的表单数据保存在本地队列等网络恢复后再批量提交。这是离线友好型应用的重要拼图。这两个方向都需要在Service Worker里增加事件监听并且需要和业务逻辑做一定的适配。以Uniapp为例可以考虑将这些能力封装成公共模块在H5端运行时动态判断是否启用而不影响小程序和App端的正常逻辑。最后分享一个我反复踩过的坑Service Worker的调试经常被“缓存”两个字扰乱视线你以为代码改错了实际上浏览器用的是旧SW。每次发完版记得先到DevTools的Application面板点一下Unregister清空Cache Storage再硬刷新一次页面看到新的SW生效后再进行功能验证。这套固定流程能帮你省掉大量无谓的排查时间。顺带说一句manifest.json里display设成standalone后从桌面图标打开的页面是独立窗口但如果你在里面用window.location.href跳转其他域名窗口不会保留PWA外壳会变成普通浏览器标签页这个体验边界在设计交互流程时要尽早考虑进去。