Vue3 + Vite 实现「保存到桌面」:PWA 可安装实践与踩坑总结

标签:PWA Vue3 Vite Service 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 装不上」等常见问题。


一、用户看到的效果是什么?

  1. 手机浏览器打开网页
  2. 页面有「保存到桌面 / Add to Home Screen」按钮
  3. 用户点击后:
    • Android Chrome:弹出系统「安装应用」确认框 → 确认后桌面出现图标
    • iOS Safari:无法自动添加 → 需要引导「分享 → 添加到主屏幕」
  4. 从桌面图标打开时,若 Manifest 配置了 display: "standalone",会以独立窗口打开,看起来不像普通浏览器页

注意:没有任何 Web API 能做到「用户点一下、完全不弹窗、静默写入桌面」。这是系统安全限制(防恶意推广)。


二、成为「可安装 PWA」的最低条件

Chrome 等浏览器通常要求:

条件 说明
HTTPS 生产环境必须;本地 localhost 可测
Web App Manifest name / short_nameicons(至少 192 & 512)、start_urldisplay
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"
    }
  ]
}

页面需要挂上:

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#a24acc" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="App" />
<link rel="apple-touch-icon" href="/icon-192.png" />

Vite + CDN base 的坑

vite.configbase 配成 CDN 地址(例如 https://img.xxx.com/cdn/app/),写在 index.html 里的:

<link rel="manifest" href="/manifest.webmanifest" />

构建时可能被改写成 CDN 跨域地址。而 Manifest 必须同源,否则安装能力会失败。

可行做法:

  1. 用 SSR 模板占位符注入同源链接,例如 <!--pwa-manifest-->/manifest.webmanifest
  2. 或在客户端用 JS 注入:
const link = 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 原理

  1. 页面满足可安装条件后,支持的浏览器会触发 beforeinstallprompt
  2. 业务侧 e.preventDefault() 并保存 event
  3. 用户点击「保存到桌面」时调用 event.prompt()
  4. 再读 userChoice 看用户是否接受

4.2 Composable 示例(精简版)

// useAddToHomeScreen.ts
import { computed, onMounted, ref } from 'vue'

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}

const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null)
const isStandalone = ref(false)
const isIOS = ref(false)
const isMobile = ref(false)
const showIOSGuide = ref(false)

function checkStandalone() {
  return (
    window.matchMedia('(display-mode: standalone)').matches ||
    (window.navigator as any).standalone === true
  )
}

export function initAddToHomeScreenListener() {
  // 务必尽早监听:事件可能在组件挂载前就触发,且通常只来一次
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault()
    deferredPrompt.value = e as BeforeInstallPromptEvent
  })
  window.addEventListener('appinstalled', () => {
    deferredPrompt.value = null
    isStandalone.value = true
  })

  const ua = navigator.userAgent
  isIOS.value = /iphone|ipad|ipod/i.test(ua)
  isMobile.value = /Android|iPhone|iPad|iPod|Mobile/i.test(ua)
  isStandalone.value = checkStandalone()
}

export function useAddToHomeScreen() {
  onMounted(() => initAddToHomeScreenListener())

  async function addToHomeScreen() {
    if (isStandalone.value) return { outcome: 'already-installed' as const }

    // Android Chrome 等:调起系统安装框
    if (deferredPrompt.value) {
      await deferredPrompt.value.prompt()
      const { outcome } = await deferredPrompt.value.userChoice
      deferredPrompt.value = null
      return { outcome }
    }

    // iOS:只能引导手动添加
    if (isIOS.value) {
      showIOSGuide.value = true
      return { outcome: 'ios-guide' as const }
    }

    // 华为浏览器等:无 API
    return { outcome: 'unsupported' as const }
  }

  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。应引导:

  1. 用系统浏览器打开(尤其从微信里出来)
  2. 浏览器菜单 →「添加到主屏幕 / 桌面快捷方式」
  3. 或引导安装 Chrome 后再用一键安装

六、踩坑案例:华为 Mate 40 提示「Please open in Chrome or Safari」

现象

点击「保存到桌面」,Toast 提示请用 Chrome 或 Safari。

原因

业务逻辑大致是:

有 deferredPrompt  → 调系统安装
是 iOS            → 出 Safari 引导
其它              → unsupported Toast

Mate 40 是 Android,不是 iOS;若当前又是 华为浏览器 / WebView,往往 不会触发 beforeinstallpromptdeferredPrompt 一直为 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 被长时间缓存导致更新困难。


九、自测清单

  1. Android Chrome
    • DevTools → Application → Manifest 无报错
    • Service Worker 为 activated
    • 点击按钮出现系统安装框
  2. iOS Safari 真机
    • 按钮弹出操作引导
    • 手动添加后,桌面打开为 standalone(无地址栏)
  3. 华为浏览器
    • 应看到手动引导,而不是「装不上就报错」
  4. 微信内打开
    • 提示「在浏览器打开」
  5. 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 记录)
  • 设置页保留常驻入口,避免关了找不到

十一、总结

  1. 「保存到桌面且像 App」= Manifest(standalone)+ SW + HTTPS + 安装/引导流程。
  2. Vue3 核心是尽早监听 beforeinstallprompt,在用户点击时 prompt()
  3. iOS、华为浏览器等 没有一键 API,产品上要做手动引导,而不是只 Toast。
  4. Vite CDN base 容易把 Manifest 变成跨域,必须同源注入。
  5. 不存在全平台「点一下就静默上桌面」;那是系统红线。

参考


Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐