30分钟用Uniapp构建可安装的PWA离线应用
发布时间:2026/10/1 12:42:51来源:尧图网络
先说我自己的一个经历。有个朋友找我做个小工具给家里老人用的用药提醒需求特别简单打开就能看到今天的药单点一下记录服药时间到点最好能有个通知。他一开始想做小程序我说老人手机上根本不会主动去翻一堆App里的小程序入口做原生App更不划算就为这点功能过一轮应用市场审核后面还得维护两个平台。我说那你试一下 PWA。用一种叫闲着也是闲着的态度我们用 Uniapp 把界面写好构建成标准的 H5 页面再配上一套 PWA 必需的清单和 Service Worker。用户第一次打开这个网页链接时用几分钟点一下浏览器里的安装或添加到主屏幕图标就会出现在手机或桌面上。之后点图标打开是一个独立窗口的应用断网了照样能操作——这就是标题里说的可桌面安装的离线应用。这篇文章就是那套流程的完整记录零基础可以直接照着做30分钟绰绰有余。1. 30分钟的整体设计Uniapp PWA 到底在做什么1.1 PWA 不是新框架而是一套让网页App化的能力很多人听到 PWA 这个名字容易发怵觉得又是什么需要从头学的新框架。真不是。PWAProgressive Web App说白了是一组浏览器标准能力的统称它给普通网页加了三层Buff可安装、可离线、可独立运行。打个比方以前网页像个临时访客来了就住在浏览器标签页里有了 PWA 这层能力它变成了一个能拿到「居留许可」的住客可以搬进你的桌面或手机桌面有一个自己的家独立窗口没网的时候该吃饭吃饭该睡觉睡觉完全不受影响。这三层Buff分别靠三个技术点支撑第一是 HTTPS 环境这相当于一个安全通行证没有它后面两个Buff都不让你生效第二是 manifest.json 清单文件它描述了应用的长相——图标叫什么、桌面名字显示什么、打开时用什么颜色过渡、以什么窗口形式展示浏览器拿到这份清单才知道怎么把你安装出来第三是 Service Worker这是一个独立于页面的后台脚本它能拦截网络请求把静态资源缓存到本地所以离线状态下页面依然能加载。这三者缺一不可。1.2 为什么选 Uniapp而不是直接写纯 HTML 或小程序先排除小程序的原因很简单小程序的运行依赖微信里那个壳用户必须先进微信才能用。这种依赖对高频常用工具来说路径太长。PWA 的入口是一个链接出现在浏览器里安装后就是一个独立图标符合用完即走、但走了还能找到的工具类应用定位。那为什么不直接用纯 HTML 写这看场景。只做一个静态页面纯 HTML 当然更轻。但一旦开始涉及页面状态、数据交互、多页面跳转以及后续可能要上架微信小程序、App纯 HTML 的工程化效率和 Uniapp 没法比。Uniapp 的独特价值在于一处编写多端输出。它编译到 H5 端时就是一个标准网页天然适配 PWA 的所有能力而未来如果想把同样的业务搬到微信小程序或打包成安卓、iOS 应用Ui 组件和业务逻辑几乎不用重写。对大多数学着做着就想扩展的人来说这是一条性价比极高的路线先快速验证 PWA同时保留了将来多端发布的退路。1.3 这30分钟的路程分成四段为了避免开头五分钟就卡在某个细节里我先给整个流程画一条清晰的路径你心里有数之后每一步走起来都不会慌。时间段要完成的事产出物0-5分钟安装工具、新建 Uniapp 项目一个能在浏览器运行的空项目5-15分钟写一个简单页面并配置 PWA 清单带图标、名称、主题色的可安装元数据15-20分钟注册 Service Worker 并配置离线缓存断网可打开的应用壳20-30分钟部署到 HTTPS、桌面安装、断网验证一个真正能用的离线应用这条路径的关键是先把链路跑通再优化细节。很多入门者喜欢一开始就抠图标像素、琢磨缓存版本号结果到最后环境都没起来。先让整体循环转一圈你才会真正理解每个配置到底影响了什么。2. 五分钟把 Uniapp 项目跑起来2.1 工具选择HBuilderX 还是命令行创建 Uniapp 项目有两条路官方 IDEHBuilderX或者CLI 方式Vue3 Vite。对零基础入门来说我更推荐 HBuilderX原因只有一个它把新建项目、编译到 H5、浏览器预览这些操作全部收进图形界面里不需要额外配置 Node 环境变量也不会因为 npm 包版本不一致导致一脸懵。下载 HBuilderX 后工具会自动内置对应版本的编译器打开就能用。CLI 方式适合本来就熟悉 npm 生态的前端开发它的优势是可以深度定制比如接入自定义的构建插件。但 30 分钟时限内我建议就走 HBuilderX先把 PWA 这件事的核心逻辑吃透。等你对构建流程熟悉了再切换到 CLI 也不迟。2.2 新建项目的正确姿势打开 HBuilderX依次点文件 - 新建 - 项目。弹出的窗口里项目类型选择uni-app 项目模板选择默认空白模板。这里有个小细节如果你打算用 Vue3 的语法风格就选界面上标着 Vue3 的那个默认模板如果对 Vue2 有偏好也可以选 Vue2 的。入门阶段二者影响不大本文示例以 Vue3 模板为准。项目名称建议直接用英文加短横线的格式比如pwa-todo-app。因为项目名会成为很多路径和标识符的基底如果带中文或空格后面编译和部署时容易出奇怪的大小写、编码问题。项目路径选一个你记得住、且不容易被文件夹权限卡住的地方避免装在系统保护目录里。2.3 跑通 H5 编译链路新建完项目后先不急着写代码。点击 HBuilderX 工具栏上的运行 - 运行到浏览器 - Chrome。如果一切正常浏览器会自动打开一个页面显示 Uniapp 默认的模板内容。这一步的意义在于确认三个层面没有问题编译器版本正常、项目结构完整、Chrome 能正常访问本地的开发服务端口。第一轮编译可能会慢一些因为编译器要生成依赖预构建产物后续改动基本就是秒级热更新。跑通后打开浏览器的开发者工具切到控制台你应该能看到正常的项目日志没有任何报红。这就算项目地基打好了。2.4 写一个能看的最小页面既然要做离线应用我们先放一个最直观的内容用来验证后续 PWA 的离线效果。在pages/index/index.vue里把模板替换成下面这一段简单展示一行文字和一张图片template view classcontainer image classlogo src/static/pwa-demo.svg modeaspectFit/image text classtitle离线待办清单/text button typeprimary clickshowInfo点击我试试/button /view /template你还得在static目录下放一张名为pwa-demo.svg的简单图片纯色圆角矩形加一行文字即可。这个图有两个作用一是让页面看起来不那么单调二是后面验证离线缓存时如果图片在断网后依然能显示说明静态资源缓存生效了。如果现在不想切图直接放一张 400 x 400 的 PNG 文件也行关键是路径正确。3. 配置 PWA 清单让浏览器认识你的应用3.1 manifest.json 里每个字段是干什么的PWA 的身份档案就是 manifest.json。浏览器安装应用时会逐条读取这份 JSON字段缺失或格式不对安装按钮就可能不出现。目前 Chrome、Edge、Firefox 以及 iOS 的 Safari 对 manifest 的标准兼容度已经很高但不同平台对某些字段的依赖程度仍有差异。我把最常用的字段用表格拆一遍。字段作用实际建议name应用全称安装时展示直接用产品名最长别超过30个字符short_name桌面图标下方显示的名称越短越好比如待办start_url点击桌面图标后的相对入口一般设为/路径模式别写绝对URLdisplay启动后的展示模式standalone独立窗口fullscreen全屏background_color启动瞬间的背景色与页面首个画面背景一致避免闪烁跳变theme_color浏览器UI和启动画面的主题色与产品主色调一致icons安装图标数组至少给192px和512px两种尺寸orientation锁定屏幕方向小工具类可锁定portrait通用类不设scope控制哪些路径算作应用范围默认和 start_url 同级子目录部署时注意这些字段里最容易翻车的是icons。如果你只放了一张 192 的图标而 Chrome 希望安装窗口看到一个 512 的资源它不会自动放大192再冒充512而是直接不弹安装提示。所以我的做法是在 static 目录下建一个pwa子目录统一放icon-192.png、icon-512.png、apple-touch-icon-180.png后续更新时好维护。3.2 在 Uniapp 里配置 h5.pwa 节点Uniapp 所有端相关的配置都收在根目录的manifest.json里。H5 端的配置在h5节点下PWA 相关的字段则是h5.pwa子节点。你不需要手写整份 JSON——在 HBuilderX 里双击manifest.json切换到可视化界面找到H5配置标签页展开后通常能看到 PWA 相关的输入区域。不同 HBuilderX 版本界面略有差异但核心字段是一致的。如果你更喜欢直接用源码编辑也可以在manifest.json里把它完整的写成这样{ name: 离线待办清单, appid: , h5: { title: 离线待办清单, router: { mode: history }, pwa: { name: 离线待办, short_name: 待办, description: 一个可以离线使用的待办清单工具, background_color: #ffffff, theme_color: #4D67F6, 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 } ] } } }这里有两个值得注意的配置点。第一router.mode我特意用了history路由里不再有#符号PWA 启动时的地址更干净离线场景下的缓存命中更稳定。第二pwa.icons里的 512 图标我加了purpose: any maskable这是为了适配 Android 自适应图标的需求。如果漏了 maskable部分安卓机型安装后图标会被强行裁切看起来像缺了一截。3.3 图标生成与 iOS 专用标签刚才说了图标要 192 和 512 两种。很多人看到这里会问我该用什么工具生成就入门而言用 HBuilderX 自带的图标生成能力就够准备一张 1024 x 1024 的方形源图可以是设计稿截图、用在线裁剪工具切出来的纯色底图然后放进图标配置页生成所有尺寸。记得图标主体内容居中偏内一点四周留白 30% 以上因为安卓的 maskable 会做圆形裁切。但还有一个坑只有打包部署后才会遇到iOS 的 Safari 不完全读 manifest 里的 icons。它特别认apple-touch-icon这个link标签。所以你需要自定义 Uniapp 的 H5 模板。方法是在项目根目录创建template.h5.html然后在manifest.json的h5.template里指向它。模板内容可以基于默认模板再补三个 iOS 专用标签meta nameapple-mobile-web-app-capable contentyes meta nameapple-mobile-web-app-status-bar-style contentdefault meta nameapple-mobile-web-app-title content待办 link relapple-touch-icon href/static/pwa/apple-touch-icon-180.png第一行让 iOS 进入全屏独立应用模式第二行控制的是 iPhone 顶部状态栏样式default表示白底黑字和浅色页面更搭。第三行是桌面图标的指定入口。你不写这三个标签iOS 用户从 Safari 里点添加到主屏幕也能装但图标会变成网页截图状态栏还是浏览器那条很出戏。4. Service Worker 离线缓存核心攻坚4.1 先理解 Service Worker 的生命周期PWA 里最不直观的就是 Service Worker因为它不跑在页面里而是由浏览器在后台单独管理的一个脚本。它的生命周期分三个阶段安装install、激活activate、拦截请求fetch。安装阶段适合把首次打开必需的文件预存进缓存。激活阶段适合清理旧缓存——比如你发布了新版本旧版本的缓存就该在这里统一删除。之后浏览器每发起一个网络请求都会先经过 fetch 监听器你在这里决定来源直接走缓存、直接走网络、还是先网络后缓存。理解这条链路后你就掌握了整个离线能力的控制权。我经常跟朋友说Service Worker 就是给网页配了个私人管家你提前告诉管家哪些东西收进储藏室、哪些每次都要现买之后无论断不断网管家都会按规矩办事。4.2 Uniapp 官方内置的 PWA 方案开箱即用配置完h5.pwa后HBuilderX 编译时会在 H5 产物里自动生成配套的manifest.json和sw.js并且在入口 HTML 里自动注册。这意味着你用最小配置就能得到一个具备离线能力的 PWA 应用。首次访问页面时Service Worker 会把当前的入口页面和静态资源缓存下来后续断网时页面依然能启动。这个内置方案的好处是零代码适合跑通流程。但它的缓存策略相对简单更新时机可能不完全贴合你的预期。具体表现是内容更新后用户可能还需要一次在线刷新或重启才能看到新版本。这里我补充说明官方内置方案更适合先让东西活起来的教学场景如果你对缓存更新要求高就用接下来这个自定义方案。4.3 自己写一份可控的 sw.js可直接抄我更推荐第二种方式把 Service Worker 的缓存策略拿回自己手里。步骤是先停用h5.pwa里的自动注册或者干脆不在 manifest 里配置 PWA 字段改为手动控制。具体做法在项目static目录下新建sw.js然后在pages/index/index.vue的onLoad或入口main.js里注册。先看sw.js的内容这是我自己在项目里一直在用的版本可以直接抄const CACHE_NAME todo-pwa-v1; const PRECACHE_URLS [/, /index.html]; self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) cache.addAll(PRECACHE_URLS)) ); self.skipWaiting(); }); self.addEventListener(activate, (event) { const cacheNames []; event.waitUntil( caches.keys().then((keys) Promise.all( keys.filter((key) key ! CACHE_NAME) .map((key) caches.delete(key)) )) ); self.clients.claim(); }); self.addEventListener(fetch, (event) { const request event.request; const url new URL(request.url); // 非 GET 请求和跨域请求直接放行不拦截 if (request.method ! GET || 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(request).then((res) res || caches.match(/index.html))) ); return; } // 其他静态资源缓存优先缓存未命中再走网络并写回缓存 event.respondWith( caches.match(request).then((cached) { const network fetch(request) .then((response) { if (response response.status 200) { const copy response.clone(); caches.open(CACHE_NAME).then((cache) cache.put(request, copy)); } return response; }) .catch(() cached); return cached || network; }) ); });这份脚本的核心逻辑是页面导航请求采用网络优先、缓存兜底保证用户每次打开都尽量拿到最新页面不带 hash 的静态资源采用缓存优先、网络兜底因为 Uniapp 编译出来的 JS、CSS 文件名都带 hash这些文件内容一旦变化就不会复用了。注册方式把下面的代码放进main.jsif (serviceWorker in navigator process.env.NODE_ENV production) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js).catch((err) { console.error(Service Worker 注册失败, err); }); }); }这里我用process.env.NODE_ENV production做了一个条件判断。为什么因为本地开发时HBuilderX 的预览服务端口是 http 且地址随时变化Service Worker 注册后会干扰开发时的热刷新逻辑。只在构建生产包时注册是最好的实践。4.4 缓存版本号与更新策略的取舍注意到脚本开头那个CACHE_NAME了吗它叫todo-pwa-v1这个 v1 就是缓存版本号。当你后续更新了网站内容记得把版本号手动改成 v2、v3。激活阶段会清掉旧的 v1 等历史缓存这样用户不会一直用旧数据。这个步骤很容易被漏掉一旦漏掉你明明发布了新版本用户界面上看到的还是几周前的旧界面又找不到原因非常尴尬。另一种方案是给 HTML 里每个静态资源自动附带 hash 版本Uniapp 构建时已经天然满足这一点。正因如此我才推荐缓存优先策略——文件名变了缓存自然不命中浏览器就会去网络拿新文件。这就是一套不需要额外版本管理也能自洽的缓存组合拳。5. 部署到 HTTPS 并验证桌面安装5.1 本地调试看效果用这两个手段在本地把 Service Worker 跑起来需要一点技巧因为正规的 Service Worker 必须要 HTTPS但localhost和127.0.0.1是浏览器特别豁免的调试地址。如果你是通过 HBuilderX 内置预览来跑默认地址是localhost:端口这个可以注册 Service Worker。打开 Chrome DevTools 的Application面板左侧能找到 Service Workers、Manifest、Cache Storage 三块分别对应 PWA 的三个支柱。在 Cache Storage 里你能看到当前缓存了哪些请求、占了多少字节Service Workers 里能看到当前脚本的状态和更新按钮。更新完 sw.js 后必须到这里手动点一次 Update或者关闭再打开页面刷新否则浏览器不会重新拉取脚本。这是我见过最多人踩的坑。5.2 部署上线HTTPS 与路径配置本地调试完成后构建生产包。在 HBuilderX 里执行发行 - 网站-H5手机版产物一般在项目目录的dist/build/h5。这个目录就是一套纯静态资源部署到任意支持 HTTPS 的静态空间就能生效。部署时有一个常见问题如果你的站点在子路径下比如https://example.com/todo/需要同步修改两处。第一是manifest.json里的start_url和scope改为/todo/第二是router.base要设置为/todo/。如果没有改Service Worker 的scope默认是/todo/sw.js所在的目录它控制不到根路径下的所有页面安装后打不开或者离线失效很正常。我建议项目根目录下的page.json或者manifest.json中尽早统一配置避免部署后再花时间排查。关于 HTTPS 证书用常规的免费证书方案就行。静态托管服务通常自带 HTTPS不需要自己配置证书。你需要做的只是把dist/build/h5里的内容上传上去然后保证访问域名和你配置的start_url一致。5.3 桌面安装和断网验证全流程部署完成后在 Chrome 或 Edge 里访问你的站点地址栏右侧会出现一个电脑加下载箭头的图标点击它就能安装。如果没出现检查Application面板里的 Manifest 项那里会明确告诉你字段哪里不合法。安装完成后桌面上会出现一个独立图标点击后是单独的窗口不再有浏览器地址栏。此时做离线验证回到浏览器打开 DevTools 的 Network 面板把No throttling切到Offline然后刷新页面。如果页面完整加载、图片这张还能显示说明缓存生效。手机端的验证更简单打开页面后先在联网状态下加载一遍然后开飞行模式从桌面图标重新打开应用一样能访问。6. 常见问题与避坑实录6.1 排查速查表做 PWA 就是这样前置条件一堆最后出问题往往不是某一个点而是三个环节里某个细节断了。我把这半年来自用和帮别人排查时遇到最多的几个问题列成一张表收藏起来能省不少时间。问题表现最常见原因解决办法地址栏没有安装图标manifest 缺少 192 或 512 图标没有注册成功的 Service Worker在 Application 里查看 Manifest 报错确认 SW 状态为 activated桌面图标是网页截图iOS 没有 apple-touch-iconAndroid 图标尺寸不满足 maskable补充 iOS 专用 meta 和 link重新配置 maskable 图标断网后打开白屏入口 HTML 未被缓存start_url 和缓存路径不一致用 Network 离线模式观察 Cache Storage 里是否有入口路径更新后用户一直看到旧界面忘记改缓存版本号导航请求没有走网络优先策略更新 CACHE_NAME 版本确认 fetch 里 navigate 分支走网络部署到子目录后安装失效start_url、scope、router.base 三者没同步三个地方统一前缀重新编译部署6.2 我踩过的几个坑希望你别再踩第一个坑本地预览时不小心把 Service Worker 注册到了开发环境。热更新模块一旦被 Service Worker 缓存住页面永远显示旧代码还以为是编译器坏了。我的对策就是前面提到的那句process.env.NODE_ENV production从根上避开。第二个坑缓存了不该缓存的接口数据。如果你对接口请求也用了缓存优先策略用户的数据可能一直显示旧的看起来像接口挂了。我现在的习惯是接口一律走网络只有这种内容不怎么变的静态文件才走缓存优先。工具类应用里页面数据和业务状态一旦被离线缓存干扰体验会很糟糕。第三个坑发布新版本忘了让 Service Worker 立即生效。我的习惯是每次发布前把CACHE_NAME里的版本号换成当天的日期比如todo-pwa-20250610。这样激活阶段就会自动清理旧缓存确保用户能拿到新版本。断网用户不在线也不会出错因为他们依旧读旧缓存等下次联网后版本号变化再迁移。6.3 一个关于扩展方向的小建议PWA 这条路走到这里其实只是打开了第一扇门。你后续可以给这个离线应用加上通知能力当用户在线时订阅 Push服务端推送提醒消息也可以接入 IndexedDB 把用户操作的数据也存到本地做一个完全脱离网络也能完整记账、记录、编辑的应用。Uniapp 的生态已经把这些基础设施都铺好了你只需要沿着 PWA 这条主线继续往前。我个人在实际操作中的体会是PWA 不是用来取代原生 App 的它最适合的场景永远是轻量、低频、不想让用户经历下载安装流程的那一类工具。用 Uniapp 做 PWA等于你用一套代码同时保住了 Web 的触达和 App 的体验再加上离线缓存兜底一个合格的个人工具就这么立起来了。如果你也正好有个小工具的想法别犹豫照这篇文章把第一个版本跑通再说。
网站建设高端定制企业官网