capacitor-vue

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Capacitor with Vue

Capacitor 结合 Vue 开发指南

Vue-specific patterns and best practices for Capacitor app development — Composition API, composables, reactivity, lifecycle hooks, Vue Router integration, and framework-specific guidance for Quasar and Nuxt.
Vue专属的Capacitor应用开发模式与最佳实践——包括Composition API、composables、响应式、生命周期钩子、Vue Router集成,以及针对Quasar和Nuxt的框架专属指导。

Prerequisites

前置条件

  1. Capacitor 6, 7, or 8 app with Vue 3.
  2. Node.js and npm installed.
  3. For iOS: Xcode installed.
  4. For Android: Android Studio installed.
  1. Capacitor 6、7或8 版本的Vue 3项目。
  2. 已安装Node.js和npm。
  3. 针对iOS开发:已安装Xcode。
  4. 针对Android开发:已安装Android Studio。

Agent Behavior

Agent 行为规范

  • Auto-detect before asking. Check the project for
    vite.config.ts
    ,
    vite.config.js
    ,
    quasar.config.js
    ,
    quasar.config.ts
    ,
    nuxt.config.ts
    ,
    package.json
    ,
    capacitor.config.ts
    or
    capacitor.config.json
    , and existing directory structure. Only ask the user when something cannot be detected.
  • Guide step-by-step. Walk the user through the process one step at a time.
  • Detect the meta-framework. Determine whether the project uses plain Vue (Vite), Quasar, or Nuxt, and adapt instructions accordingly.
  • 自动检测优先,按需询问:检查项目中的
    vite.config.ts
    vite.config.js
    quasar.config.js
    quasar.config.ts
    nuxt.config.ts
    package.json
    capacitor.config.ts
    capacitor.config.json
    文件,以及现有目录结构。仅当无法自动检测到信息时,才向用户询问。
  • 分步引导:逐步引导用户完成操作流程。
  • 检测元框架:判断项目使用的是纯Vue(Vite)、Quasar还是Nuxt,并据此调整指导内容。

Procedures

操作步骤

Step 1: Analyze the Project

步骤1:分析项目

Auto-detect the following by reading project files:
  1. Vue version: Read
    vue
    version from
    package.json
    .
  2. Capacitor version: Read
    @capacitor/core
    version from
    package.json
    . If not present, Capacitor has not been added yet — proceed to Step 2.
  3. Meta-framework: Detect the framework by checking for these files in order:
    • quasar.config.js
      or
      quasar.config.ts
      Quasar project. Proceed to
      references/quasar.md
      .
    • nuxt.config.ts
      or
      nuxt.config.js
      Nuxt project. Proceed to
      references/nuxt.md
      .
    • vite.config.ts
      or
      vite.config.js
      Plain Vue (Vite) project. Continue with the steps below.
  4. Platforms: Check which directories exist (
    android/
    ,
    ios/
    ).
  5. Capacitor config format: Check for
    capacitor.config.ts
    (TypeScript) or
    capacitor.config.json
    (JSON).
  6. Build output directory: Read
    build.outDir
    from
    vite.config.ts
    or
    vite.config.js
    . The default is
    dist
    .
通过读取项目文件自动检测以下信息:
  1. Vue版本:从
    package.json
    中读取
    vue
    的版本。
  2. Capacitor版本:从
    package.json
    中读取
    @capacitor/core
    的版本。如果未找到该依赖,说明Capacitor尚未添加——请继续步骤2。
  3. 元框架:按以下顺序检查文件以识别框架:
    • 存在
      quasar.config.js
      quasar.config.ts
      —— Quasar 项目,请参考
      references/quasar.md
    • 存在
      nuxt.config.ts
      nuxt.config.js
      —— Nuxt 项目,请参考
      references/nuxt.md
    • 存在
      vite.config.ts
      vite.config.js
      —— 纯Vue(Vite) 项目,请继续以下步骤。
  4. 目标平台:检查是否存在
    android/
    ios/
    目录。
  5. Capacitor配置格式:检查是
    capacitor.config.ts
    (TypeScript格式)还是
    capacitor.config.json
    (JSON格式)。
  6. 构建输出目录:从
    vite.config.ts
    vite.config.js
    中读取
    build.outDir
    的值,默认是
    dist

Step 2: Add Capacitor to a Vue Project

步骤2:为Vue项目添加Capacitor

Skip if
@capacitor/core
is already in
package.json
. Skip if the project uses Quasar (Quasar has its own Capacitor integration — see
references/quasar.md
).
  1. Install Capacitor core and CLI:
    bash
    npm install @capacitor/core
    npm install -D @capacitor/cli
  2. Initialize Capacitor:
    bash
    npx cap init
    When prompted, set the web directory to the Vue build output path detected in Step 1. For Vite-based Vue projects, this is typically
    dist
    .
  3. Verify the
    webDir
    value in the generated
    capacitor.config.ts
    or
    capacitor.config.json
    matches the Vue build output path. If incorrect, update it:
    capacitor.config.ts
    :
    typescript
    import type { CapacitorConfig } from '@capacitor/cli';
    
    const config: CapacitorConfig = {
      appId: 'com.example.app',
      appName: 'my-app',
      webDir: 'dist',
    };
    
    export default config;
    capacitor.config.json
    :
    json
    {
      "appId": "com.example.app",
      "appName": "my-app",
      "webDir": "dist"
    }
  4. Build the Vue app and add platforms:
    bash
    npm run build
    npm install @capacitor/android @capacitor/ios
    npx cap add android
    npx cap add ios
    npx cap sync
如果
package.json
中已存在
@capacitor/core
,请跳过此步骤。如果项目使用Quasar(Quasar有专属的Capacitor集成方式——请参考
references/quasar.md
),也请跳过此步骤。
  1. 安装Capacitor核心库和CLI:
    bash
    npm install @capacitor/core
    npm install -D @capacitor/cli
  2. 初始化Capacitor:
    bash
    npx cap init
    当出现提示时,将web目录设置为步骤1中检测到的Vue构建输出路径。对于基于Vite的Vue项目,通常为
    dist
  3. 验证生成的
    capacitor.config.ts
    capacitor.config.json
    中的
    webDir
    值是否与Vue构建输出路径匹配。如果不匹配,请进行更新:
    capacitor.config.ts
    :
    typescript
    import type { CapacitorConfig } from '@capacitor/cli';
    
    const config: CapacitorConfig = {
      appId: 'com.example.app',
      appName: 'my-app',
      webDir: 'dist',
    };
    
    export default config;
    capacitor.config.json
    :
    json
    {
      "appId": "com.example.app",
      "appName": "my-app",
      "webDir": "dist"
    }
  4. 构建Vue应用并添加目标平台:
    bash
    npm run build
    npm install @capacitor/android @capacitor/ios
    npx cap add android
    npx cap add ios
    npx cap sync

Step 3: Project Structure

步骤3:项目结构

A Capacitor Vue project (Vite-based) has this structure:
my-app/
├── android/                  # Android native project
├── ios/                      # iOS native project
├── public/
├── src/
│   ├── assets/
│   ├── components/
│   ├── composables/          # Custom composables for Capacitor plugins
│   ├── router/
│   │   └── index.ts          # Vue Router configuration
│   ├── views/
│   ├── App.vue
│   └── main.ts
├── capacitor.config.ts       # or capacitor.config.json
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
Key points:
  • The
    android/
    and
    ios/
    directories contain native projects and should be committed to version control.
  • The
    src/
    directory contains the Vue app, which is the web layer of the Capacitor app.
  • Place custom composables that wrap Capacitor plugins in
    src/composables/
    .
  • Capacitor plugins are called from Vue components or composables inside
    src/
    .
基于Vite的Capacitor Vue项目结构如下:
my-app/
├── android/                  # Android原生项目
├── ios/                      # iOS原生项目
├── public/
├── src/
│   ├── assets/
│   ├── components/
│   ├── composables/          # 封装Capacitor插件的自定义composables
│   ├── router/
│   │   └── index.ts          # Vue Router配置文件
│   ├── views/
│   ├── App.vue
│   └── main.ts
├── capacitor.config.ts       # 或capacitor.config.json
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
关键要点:
  • android/
    ios/
    目录包含原生项目,应提交到版本控制系统中。
  • src/
    目录包含Vue应用代码,是Capacitor应用的Web层。
  • 封装Capacitor插件的自定义composables应放在
    src/composables/
    目录下。
  • Capacitor插件应在
    src/
    目录内的Vue组件或composables中调用。

Step 4: Using Capacitor Plugins in Vue

步骤4:在Vue中使用Capacitor插件

Capacitor plugins are plain TypeScript APIs. Import and call them directly in Vue components using the Composition API.
Capacitor插件是纯TypeScript API,可以在Vue组件中直接导入并结合Composition API使用。

Direct Usage in a Component

在组件中直接使用

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Geolocation } from '@capacitor/geolocation';

const latitude = ref<number | null>(null);
const longitude = ref<number | null>(null);

async function getCurrentPosition() {
  const position = await Geolocation.getCurrentPosition();
  latitude.value = position.coords.latitude;
  longitude.value = position.coords.longitude;
}
</script>

<template>
  <div>
    <p>Latitude: {{ latitude }}</p>
    <p>Longitude: {{ longitude }}</p>
    <button @click="getCurrentPosition">Get Location</button>
  </div>
</template>
vue
<script setup lang="ts">
import { ref } from 'vue';
import { Geolocation } from '@capacitor/geolocation';

const latitude = ref<number | null>(null);
const longitude = ref<number | null>(null);

async function getCurrentPosition() {
  const position = await Geolocation.getCurrentPosition();
  latitude.value = position.coords.latitude;
  longitude.value = position.coords.longitude;
}
</script>

<template>
  <div>
    <p>纬度:{{ latitude }}</p>
    <p>经度:{{ longitude }}</p>
    <button @click="getCurrentPosition">获取位置</button>
  </div>
</template>

Wrapping Plugins in Composables (Recommended)

将插件封装到Composables中(推荐)

Wrapping Capacitor plugins in composables provides reusability, encapsulated reactive state, and automatic cleanup:
typescript
// src/composables/useCamera.ts
import { ref } from 'vue';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';

export function useCamera() {
  const photo = ref<Photo | null>(null);
  const error = ref<string | null>(null);

  async function takePhoto(): Promise<void> {
    try {
      error.value = null;
      photo.value = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera,
      });
    } catch (e) {
      error.value = e instanceof Error ? e.message : String(e);
    }
  }

  async function pickFromGallery(): Promise<void> {
    try {
      error.value = null;
      photo.value = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.Uri,
        source: CameraSource.Photos,
      });
    } catch (e) {
      error.value = e instanceof Error ? e.message : String(e);
    }
  }

  return {
    photo,
    error,
    takePhoto,
    pickFromGallery,
  };
}
Use the composable in a component:
vue
<script setup lang="ts">
import { useCamera } from '@/composables/useCamera';

const { photo, error, takePhoto } = useCamera();
</script>

<template>
  <div>
    <button @click="takePhoto">Take Photo</button>
    <p v-if="error">Error: {{ error }}</p>
    <img v-if="photo?.webPath" :src="photo.webPath" alt="Captured photo" />
  </div>
</template>
将Capacitor插件封装到composables中可以提升复用性、封装响应式状态并实现自动清理:
typescript
// src/composables/useCamera.ts
import { ref } from 'vue';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';

export function useCamera() {
  const photo = ref<Photo | null>(null);
  const error = ref<string | null>(null);

  async function takePhoto(): Promise<void> {
    try {
      error.value = null;
      photo.value = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera,
      });
    } catch (e) {
      error.value = e instanceof Error ? e.message : String(e);
    }
  }

  async function pickFromGallery(): Promise<void> {
    try {
      error.value = null;
      photo.value = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.Uri,
        source: CameraSource.Photos,
      });
    } catch (e) {
      error.value = e instanceof Error ? e.message : String(e);
    }
  }

  return {
    photo,
    error,
    takePhoto,
    pickFromGallery,
  };
}
在组件中使用该composable:
vue
<script setup lang="ts">
import { useCamera } from '@/composables/useCamera';

const { photo, error, takePhoto } = useCamera();
</script>

<template>
  <div>
    <button @click="takePhoto">拍照</button>
    <p v-if="error">错误:{{ error }}</p>
    <img v-if="photo?.webPath" :src="photo.webPath" alt="拍摄的照片" />
  </div>
</template>

Step 5: Plugin Event Listeners with Lifecycle Hooks

步骤5:结合生命周期钩子使用插件事件监听器

Capacitor plugin event listeners must be registered in
onMounted
and removed in
onUnmounted
to prevent memory leaks. Vue's reactivity system picks up
ref
changes automatically, so there is no NgZone-equivalent issue — but cleanup is still critical.
Capacitor插件的事件监听器必须在
onMounted
中注册,并在
onUnmounted
中移除,以防止内存泄漏。Vue的响应式系统会自动处理
ref
的变化,因此不存在类似NgZone的问题,但清理操作仍然至关重要。

Composable with Automatic Cleanup

带有自动清理的Composable

typescript
// src/composables/useNetwork.ts
import { ref, onMounted, onUnmounted } from 'vue';
import { Network } from '@capacitor/network';
import type { ConnectionStatus } from '@capacitor/network';
import type { PluginListenerHandle } from '@capacitor/core';

export function useNetwork() {
  const status = ref<ConnectionStatus | null>(null);
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    status.value = await Network.getStatus();
    listenerHandle = await Network.addListener('networkStatusChange', (newStatus) => {
      status.value = newStatus;
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });

  return {
    status,
  };
}
Usage in a component:
vue
<script setup lang="ts">
import { useNetwork } from '@/composables/useNetwork';

const { status } = useNetwork();
</script>

<template>
  <p v-if="status">Network: {{ status.connected ? 'Online' : 'Offline' }}</p>
</template>
typescript
// src/composables/useNetwork.ts
import { ref, onMounted, onUnmounted } from 'vue';
import { Network } from '@capacitor/network';
import type { ConnectionStatus } from '@capacitor/network';
import type { PluginListenerHandle } from '@capacitor/core';

export function useNetwork() {
  const status = ref<ConnectionStatus | null>(null);
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    status.value = await Network.getStatus();
    listenerHandle = await Network.addListener('networkStatusChange', (newStatus) => {
      status.value = newStatus;
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });

  return {
    status,
  };
}
在组件中使用:
vue
<script setup lang="ts">
import { useNetwork } from '@/composables/useNetwork';

const { status } = useNetwork();
</script>

<template>
  <p v-if="status">网络状态:{{ status.connected ? '在线' : '离线' }}</p>
</template>

App-Wide Listeners via App.vue

通过App.vue注册全局监听器

For listeners that should persist for the entire app lifecycle (e.g., app state changes), register them in
App.vue
:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { App } from '@capacitor/app';
import type { PluginListenerHandle } from '@capacitor/core';
import { RouterView } from 'vue-router';

let appStateListener: PluginListenerHandle | null = null;

onMounted(async () => {
  appStateListener = await App.addListener('appStateChange', (state) => {
    console.log('App state changed. Is active:', state.isActive);
  });
});

onUnmounted(async () => {
  await appStateListener?.remove();
});
</script>

<template>
  <RouterView />
</template>
对于需要在整个应用生命周期中持续生效的监听器(如应用状态变化),可以在
App.vue
中注册:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { App } from '@capacitor/app';
import type { PluginListenerHandle } from '@capacitor/core';
import { RouterView } from 'vue-router';

let appStateListener: PluginListenerHandle | null = null;

onMounted(async () => {
  appStateListener = await App.addListener('appStateChange', (state) => {
    console.log('应用状态已变更,当前是否活跃:', state.isActive);
  });
});

onUnmounted(async () => {
  await appStateListener?.remove();
});
</script>

<template>
  <RouterView />
</template>

Step 6: Platform Detection

步骤6:平台检测

Use
Capacitor.isNativePlatform()
and
Capacitor.getPlatform()
to conditionally run native-only code. Wrap this in a composable for reuse:
typescript
// src/composables/usePlatform.ts
import { Capacitor } from '@capacitor/core';

export function usePlatform() {
  const platform = Capacitor.getPlatform() as 'web' | 'ios' | 'android';
  const isNative = Capacitor.isNativePlatform();
  const isIos = platform === 'ios';
  const isAndroid = platform === 'android';
  const isWeb = platform === 'web';

  return {
    platform,
    isNative,
    isIos,
    isAndroid,
    isWeb,
  };
}
Use it in components to show/hide native-only features:
vue
<script setup lang="ts">
import { usePlatform } from '@/composables/usePlatform';

const { isNative } = usePlatform();
</script>

<template>
  <button v-if="isNative" @click="openNativeSettings()">Open Device Settings</button>
</template>
使用
Capacitor.isNativePlatform()
Capacitor.getPlatform()
来条件性地运行仅原生环境支持的代码。将其封装到composable中以实现复用:
typescript
// src/composables/usePlatform.ts
import { Capacitor } from '@capacitor/core';

export function usePlatform() {
  const platform = Capacitor.getPlatform() as 'web' | 'ios' | 'android';
  const isNative = Capacitor.isNativePlatform();
  const isIos = platform === 'ios';
  const isAndroid = platform === 'android';
  const isWeb = platform === 'web';

  return {
    platform,
    isNative,
    isIos,
    isAndroid,
    isWeb,
  };
}
在组件中使用它来显示或隐藏仅原生环境支持的功能:
vue
<script setup lang="ts">
import { usePlatform } from '@/composables/usePlatform';

const { isNative } = usePlatform();
</script>

<template>
  <button v-if="isNative" @click="openNativeSettings()">打开设备设置</button>
</template>

Step 7: Deep Link Routing with Vue Router

步骤7:结合Vue Router实现深度链接路由

Handle deep links by mapping Capacitor's
App.addListener('appUrlOpen', ...)
event to Vue Router navigation. Set this up in
App.vue
or a dedicated composable:
typescript
// src/composables/useDeepLinks.ts
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { App } from '@capacitor/app';
import type { PluginListenerHandle } from '@capacitor/core';

export function useDeepLinks() {
  const router = useRouter();
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    listenerHandle = await App.addListener('appUrlOpen', (event) => {
      const url = new URL(event.url);
      const path = url.pathname;

      // Navigate to the route matching the deep link path.
      // Adjust the path parsing logic to match the app's URL scheme.
      if (path) {
        router.push(path);
      }
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });
}
Use the composable in
App.vue
:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { useDeepLinks } from '@/composables/useDeepLinks';

useDeepLinks();
</script>

<template>
  <RouterView />
</template>
通过将Capacitor的
App.addListener('appUrlOpen', ...)
事件映射到Vue Router导航来处理深度链接。可以在
App.vue
或专门的composable中进行配置:
typescript
// src/composables/useDeepLinks.ts
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { App } from '@capacitor/app';
import type { PluginListenerHandle } from '@capacitor/core';

export function useDeepLinks() {
  const router = useRouter();
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    listenerHandle = await App.addListener('appUrlOpen', (event) => {
      const url = new URL(event.url);
      const path = url.pathname;

      // 导航到与深度链接路径匹配的路由
      // 根据应用的URL scheme调整路径解析逻辑
      if (path) {
        router.push(path);
      }
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });
}
App.vue
中使用该composable:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { useDeepLinks } from '@/composables/useDeepLinks';

useDeepLinks();
</script>

<template>
  <RouterView />
</template>

Step 8: Back Button Handling (Android)

步骤8:Android返回按钮处理

Handle the Android hardware back button using
App.addListener('backButton', ...)
combined with Vue Router:
typescript
// src/composables/useBackButton.ts
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { App } from '@capacitor/app';
import { Capacitor } from '@capacitor/core';
import type { PluginListenerHandle } from '@capacitor/core';

export function useBackButton() {
  const router = useRouter();
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    if (Capacitor.getPlatform() !== 'android') {
      return;
    }

    listenerHandle = await App.addListener('backButton', ({ canGoBack }) => {
      if (canGoBack) {
        router.back();
      } else {
        App.exitApp();
      }
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });
}
Use the composable in
App.vue
:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { useBackButton } from '@/composables/useBackButton';

useBackButton();
</script>

<template>
  <RouterView />
</template>
结合
App.addListener('backButton', ...)
和Vue Router来处理Android硬件返回按钮:
typescript
// src/composables/useBackButton.ts
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { App } from '@capacitor/app';
import { Capacitor } from '@capacitor/core';
import type { PluginListenerHandle } from '@capacitor/core';

export function useBackButton() {
  const router = useRouter();
  let listenerHandle: PluginListenerHandle | null = null;

  onMounted(async () => {
    if (Capacitor.getPlatform() !== 'android') {
      return;
    }

    listenerHandle = await App.addListener('backButton', ({ canGoBack }) => {
      if (canGoBack) {
        router.back();
      } else {
        App.exitApp();
      }
    });
  });

  onUnmounted(async () => {
    await listenerHandle?.remove();
  });
}
App.vue
中使用该composable:
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { useBackButton } from '@/composables/useBackButton';

useBackButton();
</script>

<template>
  <RouterView />
</template>

Step 9: PWA Elements Setup

步骤9:PWA Elements配置

Some Capacitor plugins (e.g., Camera, Toast) require
@ionic/pwa-elements
for web fallback UI. If the project uses any of these plugins and targets the web:
  1. Install PWA Elements:
    bash
    npm install @ionic/pwa-elements
  2. Register the custom elements in
    src/main.ts
    before
    createApp()
    :
    typescript
    import { createApp } from 'vue';
    import { defineCustomElements } from '@ionic/pwa-elements/loader';
    import App from './App.vue';
    import router from './router';
    
    defineCustomElements(window);
    
    const app = createApp(App);
    app.use(router);
    app.mount('#app');
部分Capacitor插件(如Camera、Toast)需要
@ionic/pwa-elements
来提供Web端的 fallback UI。如果项目使用了这些插件且目标平台包含Web:
  1. 安装PWA Elements:
    bash
    npm install @ionic/pwa-elements
  2. src/main.ts
    createApp()
    之前
    注册自定义元素:
    typescript
    import { createApp } from 'vue';
    import { defineCustomElements } from '@ionic/pwa-elements/loader';
    import App from './App.vue';
    import router from './router';
    
    defineCustomElements(window);
    
    const app = createApp(App);
    app.use(router);
    app.mount('#app');

Step 10: Build and Sync Workflow

步骤10:构建与同步工作流

After making changes to the Vue app, build and sync to native platforms:
bash
npm run build
npx cap sync
To run on a device or emulator:
bash
npx cap run android
npx cap run ios
To open the native IDE for advanced configuration or debugging:
bash
npx cap open android
npx cap open ios
For live reload during development:
bash
npx cap run android --livereload --external
npx cap run ios --livereload --external
This starts the Vite dev server internally and configures the native app to load from the development server.
在对Vue应用进行修改后,需要构建并同步到原生平台:
bash
npm run build
npx cap sync
要在设备或模拟器上运行应用:
bash
npx cap run android
npx cap run ios
要打开原生IDE进行高级配置或调试:
bash
npx cap open android
npx cap open ios
开发期间启用热重载:
bash
npx cap run android --livereload --external
npx cap run ios --livereload --external
这会在内部启动Vite开发服务器,并配置原生应用从开发服务器加载内容。

Error Handling

错误处理

  • webDir
    mismatch
    : If
    npx cap sync
    copies the wrong files, verify that
    webDir
    in
    capacitor.config.ts
    or
    capacitor.config.json
    matches the Vue build output path. For Vite-based Vue projects, the default output directory is
    dist
    .
  • Plugin not found at runtime: Run
    npx cap sync
    after installing any new plugin. Verify the plugin appears in
    package.json
    dependencies.
  • Memory leaks from listeners: Always remove plugin listeners in
    onUnmounted
    . Store the
    PluginListenerHandle
    returned by
    addListener
    and call
    handle.remove()
    on unmount.
  • Deep links not working: Verify the app URL scheme or universal links are configured in the native projects (
    android/app/src/main/AndroidManifest.xml
    for Android,
    ios/App/App/Info.plist
    and associated domain entitlement for iOS). Verify
    useDeepLinks()
    is called in
    App.vue
    .
  • Back button closes app unexpectedly: Ensure the back button listener checks
    canGoBack
    before calling
    App.exitApp()
    . Only exit when there is no navigation history.
  • PWA Elements not rendering on web: Verify
    defineCustomElements(window)
    is called in
    src/main.ts
    before
    createApp()
    . Verify
    @ionic/pwa-elements
    is installed.
  • Quasar project — Capacitor not detected: Use
    quasar mode add capacitor
    instead of manually installing Capacitor. See
    references/quasar.md
    .
  • Nuxt project — build output not compatible: Nuxt's default SSR output is not compatible with Capacitor. The project must use
    ssr: false
    in
    nuxt.config.ts
    to generate a static SPA. See
    references/nuxt.md
    .
  • webDir
    不匹配
    :如果
    npx cap sync
    复制了错误的文件,请验证
    capacitor.config.ts
    capacitor.config.json
    中的
    webDir
    是否与Vue构建输出路径匹配。对于基于Vite的Vue项目,默认输出目录是
    dist
  • 运行时找不到插件:安装新插件后请运行
    npx cap sync
    。验证插件是否出现在
    package.json
    的依赖中。
  • 监听器导致内存泄漏:务必在
    onUnmounted
    中移除插件监听器。保存
    addListener
    返回的
    PluginListenerHandle
    ,并在卸载时调用
    handle.remove()
  • 深度链接无法工作:验证原生项目中是否配置了应用URL scheme或通用链接(Android为
    android/app/src/main/AndroidManifest.xml
    ,iOS为
    ios/App/App/Info.plist
    及关联域权限)。验证
    App.vue
    中是否调用了
    useDeepLinks()
  • 返回按钮意外关闭应用:确保返回按钮监听器在调用
    App.exitApp()
    之前检查
    canGoBack
    。仅当没有导航历史时才退出应用。
  • Web端PWA Elements无法渲染:验证
    src/main.ts
    中是否在
    createApp()
    之前调用了
    defineCustomElements(window)
    。验证是否已安装
    @ionic/pwa-elements
  • Quasar项目无法检测到Capacitor:使用
    quasar mode add capacitor
    替代手动安装Capacitor,请参考
    references/quasar.md
  • Nuxt项目构建输出不兼容:Nuxt默认的SSR输出与Capacitor不兼容。项目必须在
    nuxt.config.ts
    中设置
    ssr: false
    以生成静态SPA,请参考
    references/nuxt.md

Related Skills

相关技能

  • capacitor-app-creation
    — Create a new Capacitor app from scratch.
  • capacitor-app-development
    — General Capacitor development guidance not specific to Vue.
  • capacitor-plugins
    — Install and configure Capacitor plugins from official and community sources.
  • ionic-vue
    — Ionic Framework with Vue (UI components, navigation, theming on top of Capacitor).
  • capacitor-app-upgrades
    — Upgrade a Capacitor app to a newer major version.
  • capacitor-app-creation
    —— 从头创建新的Capacitor应用。
  • capacitor-app-development
    —— 非Vue专属的通用Capacitor开发指导。
  • capacitor-plugins
    —— 安装和配置官方及社区提供的Capacitor插件。
  • ionic-vue
    —— Ionic Framework与Vue结合(基于Capacitor的UI组件、导航、主题设置)。
  • capacitor-app-upgrades
    —— 将Capacitor应用升级到更高的主版本。",