Vue3 + Vite 实现「保存到桌面」:PWA 可安装实践与踩坑总结
标签:
PWAVue3ViteService Worker添加到主屏幕beforeinstallprompt
适合人群:H5 / 移动端前端、需要做「保存到桌面 / 安装到主屏幕」的同学
前言
很多电商、内容类 H5 希望用户点一下「保存到桌面」,手机主屏幕多出一个图标,再打开时像 App 一样(没有浏览器地址栏)。
这其实是PWA(Progressive Web App)可安装能力的一部分:
- 依赖Web App Manifest
- 依赖Service Worker
- 依赖HTTPS
- Android Chrome 等可用
beforeinstallprompt调起系统安装框 - iOS / 多数国产浏览器不能程序化一键安装,只能引导用户手动操作
本文结合 Vue3 + Vite(含 SSR、CDN base)场景,整理:原理、实现步骤、机型兼容、以及「为什么华为 Mate 40 装不上」等常见问题。
一、用户看到的效果是什么?
- 手机浏览器打开网页
- 页面有「保存到桌面 / Add to Home Screen」按钮
- 用户点击后:
- Android Chrome:弹出系统「安装应用」确认框 → 确认后桌面出现图标
- iOS Safari:无法自动添加 → 需要引导「分享 → 添加到主屏幕」
- 从桌面图标打开时,若 Manifest 配置了
display: "standalone",会以独立窗口打开,看起来不像普通浏览器页
注意:没有任何 Web API 能做到「用户点一下、完全不弹窗、静默写入桌面」。这是系统安全限制(防恶意推广)。
二、成为「可安装 PWA」的最低条件
Chrome 等浏览器通常要求:
| 条件 | 说明 |
|---|---|
| HTTPS | 生产环境必须;本地localhost可测 |
| Web App Manifest | 含name/short_name、icons(至少 192 & 512)、start_url、display |
| Service Worker | 已注册且可控当前 scope |
| 用户手势 | 调用prompt()必须由用户点击触发 |
display: "standalone"(或fullscreen)决定「像不像 App」。没有 Manifest,即便手动添加到主屏幕,也可能只是带浏览器壳的书签。
三、Manifest 怎么写?
可放在站点同源路径,例如/manifest.webmanifest:
{"id":"/","name":"Your App Name","short_name":"App","description":"App description","start_url":"/","scope":"/","display":"standalone","orientation":"portrait-primary","theme_color":"#a24acc","background_color":"#ffffff","icons":[{"src":"https://example.com/icon-192.png","sizes":"192x192","type":"image/png","purpose":"any"},{"src":"https://example.com/icon-512.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"https://example.com/icon-512.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}页面需要挂上:
<linkrel="manifest"href="/manifest.webmanifest"/><metaname="theme-color"content="#a24acc"/><metaname="apple-mobile-web-app-capable"content="yes"/><metaname="apple-mobile-web-app-title"content="App"/><linkrel="apple-touch-icon"href="/icon-192.png"/>Vite + CDNbase的坑
若vite.config里base配成 CDN 地址(例如https://img.xxx.com/cdn/app/),写在index.html里的:
<linkrel="manifest"href="/manifest.webmanifest"/>构建时可能被改写成CDN 跨域地址。而Manifest 必须同源,否则安装能力会失败。
可行做法:
- 用 SSR 模板占位符注入同源链接,例如
<!--pwa-manifest-->→/manifest.webmanifest - 或在客户端用 JS 注入:
constlink=document.createElement('link')link.rel='manifest'link.href=`${location.origin}/manifest.webmanifest`document.head.appendChild(link)同时确保 Node / Nginx 能直接返回/manifest.webmanifest和/service-worker.js,不要被 SSR 的*路由渲染成 HTML。
四、Vue3 核心:beforeinstallprompt
4.1 原理
- 页面满足可安装条件后,支持的浏览器会触发
beforeinstallprompt - 业务侧
e.preventDefault()并保存 event - 用户点击「保存到桌面」时调用
event.prompt() - 再读
userChoice看用户是否接受
4.2 Composable 示例(精简版)
// useAddToHomeScreen.tsimport{computed,onMounted,ref}from'vue'interfaceBeforeInstallPromptEventextendsEvent{prompt:()=>Promise<void>userChoice:Promise<{outcome:'accepted'|'dismissed'}>}constdeferredPrompt=ref<BeforeInstallPromptEvent|null>(null)constisStandalone=ref(false)constisIOS=ref(false)constisMobile=ref(false)constshowIOSGuide=ref(false)functioncheckStandalone(){return(window.matchMedia('(display-mode: standalone)').matches||(window.navigatorasany).standalone===true)}exportfunctioninitAddToHomeScreenListener(){// 务必尽早监听:事件可能在组件挂载前就触发,且通常只来一次window.addEventListener('beforeinstallprompt',(e)=>{e.preventDefault()deferredPrompt.value=easBeforeInstallPromptEvent})window.addEventListener('appinstalled',()=>{deferredPrompt.value=nullisStandalone.value=true})constua=navigator.userAgent isIOS.value=/iphone|ipad|ipod/i.test(ua)isMobile.value=/Android|iPhone|iPad|iPod|Mobile/i.test(ua)isStandalone.value=checkStandalone()}exportfunctionuseAddToHomeScreen(){onMounted(()=>initAddToHomeScreenListener())asyncfunctionaddToHomeScreen(){if(isStandalone.value)return{outcome:'already-installed'asconst}// Android Chrome 等:调起系统安装框if(deferredPrompt.value){awaitdeferredPrompt.value.prompt()const{outcome}=awaitdeferredPrompt.value.userChoice deferredPrompt.value=nullreturn{outcome}}// iOS:只能引导手动添加if(isIOS.value){showIOSGuide.value=truereturn{outcome:'ios-guide'asconst}}// 华为浏览器等:无 APIreturn{outcome:'unsupported'asconst}}return{isStandalone,isIOS,isMobile,showIOSGuide,canNativeInstall:computed(()=>!!deferredPrompt.value),addToHomeScreen}}4.3 按钮侧
<script setup lang="ts"> import { Toast } from 'vant' import { useAddToHomeScreen } from '@/hooks/useAddToHomeScreen' const { addToHomeScreen, showIOSGuide } = useAddToHomeScreen() async function onSave() { const { outcome } = await addToHomeScreen() if (outcome === 'unsupported') { Toast('请使用 Chrome 打开,或通过浏览器菜单添加到主屏幕') } } </script> <template> <button type="button" @click="onSave">保存到桌面</button> <!-- iOS 引导弹层:分享 → 添加到主屏幕 → 添加 --> </template>4.4 和vite-plugin-pwa的关系
很多 Vite 项目已接入vite-plugin-pwa:
- 可生成 / 注入 Service Worker
- 也可生成 Manifest
若已有自定义 SW(缓存、Push 等),可用strategies: 'injectManifest'。
若只想自己放public/manifest.webmanifest,可设manifest: false,避免和 CDN base 冲突。
五、哪些手机 / 浏览器支持?
5.1 能「半自动安装」(有beforeinstallprompt)
| 平台 | 浏览器 | 说明 |
|---|---|---|
| Android | Chrome | 最完整 |
| Android | Edge | 一般可用 |
| Android | Samsung Internet | 多数机型可用 |
| 桌面 | Chrome / Edge | 可「安装应用」 |
流程:按钮 →prompt()→系统确认框→ 用户再点一次确认。
5.2 只能「引导手动添加」
| 平台 | 浏览器 | 说明 |
|---|---|---|
| iPhone / iPad | Safari | 分享 → 添加到主屏幕 |
| iPhone / iPad | Chrome / Edge 等 | 内核限制,通常无「添加到主屏幕」,需用 Safari |
iOS没有beforeinstallprompt,JS 无法直接写桌面图标。
5.3 基本不支持一键 API
- 华为浏览器、部分国产浏览器
- 微信 / 抖音等App 内置 WebView
- Android Firefox(通常无该安装 API)
这些环境点按钮很容易走到unsupported。应引导:
- 用系统浏览器打开(尤其从微信里出来)
- 浏览器菜单 →「添加到主屏幕 / 桌面快捷方式」
- 或引导安装 Chrome 后再用一键安装
六、踩坑案例:华为 Mate 40 提示「Please open in Chrome or Safari」
现象
点击「保存到桌面」,Toast 提示请用 Chrome 或 Safari。
原因
业务逻辑大致是:
有 deferredPrompt → 调系统安装 是 iOS → 出 Safari 引导 其它 → unsupported ToastMate 40 是 Android,不是 iOS;若当前又是华为浏览器 / WebView,往往不会触发beforeinstallprompt,deferredPrompt一直为null,于是落到unsupported。
这不是「华为手机不能加桌面图标」,而是:
当前浏览器没有提供 Web 一键安装 API。
建议产品体验
对「Android 且没有deferredPrompt」不要只弹英文 Toast,应弹出和 iOS 类似的手动步骤,例如:
- 华为浏览器:右上角菜单 → 添加到主屏幕
- Chrome:菜单 → 安装应用 / 添加到主屏幕
- 微信内:右上角 → 在浏览器打开
七、能不能「点击按钮自动添加到桌面」?
| 诉求 | 是否可行 |
|---|---|
| 完全静默、无确认、任意浏览器自动写桌面 | ❌ 不可行(系统安全限制) |
| Chrome 等:一点出系统安装框,用户再确认 | ✅ H5 上限 |
| iOS:一点完成添加 | ❌ 只能引导手动 |
| 华为浏览器:一点完成添加 | ❌ 只能引导手动 |
| App / 快应用创建桌面快捷方式 | ⚠️ 走原生能力,不是纯 H5 |
结论:
H5 能做到的「自动」最多是调起系统安装确认框;做不到全机型静默添加。强需求只能考虑原生 App、厂商能力或引导用户换 Chrome / 手动添加。
八、SSR / 静态资源服务注意点
以 Fastify + Vite SSR 为例,若只挂了/assets/静态目录,根路径的:
/manifest.webmanifest/service-worker.js
可能被app.get('*')当成页面 SSR 成 HTML,导致安装失败。
应单独注册路由,返回正确Content-Type,并建议:
Cache-Control: no-cache避免 SW / Manifest 被长时间缓存导致更新困难。
九、自测清单
- Android Chrome
- DevTools → Application → Manifest 无报错
- Service Worker 为 activated
- 点击按钮出现系统安装框
- iOS Safari 真机
- 按钮弹出操作引导
- 手动添加后,桌面打开为 standalone(无地址栏)
- 华为浏览器
- 应看到手动引导,而不是「装不上就报错」
- 微信内打开
- 提示「在浏览器打开」
- Lighthouse → Progressive Web App(可选)
十、推荐落地结构(便于复用)
public/manifest.webmanifest # 同源 Manifest src/hooks/useAddToHomeScreen.ts # 监听 + 安装逻辑 src/components/AddToHomeScreen/ index.vue # 悬浮入口按钮 GuidePopup.vue # iOS / 通用手动引导 layouts/xxx # 全局挂载入口 settings/xxx # 设置页二次入口(可选) server # 提供 /manifest、/service-worker.js交互建议:
- 已是
standalone:隐藏入口 - 允许用户关闭悬浮条(localStorage 记录)
- 设置页保留常驻入口,避免关了找不到
十一、总结
- 「保存到桌面且像 App」= Manifest(
standalone)+ SW + HTTPS + 安装/引导流程。 - Vue3 核心是尽早监听
beforeinstallprompt,在用户点击时prompt()。 - iOS、华为浏览器等没有一键 API,产品上要做手动引导,而不是只 Toast。
- Vite CDN
base容易把 Manifest 变成跨域,必须同源注入。 - 不存在全平台「点一下就静默上桌面」;那是系统红线。
参考
- Web App Manifest - MDN
- How to provide your own in-app install experience - web.dev
- vite-plugin-pwa