乐于分享
好东西不私藏

Vue 项目变身 App?PWA 和 Capacitor,选哪个?

Vue 项目变身 App?PWA 和 Capacitor,选哪个?

Vue 项目做好了,老板说:能不能出个 App?

这时候你有两条路:

PWA(渐进式 Web 应用):让你的网页具备 App 的体验——可以安装到桌面、离线使用、推送通知。本质上还是网页,不需要上架应用商店。

Capacitor:把你的 Vue 项目打包成真正的原生 App,可以上架 App Store 和 Google Play,能调用摄像头、GPS、蓝牙等原生 API。

两条路都有适用场景,选错了会很痛苦。

选什么

选 PWA,如果:

  • • 不需要上架应用商店
  • • 主要场景是移动端浏览器访问
  • • 需要离线功能和桌面快捷方式
  • • 不需要调用复杂的原生设备 API
  • • 想快速上线,不想处理 App 审核

选 Capacitor,如果:

  • • 需要上架 App Store / Google Play
  • • 需要调用摄像头、蓝牙、NFC 等原生 API
  • • 需要推送通知(iOS 的推送必须走原生)
  • • 用户群体习惯从应用商店下载 App

PWA 实现

安装 vite-plugin-pwa

pnpm add -D vite-plugin-pwa

配置

// vite.config.tsimport { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import { VitePWA } from "vite-plugin-pwa";export default defineConfig({  plugins: [    vue(),    VitePWA({      registerType: "autoUpdate", // 有新版本自动更新      manifest: {        name: "我的 Vue App",        short_name: "VueApp",        description: "一个 Vue PWA 应用",        theme_color: "#42b883",        background_color: "#ffffff",        display: "standalone", // 全屏显示,像原生 App        icons: [          {            src: "/icons/icon-192.png",            sizes: "192x192",            type: "image/png",          },          {            src: "/icons/icon-512.png",            sizes: "512x512",            type: "image/png",          },        ],      },      workbox: {        // 缓存策略        runtimeCaching: [          {            urlPattern: /^https:\/\/api\.example\.com\/.*/,            handler: "NetworkFirst", // 优先网络,失败用缓存            options: {              cacheName: "api-cache",              expiration: {                maxEntries: 100,                maxAgeSeconds: 60 * 60 * 24, // 缓存 1 天              },            },          },          {            urlPattern: /\.(png|jpg|jpeg|svg|gif)$/,            handler: "CacheFirst", // 优先缓存,减少网络请求            options: {              cacheName: "image-cache",            },          },        ],      },    }),  ],});

离线提示

当用户离线时,给出友好提示:

<!-- src/components/OfflineNotice.vue --><script setup>  import { useOnline } from "@vueuse/core";  const isOnline = useOnline();</script><template>  <div v-if="!isOnline" class="offline-banner">    📡 当前处于离线状态,部分功能可能不可用  </div></template>

更新提示

有新版本时提示用户刷新:

<!-- src/App.vue --><script setup>  import { useRegisterSW } from "virtual:pwa-register/vue";  const { needRefresh, updateServiceWorker } = useRegisterSW();</script><template>  <div v-if="needRefresh" class="update-banner">    发现新版本!    <button @click="updateServiceWorker()">立即更新</button>  </div>  <RouterView /></template>

Capacitor 实现

安装

pnpm add @capacitor/core @capacitor/clipnpm add @capacitor/android @capacitor/ios

初始化

npx cap init "我的 App" "com.example.myapp"

这会在项目根目录生成 capacitor.config.ts

// capacitor.config.tsimport { CapacitorConfig } from "@capacitor/cli";const config: CapacitorConfig = {  appId: "com.example.myapp",  appName: "我的 App",  webDir: "dist", // Vite 构建输出目录  server: {    androidScheme: "https",  },};export default config;

添加平台

npx cap add androidnpx cap add ios

构建和同步

# 构建 Vue 项目pnpm build# 同步到原生项目npx cap sync# 打开 Android Studionpx cap open android# 打开 Xcodenpx cap open ios

调用原生 API

Capacitor 提供了丰富的原生插件:

pnpm add @capacitor/camera @capacitor/geolocation @capacitor/push-notifications
import { Camera, CameraResultType } from "@capacitor/camera";import { Geolocation } from "@capacitor/geolocation";// 拍照async function takePhoto() {  const photo = await Camera.getPhoto({    quality: 90,    allowEditing: false,    resultType: CameraResultType.Uri,  });  return photo.webPath;}// 获取位置async function getLocation() {  const position = await Geolocation.getCurrentPosition();  return {    lat: position.coords.latitude,    lng: position.coords.longitude,  };}

在 Vue 组件里使用:

<script setup>  import { Camera, CameraResultType } from "@capacitor/camera";  import { ref } from "vue";  const photoUrl = ref("");  async function handleTakePhoto() {    const photo = await Camera.getPhoto({      quality: 90,      resultType: CameraResultType.Uri,    });    photoUrl.value = photo.webPath;  }</script><template>  <div>    <button @click="handleTakePhoto">拍照</button>    <img v-if="photoUrl" :src="photoUrl" />  </div></template>

热更新(开发调试)

开发时不需要每次都重新构建,可以用 Capacitor 的实时重载:

// capacitor.config.ts(开发环境)const config: CapacitorConfig = {  appId: "com.example.myapp",  appName: "我的 App",  webDir: "dist",  server: {    url: "http://192.168.1.100:5173", // 你的开发机 IP    cleartext: true,  },};

这样手机上的 App 会直接加载开发服务器的内容,修改代码立即生效。

PWA vs Capacitor 对比

对比项
PWA
Capacitor
上架应用商店
原生 API 访问
有限
完整
离线支持
推送通知
部分支持
完整支持
开发成本
发布流程
简单
需要审核
iOS 支持
有限制
完整

写在最后

PWA 和 Capacitor 不是竞争关系,而是两个不同场景的解决方案。

如果你的用户主要通过浏览器访问,PWA 能用最低的成本提升移动端体验。

如果你需要上架应用商店或者调用复杂的原生 API,Capacitor 是最顺手的选择,因为你的 Vue 代码几乎不需要改动。

文档:

  • • vite-plugin-pwa: https://vite-pwa-org.netlify.app
  • • Capacitor 官网: https://capacitorjs.com

#VuePWA #Capacitor #移动端 #离线应用 #跨平台 #ServiceWorker #原生App #VitePluginPwa