要使用 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。