跳转至正文

如何使用 Capacitor 构建 Ionic 条码扫描器

使用 Capacitor 8 构建 Ionic 条码扫描器:配置应用,添加 @capacitor/barcode-scanner,在 iOS、Android 和 Web 上扫描 QR 码和条码。

混合移动应用内手机扫描条形码的插图

要使用 Capacitor 构建 Ionic 条形码扫描器,请创建一个带有 Capacitor 集成的 Ionic 应用,并安装官方 @capacitor/barcode-scanner 插件,将 Android 的 minSdkVersion 提升至 26,添加 iOS 相机使用说明,并从按钮调用 CapacitorBarcodeScanner.scanBarcode() 。该插件在 iOS 和 Android 上打开原生全屏扫描器,在 Web 上打开浏览器扫描器,并返回解码后的文本和格式。

本教程将在 Capacitor 8 上使用 Ionic Angular 构建一个可用的扫描器应用,随后涵盖格式过滤、错误处理、Android 扫描引擎、连续扫描、企业级硬件扫描器以及常见故障。

你将构建什么

一个单页 Ionic 应用,包含一个扫描按钮,该按钮可以:

  • 打开相机扫描器,
  • 显示每个结果及其格式,
  • 允许你复制或打开扫描到的值,
  • 干净地处理取消和权限错误。

code 使用 Angular 独立组件。在 React 或 Vue 中,插件调用完全相同,因此只有模板语法会发生变化。

前提条件

  • Node.js LTS 和 Bun。
  • Android 端:带 SDK 的 Android Studio,以及带摄像头的真机或模拟器。
  • iOS 端:一台装有 Xcode 26 的 Mac;自 2026 年 4 月起,App Store Connect 要求上传必须使用该版本。
  • 最好用真机。模拟器可以用虚拟摄像头扫描,但条形码识别很依赖真实光照和对焦。

第 1 步:创建 Ionic 应用

bunx @ionic/cli start barcode-scanner blank --type=angular-standalone --capacitor
cd barcode-scanner

Ionic CLI 会搭建 Angular 应用,并带上 @capacitor/core 以及 @capacitor/cli 已安装的依赖。请检查 capacitor.config.ts: appId 应为你的反向域名 ID,例如 com.example.scanner,以及 webDir 应与构建输出一致(www 适用于 Ionic Angular)。

先构建一次,让原生平台有内容可复制:

bun run build

步骤 2:添加 Android 和 iOS

bun add @capacitor/android @capacitor/ios
bunx cap add android
bunx cap add ios

Capacitor 8 使用 Swift Package Manager 创建新的 iOS 项目。如果你要升级的是旧应用,请遵循 Capacitor 8 升级指南 首先。

步骤 3:安装条码扫描插件

bun add @capacitor/barcode-scanner
bunx cap sync

@capacitor/barcode-scanner 是 Capacitor 团队官方的扫描器。3.x 版本支持 Capacitor 8。

Android 配置

该插件需要 Android API 26 或更高版本。Capacitor 8 默认是 24,因此请修改 android/variables.gradle:

ext {
    minSdkVersion = 26
    // keep the other values Capacitor generated
}

在 Android 上,你可以为每次扫描选择解码引擎:ZXing(默认,支持插件列出的所有格式)或 Google ML Kit(处理难识别的条码时速度更快,但不支持 MaxiCode、RSS-14、RSS Expanded 或 UPC/EAN 扩展)。

iOS 配置

将相机使用说明添加到 ios/App/App/Info.plist:

<key>NSCameraUsageDescription</key>
<string>The camera is used to scan barcodes and QR codes.</string>

如果没有它,iOS 会在首次访问相机时终止应用。描述要写给用户看,因为 App Review 也会读。iOS 使用 Apple 的 Vision framework,支持列出的所有格式,但 MaxiCode 和 UPC/EAN 扩展除外,并且无法区分 UPC-A 和 EAN-13。

步骤 4:编写扫描服务

将插件调用封装在服务中,便于复用和测试。

// src/app/services/scanner.service.ts
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import {
  CapacitorBarcodeScanner,
  CapacitorBarcodeScannerAndroidScanningLibrary,
  CapacitorBarcodeScannerCameraDirection,
  CapacitorBarcodeScannerScanOrientation,
  CapacitorBarcodeScannerTypeHint,
  CapacitorBarcodeScannerTypeHintALLOption,
} from '@capacitor/barcode-scanner';

export interface ScanResult {
  value: string;
  format: string;
  scannedAt: Date;
}

export type ScanOutcome =
  | { status: 'ok'; result: ScanResult }
  | { status: 'cancelled' }
  | { status: 'denied' }
  | { status: 'error'; message: string };

@Injectable({ providedIn: 'root' })
export class ScannerService {
  async scan(onlyQr = false): Promise<ScanOutcome> {
    try {
      const { ScanResult, format } = await CapacitorBarcodeScanner.scanBarcode({
        hint: onlyQr
          ? CapacitorBarcodeScannerTypeHint.QR_CODE
          : CapacitorBarcodeScannerTypeHintALLOption.ALL,
        scanInstructions: 'Point the camera at a barcode',
        cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
        scanOrientation: CapacitorBarcodeScannerScanOrientation.ADAPTIVE,
        cancelButtonAccessibilityLabel: 'Cancel scanning',
        torchButtonOnAccessibilityLabel: 'Turn flashlight off',
        torchButtonOffAccessibilityLabel: 'Turn flashlight on',
        android: {
          scanningLibrary: CapacitorBarcodeScannerAndroidScanningLibrary.MLKIT,
        },
        web: {
          showCameraSelection: true,
        },
      });

      return {
        status: 'ok',
        result: {
          value: ScanResult,
          format: CapacitorBarcodeScannerTypeHint[format] ?? String(format),
          scannedAt: new Date(),
        },
      };
    } catch (err: any) {
      const code: string = err?.code ?? '';
      if (code.endsWith('0006')) return { status: 'cancelled' };
      if (code.endsWith('0007')) return { status: 'denied' };
      return { status: 'error', message: err?.message ?? 'Scan failed' };
    }
  }

  get platform() {
    return Capacitor.getPlatform();
  }
}

插件返回的错误带有以下形式的代码 OS-PLUG-BARC-XXXX。在 iOS 上, 0006 表示用户已取消,而 0007 表示相机访问权限被拒绝。请将取消视为正常结果,而非错误。在 Android 上测试时,记录原始 code 以确认目标设备上的映射一致。

本指南中介绍的 format 结果中的值是一个数字枚举。 CapacitorBarcodeScannerTypeHint[format] 将其转换回名称,例如 QR_CODE 或 EAN_13.

第 5 步:构建页面

// src/app/home/home.page.ts
import { Component, inject, signal } from '@angular/core';
import { DatePipe } from '@angular/common';
import {
  IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel,
  IonFab, IonFabButton, IonIcon, IonToggle, IonNote, ToastController,
} from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { scanOutline } from 'ionicons/icons';
import { ScannerService, ScanResult } from '../services/scanner.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  imports: [
    DatePipe, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem,
    IonLabel, IonFab, IonFabButton, IonIcon, IonToggle, IonNote,
  ],
})
export class HomePage {
  private scanner = inject(ScannerService);
  private toast = inject(ToastController);

  results = signal<ScanResult[]>([]);
  onlyQr = signal(false);
  scanning = signal(false);

  constructor() {
    addIcons({ scanOutline });
  }

  async scan() {
    if (this.scanning()) return;
    this.scanning.set(true);
    const outcome = await this.scanner.scan(this.onlyQr());
    this.scanning.set(false);

    if (outcome.status === 'ok') {
      this.results.update((list) => [outcome.result, ...list]);
    } else if (outcome.status === 'denied') {
      await this.show('Camera access is off. Enable it in Settings to scan.');
    } else if (outcome.status === 'error') {
      await this.show(outcome.message);
    }
  }

  private async show(message: string) {
    const t = await this.toast.create({ message, duration: 3000 });
    await t.present();
  }
}
<!-- src/app/home/home.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>Barcode Scanner</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item>
      <ion-toggle [checked]="onlyQr()" (ionChange)="onlyQr.set($event.detail.checked)">
        QR codes only
      </ion-toggle>
    </ion-item>

    @for (r of results(); track r.scannedAt) {
      <ion-item>
        <ion-label>
          <h2>{{ r.value }}</h2>
          <p>{{ r.format }} · {{ r.scannedAt | date: 'shortTime' }}</p>
        </ion-label>
      </ion-item>
    } @empty {
      <ion-item lines="none">
        <ion-note>No scans yet. Tap the button to scan.</ion-note>
      </ion-item>
    }
  </ion-list>

  <ion-fab slot="fixed" vertical="bottom" horizontal="end">
    <ion-fab-button (click)="scan()" [disabled]="scanning()" aria-label="Scan a barcode">
      <ion-icon name="scan-outline" aria-hidden="true"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

第 6 步:在设备上运行

bun run build
bunx cap sync
bunx cap run android
bunx cap run ios

对于 Web 版本, bun run start 在 localhost 上运行应用,浏览器允许访问摄像头。通过局域网 IP 访问时需要 HTTPS。

为了加快迭代速度,请在设备上使用实时重载: bunx cap run android --livereload --external 每次保存时自动重载应用。

支持的格式

格式 Android ZXing Android ML Kit iOS
QR Code、Aztec、Data Matrix、PDF417 是 是 是
EAN-13、EAN-8、UPC-A、UPC-E 是 是 是(UPC-A 和 EAN-13 处理方式相同)
Code 128、Code 39、Code 93、Codabar、ITF 是 是 是
RSS-14、RSS Expanded 是 否 是
MaxiCode 是 否 否
UPC/EAN 扩展 是 否 否

如果传入不支持的格式作为 hint,插件将回退为扫描任意格式。

安全处理扫描值

条形码属于不可信输入。在处理之前:

  • 打开 URL 前先进行验证,并先向用户显示域名。
  • 切勿将扫描到的字符串作为 code 执行,或将其注入 HTML。
  • 对于产品代码,请在调用后端之前验证校验位。
  • 如果用户可以快速扫描,请对后端查询实施速率限制。

不止于基础扫描器

scanBarcode 是一次性的全屏流程。这适用于大多数应用。其他需求则需要其他工具:

  • 在你自己的 UI 中连续扫描。 在你的 Web UI 后面运行摄像头画面,使用 @capgo/camera-preview,抓取帧用 captureSample,并在 JavaScript 中用库解码,例如 zxing-wasm.
  • 坚固的企业级设备。 Zebra 手持设备配有硬件扫描器。 @capgo/capacitor-zebra-datawedge 管理 DataWedge 配置文件和扫描触发器,比这些设备上的摄像头更快、更可靠。
  • 文档拍摄。 对于收据和纸质表单,文档扫描插件比条码阅读器更合适。参见 插件目录中浏览.

从 Cordova 迁移过来?我们的 Cordova 条码扫描器指南 介绍了旧插件和迁移路径。

故障排除

iOS 应用在开始扫描时关闭。 NSCameraUsageDescription 未包含在 Info.plist.

Android build 因 manifest merger 或 minSdk 错误而失败。 minSdkVersion 仍为 24。请在中将其设置为 26 android/variables.gradle 并同步。

扫描器已打开,但无法读取 code。 尝试使用另一个 Android 引擎、改善光照、开启手电筒,并将范围限制 hint 为你期望的格式。极小或损坏的条码需要把摄像头靠近一些。

每次都提示权限被拒绝。 用户之前拒绝了相机权限。请显示一条消息,说明如何在系统设置中开启权限。系统不会再次弹出 prompt。

Web 扫描器无法启动。 页面未通过 HTTPS 或 localhost 提供服务,或者另一个标签页正在占用相机。

平板上方向设置不生效。 从目标 Android SDK 36 的应用开始,Android 16 在大屏设备上会忽略固定方向设置。请使用 ADAPTIVE.

无需等待审核即可发布修复

扫描器上线后,大多数改动(比如新的校验规则、UI 调整或换一个后端查询)都是 Web code。 Capgo 实时更新 几分钟内推送到已安装的应用,而插件升级和 Info.plist 改动仍需走应用商店。当你需要新的原生构建、又没有本地 Mac 环境时, Capgo 构建 可在云端生成已签名的 iOS 和 Android 二进制文件。

总结

在 Capacitor 8 上做 Ionic 条码扫描器,只需一个官方插件、两处原生配置改动(Android minSdkVersion 26 和 iOS 相机用途字符串),以及一次 scanBarcode 调用。能限制格式就限制,把取消和权限错误当作正常结果,校验扫描到的所有内容,一次性流程不够用时再上相机预览或 DataWedge。

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。