正在加载,请稍候…

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移

Capacitor 混合应用开发完整指南,涵盖原生插件创建、设备功能访问以及将现有 Web 应用迁移到 iOS 和 Android。

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移

Ionic 的 Capacitor 弥合了 Web 与原生移动开发之间的差距。它将 Web 应用封装在原生壳中,同时通过插件系统提供对原生设备功能的访问。本指南涵盖生产级 Capacitor 开发,从创建原生插件到迁移现有 Web 应用。

什么是 Capacitor?

Capacitor 是一个跨平台原生运行时,可在 iOS、Android 和 Web 上原生运行 Web 应用。与 Cordova 不同,Capacitor:

  • 使用现代 Web 标准(ES 模块、async/await)
  • 与现有原生工具(Xcode、Android Studio)集成
  • 原生支持 Swift 和 Kotlin 插件
  • 运行时占用空间小得多

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移 插图

设置 Capacitor

新项目设置

npm init @capacitor/app my-app
cd my-app
npm install @capacitor/core @capacitor/cli
npx cap init

# 添加平台
npx cap add ios
npx cap add android

现有 Web 应用集成

cd existing-web-app
npm install @capacitor/core @capacitor/cli
npx cap init "My App" com.example.myapp --web-dir dist

# 先构建 Web 应用
npm run build

# 同步到原生项目
npx cap sync

capacitor.config.ts

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'My App',
  webDir: 'dist',
  server: {
    androidScheme: 'https',
    // 开发时使用
    // url: 'http://localhost:5173',
    // cleartext: true,
  },
  plugins: {
    SplashScreen: {
      launchShowDuration: 2000,
      launchAutoHide: true,
      backgroundColor: '#ffffff',
      iosSpinnerStyle: 'small',
      showSpinner: false,
    },
    PushNotifications: {
      presentationOptions: ['badge', 'sound', 'alert'],
    },
    LocalNotifications: {
      smallIcon: 'ic_stat_icon_config_sample',
      iconColor: '#488AFF',
    },
  },
  ios: {
    contentInset: 'automatic',
    allowsLinkPreview: false,
  },
  android: {
    allowMixedContent: false,
    captureInput: true,
  },
};

export default config;

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移 插图

使用官方 Capacitor 插件

import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Geolocation } from '@capacitor/geolocation';
import { PushNotifications } from '@capacitor/push-notifications';
import { Haptics, ImpactStyle } from '@capacitor/haptics';

// 带回退的相机
async function capturePhoto(): Promise<string> {
  const photo = await Camera.getPhoto({
    resultType: CameraResultType.DataUrl,
    source: CameraSource.Camera,
    quality: 90,
    allowEditing: false,
  });

  return photo.dataUrl ?? '';
}

// 文件系统操作
async function saveFile(filename: string, data: string): Promise<void> {
  await Filesystem.writeFile({
    path: filename,
    data,
    directory: Directory.Documents,
    encoding: 'utf8',
  });
}

// 地理定位
async function getCurrentPosition() {
  const position = await Geolocation.getCurrentPosition({
    enableHighAccuracy: true,
    timeout: 10000,
  });

  return {
    lat: position.coords.latitude,
    lng: position.coords.longitude,
    accuracy: position.coords.accuracy,
  };
}

// 触觉反馈
async function triggerHaptic() {
  await Haptics.impact({ style: ImpactStyle.Medium });
}

创建自定义原生插件

定义插件接口

// src/plugins/my-plugin/definitions.ts
export interface MyPluginPlugin {
  echo(options: { value: string }): Promise<{ value: string }>;
  getBatteryInfo(): Promise<BatteryInfo>;
  watchBattery(callback: (info: BatteryInfo) => void): Promise<string>;
  removeListener(listenerId: string): Promise<void>;
}

export interface BatteryInfo {
  level: number;
  isCharging: boolean;
  temperature?: number;
}

iOS 实现(Swift)

// ios/App/Plugins/MyPlugin/MyPlugin.swift
import Foundation
import Capacitor

@objc(MyPlugin)
public class MyPlugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "MyPlugin"
    public let jsName = "MyPlugin"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "getBatteryInfo", returnType: CAPPluginReturnPromise),
    ]

    @objc func echo(_ call: CAPPluginCall) {
        let value = call.getString("value") ?? ""
        call.resolve(["value": value])
    }

    @objc func getBatteryInfo(_ call: CAPPluginCall) {
        UIDevice.current.isBatteryMonitoringEnabled = true
        let level = UIDevice.current.batteryLevel
        let isCharging = UIDevice.current.batteryState == .charging
                      || UIDevice.current.batteryState == .full

        call.resolve([
            "level": Int(level * 100),
            "isCharging": isCharging,
        ])
    }
}

使用 Capacitor 构建混合应用:原生插件与 Web 转 App 迁移 插图

Android 实现(Kotlin)

// android/app/src/main/java/com/example/app/MyPlugin.kt
package com.example.app

import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import com.getcapacitor.JSObject

@CapacitorPlugin(name = "MyPlugin")
class MyPlugin : Plugin() {

    @PluginMethod
    fun echo(call: PluginCall) {
        val value = call.getString("value") ?: ""
        val ret = JSObject()
        ret.put("value", value)
        call.resolve(ret)
    }

    @PluginMethod
    fun getBatteryInfo(call: PluginCall) {
        val batteryIntent = context.registerReceiver(
            null,
            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        )

        val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
        val scale = batteryIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
        val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1

        val batteryPct = if (level != -1 && scale != -1) level * 100 / scale.toFloat() else -1f
        val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
                      || status == BatteryManager.BATTERY_STATUS_FULL

        val ret = JSObject()
        ret.put("level", batteryPct.toInt())
        ret.put("isCharging", isCharging)
        call.resolve(ret)
    }
}

从 Cordova 迁移到 Capacitor

# 安装迁移工具
npm install -g @capacitor/cordova-migration

# 运行迁移
capacitor-cordova-migration migrate

手动步骤:

  1. 将 Cordova 插件替换为 Capacitor 等效插件
  2. config.xml 设置更新到 capacitor.config.ts
  3. 更新 JavaScript API 调用
  4. 测试每个原生功能

开发时热重载

// capacitor.config.ts - 开发配置
const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'My App',
  webDir: 'dist',
  server: {
    url: 'http://192.168.1.100:5173',  // 你的本地 IP
    cleartext: true,
  },
};
# 启动开发服务器
npm run dev

# 在原生 IDE 中打开
npx cap open ios
npx cap open android

使用 Capacitor 的推送通知

import { PushNotifications } from '@capacitor/push-notifications';

async function registerPushNotifications() {
  let permStatus = await PushNotifications.checkPermissions();

  if (permStatus.receive === 'prompt') {
    permStatus = await PushNotifications.requestPermissions();
  }

  if (permStatus.receive !== 'granted') {
    throw new Error('推送通知权限被拒绝');
  }

  await PushNotifications.register();
}

// 监听注册令牌
PushNotifications.addListener('registration', (token) => {
  console.log('推送注册成功:', token.value);
  sendTokenToServer(token.value);
});

// 处理收到的推送通知
PushNotifications.addListener('pushNotificationReceived', (notification) => {
  console.log('收到推送:', notification);
  showInAppNotification(notification.title, notification.body);
});

// 处理点击通知
PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
  const { data } = action.notification;
  navigateToScreen(data.screen);
});

结论

Capacitor 在 Web 开发技能与原生移动交付之间提供了出色的桥梁。其插件架构使得访问原生 API 变得简单,同时保持 Web 应用作为主要代码库。对于拥有现有 Web 应用或强大 Web 专业知识的团队,Capacitor 提供了一条务实的跨平台移动应用路径,而无需牺牲原生能力。