更新记录
1.0.0(2026-10-19)
无
平台兼容性
uni-app(4.87)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ | √ | √ | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | - | - | - | - | - | - | - | - |
fs-scan · uni-app 跨端扫码插件
自定义全屏扫码页 + 多码点选 + 定格,一套 API 覆盖 App / H5 / 各小程序。
- App(Android / iOS) —— 原生实现,能力最全:
- Android:
Camera2取流 +ZXing解码,独立全屏Activity - iOS:
AVFoundation的AVCaptureMetadataOutput硬件级识别,独立全屏ViewController - 相册识别:Android 走
ACTION_PICK+ ZXing,iOS 走Vision的VNDetectBarcodesRequest
- Android:
- H5 ——
js_sdk/fs-scan-h5.js自研:getUserMedia取流 +BarcodeDetector解码, 不支持时用内置的 ZXing 兜底(随包发布、离线可用);同样有自定义扫码页、多码点选、定格 - 小程序 / 鸿蒙 —— 降级为平台原生
uni.scanCode(系统 UI、单码、无法点选)
共同特性:
- 整帧全屏识别:没有「必须把码放进某个框」的限制,画面上任何位置都能识别
- 多码点选(默认开启):≥2 个码时每个码中心叠一个圆形按钮(颜色跟随
scanColor)由用户点选, 稳定 300ms 后画面定格;只有 1 个码时直接交付、不留等待 - 相册多码:图里有多个码时把这张图摊开,在每个码的位置放按钮等点选
- 实时追踪:把每个码的实时位置(0~1 归一化比例)回传给 JS
- UI 全部代码绘制(暗角、圆按钮、箭头、图标),不依赖任何自定义 res / 图片资源
一、平台与能力矩阵
| 平台 | 自定义扫码页 | 多码点选 / 定格 | 相册多码 | 实时追踪 | 实现方式 |
|---|---|---|---|---|---|
| App-Android | ✅ | ✅ | ✅ | ✅ | UTS 原生(minSdk 21 / Android 5.0+,armeabi-v7a + arm64-v8a) |
| App-iOS | ✅ | ✅ | ✅ | ✅ | UTS 原生(deploymentTarget 12.0 / iOS 12+) |
| H5 | ✅ | ✅ | ✅ | ✅ | js_sdk/fs-scan-h5.js(自研) |
| 微信 / 支付宝 / 百度 / QQ / 快手 / 飞书小程序 | ❌ | ❌ | ❌ | ❌ | uni.scanCode(系统 UI) |
| 鸿蒙 | ❌ | ❌ | ❌ | ❌ | uni.scanCode |
Vue2 / Vue3 都支持 —— 插件是纯 JS 调用 UTS,与 Vue 版本无关。
小程序为什么做不到? 不是没实现,是平台边界:小程序里没有可用的高性能解码器,
唯一能自定义的路径是微信的 camera 组件 + 自研解码,帧率和内存都撑不住,还要塞几百 KB 的 wasm。
所有小程序能给的就是 uni.scanCode —— 系统 UI、单码、不可定制。
HBuilderX 版本要求:4.27.0 及以上。 插件用了 @UTSJS.keepAlive 装饰器来让
success(连续扫码)/ onTrace 这类回调可以反复触发,而这个装饰器从 4.27 才开始支持。
4.25 ~ 4.26 上它不会生效,回调触发一次就被回收,第二次调用报「回调函数已释放,不能再次执行」;
4.25 以下则直接编译不过。
要不要用 uni-app x?
结论:uni-app x 项目请直接用官方 uni.scanCode,不要用本插件。 理由:
| 官方能力更强 | uni-app x 内置的扫码 API 本身就是原生实现,官方文档明确说「扫码效果、UI 自定义性都优于 uni-app」,还内置了可自定义的扫码组件 |
| 覆盖端更少 | uni-app x 目前只支持 Android / iOS / 鸿蒙 / Web / 微信小程序 / 支付宝小程序,百度 / QQ / 快手 / 飞书小程序根本没有 |
| 不支持 Vue2 | uni-app x 只有 Vue3(uvue)语法;本插件 Vue2 / Vue3 都能用 |
| 本插件未验证 | 理论上 UTS 插件两边共用,但 interface.uts 需要按 uni-app x 的类型系统重验,目前没有验证过 |
二、API
两种引入方式,按需选一种:
// ① 只在 App 端用 —— 直接用插件原生实现(能力最全)
import { startScan, scanImage, closeScan, isScanning } from '@/uni_modules/fs-scan'
// ② 需要同时跑 App / H5 / 小程序 —— 走跨端门面(推荐)
import { startScan, scanImage, closeScan, isScanning, canMultiPick }
from '@/uni_modules/fs-scan/js_sdk/index.js'
两者的 startScan(options) 参数、回调、错误码完全一致,业务代码换端不用改。
区别只有一个:方式①在 H5 / 小程序上不可用(插件只有 App 端原生实现),
方式②会自动按平台路由。
想知道当前端有没有多码点选,用 canMultiPick():
if (canMultiPick()) {
// App / H5:按「多个码让用户挑」设计
} else {
// 小程序 / 鸿蒙:只有单码,直接处理
}
startScan(options)
打开全屏扫码页。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title | string | 扫描二维码/条形码 |
顶部标题 |
| tips | string | 将二维码/条形码放入画面,即可自动识别 |
底部提示文字 |
| continuous | boolean | false |
连续扫码。开启后 success 可被多次触发,页面不会自动关闭 |
| scanColor | string | #00D2A0 |
点选圆按钮与箭头的颜色(demo 工程把默认色改成了界面主题蓝 #2F7BFF) |
| maskColor | string | #99000000 |
顶部/底部渐变压暗的颜色(#AARRGGBB) |
| showAlbum | boolean | true |
是否显示相册按钮 |
| showTorch | boolean | true |
是否显示手电筒按钮(设备无闪光灯时自动置灰) |
| onlyFromCamera | boolean | false |
只允许相机扫码 |
| scanType | string | all |
all / qrCode / barCode |
| multi | boolean | true |
多码点选,默认开启。详见「四、多码与追踪」 |
| trace | boolean | false |
是否把实时识别位置回传 onTrace。详见「四、多码与追踪」 |
| debug | boolean | false |
调试输出开关,只对这一次调用生效(仅 H5 有实现)。默认关,关着时画面上没有调试行、控制台也没有任何输出。详见「五、H5 差异」第 6 条 |
| success | function | - | (res) => void,res = { content, format, from } |
| fail | function | - | (err) => void,err = { errCode, errMsg } |
| cancel | function | - | 用户点返回 / 关闭时触发 |
| onTrace | function | - | (res) => void,实时位置回调,需要 trace: true。res = { codes: [...] } |
success 回调参数:
| 字段 | 类型 | 说明 |
|---|---|---|
| content | string | 解析出的原始字符串 |
| format | string | 码制名,如 QR_CODE、EAN_13、CODE_128 |
| from | string | camera(相机实时扫描) / album(相册图片识别) |
onTrace 回调参数:
| 字段 | 类型 | 说明 |
|---|---|---|
| codes | array | 当前画面里识别到的全部码;空数组表示此刻没有码 |
| codes[].content | string | 解析出的内容 |
| codes[].format | string | 码制名 |
| codes[].x / y | number | 包围盒左上角,归一化比例 0~1 |
| codes[].width / height | number | 包围盒宽高,归一化比例 0~1 |
坐标刻意用 0~1 的相对比例而不是物理像素:业务侧只要乘以自己的页面宽高即可,不用关心设备 dpr 与原生像素的换算。
scanImage(imagePath, options)
识别本地图片中的条码,常用于「图片长按识别」或自建相册入口。
这是一条没有交互界面的纯 API:结果只能一个一个回调,所以它固定只返回图中第一个码。 想「一张图里多个码让用户挑」,走扫码页内的相册按钮(见 4.1 的相册行)—— 那里有屏幕可以承载点选按钮。
| 参数 | 类型 | 说明 |
|---|---|---|
| imagePath | string | 图片路径,支持绝对路径与 file:// 前缀 |
| options.scanType | string | 同 startScan |
| options.success | function | 同 startScan(from 为 album) |
| options.fail | function | 同 startScan |
closeScan()
主动关闭扫码页。仅在 continuous: true 场景下由业务侧调用。
isScanning()
返回当前是否处于扫码页中,boolean。
三、错误码
fail 回调中的 errCode,App / H5 / 小程序三端保持一致:
| errCode | 含义 | H5 上的具体触发条件 |
|---|---|---|
| 1001 | 拿不到页面上下文,无法打开扫码页 | 平台不支持(如快应用) |
| 1002 | 相机权限被拒绝 | getUserMedia 抛 NotAllowedError / SecurityError |
| 1003 | 相机打开失败(被占用 / 无摄像头 / 会话配置失败) | 无 mediaDevices(http 页面)、NotFoundError、OverconstrainedError |
| 1004 | 图片读取失败 | FileReader / Image 解码失败 |
| 1005 | 未从图片中识别到条码(仅相册路径,属非致命错误,扫码页保持打开) | 图里没有码 |
| 1006 | 图片识别过程异常 | ZXing 加载失败 / 解码器初始化失败 |
| 1007 | 拉起扫码页失败 / 扫码页已打开 | — |
小程序端的 fail 统一归一化成 1004(除用户取消外),因为各平台原始错误没有细分码可用。
四、多码与追踪
多码点选与实时追踪是两项独立能力,可任意组合。
4.1 多码点选(multi,默认开启)
默认行为是:画面里一旦出现 2 个及以上条码,插件就给每一个叠一个圆按钮、画面定格,停下等用户点选, 绝不擅自取第一个。因为「画面里有多个码」时取第一个本质上是在替用户瞎猜。
标记长什么样:每个码的中心一个实心圆(直径 44dp,内含白色右箭头),看到直接点。
颜色由 scanColor 决定(插件默认 #00D2A0,demo 里被改成主题蓝 #2F7BFF)。
不画包围盒、不画数字角标 —— 包围盒会把画面切得七零八落,数字角标还要先「读编号再找位置」,
圆按钮本身就是可点目标,少一步映射。
| 画面里的码 | 行为 |
|---|---|
| ≥ 2 个 | 每个码中心一个圆按钮;稳定约 300ms 后定格,底部提示「已锁定 N 个二维码,点击选择(移开画面可重新扫描)」,等用户点击 |
| 已定格 | 预览画面本身停住(不再是实时取景画面),按钮与提示也不再刷新、码的数量不再跳变;不会自动交付,只有点选才返回结果 |
| 已定格 → 画面里 0 个码(连续 800ms,仅 App 端) | 自动解除:清空按钮、恢复实时预览、提示恢复为 tips,可以重新对准。H5 端没有这个出口,见下方说明 |
| 1 个(近期从未出现过第 2 个不同的码) | 不再等待、不再画标记、也不再显示「识别中…」:约 0.2 秒(SINGLE_AUTO_MS,只够再看一帧)后立刻自动交付 —— 单码就是「对准即完成」 |
| 1 个(刚出现过其他码) | 仍在「点选模式」的保持窗口内,按钮照常显示、仍可点选,不抢跑(见下) |
| 0 个 | 清空所有按钮,提示文案恢复为 tips |
| 相册:图里 ≥ 2 个 | 把这张图本身铺满屏幕,在每个码的位置一个圆按钮,底部提示「相册识别到 N 个码,点击选择(点相册可重选)」,等用户点击;不点就一直停着(相机不会把码扫走) |
| 相册:图里 1 个 | 与相机单码一致:直接交付、不画按钮,而且没有那 0.2 秒的等待(静态图不存在「下一帧才认出来的第二个码」) |
hlScan.startScan({
// multi 默认就是 true,不用显式传
title: '请选择要使用的二维码',
success (res) {
console.log('用户选了:', res.content)
}
})
几个细节:
- 识别到多个码后会「定格」:画面里同时稳定出现 ≥2 个码时立刻停下 ——
预览画面本身冻住(Android 把当时那一帧截图盖在预览上;iOS 直接关掉预览连接、停在最后一帧),
按钮位置与提示也不再随帧刷新、码的数量不再跳变,一直停在那里等用户点选。
这样用户点选时不会再看一个还在晃的取景画面,手指可以从容地移过去点。
相机与解码不停 —— 解除的判据是「画面里一个码都没有」,得靠解码继续跑才知道用户把手机移开了
(iOS 的 metadata 输出是独立连接,关掉预览不影响它)。
定格的唯一自动出口是「画面里一个码都没有持续
UNLOCK_EMPTY_MS(默认 800ms)」: 把手机移开、对着白墙约 0.8 秒后自动解除、恢复实时预览;点选交付同样会解除。 定格判定放在「空帧」处理之前 —— 定格期间某一帧丢码(手抖、反光)不会把标记清掉, 否则用户刚要点的按钮会在点下去的前一刻消失。H5 端是「定格即终态」:定格后不再解码,也不会因为移开、手抖、画面里没码而解冻重扫。 原因很实际 —— 移动手机本身就会让某些帧识别不到码,一解冻就会「恢复实时画面 → 重新攒 300ms → 再次定格」, 用户看到的是画面来回跳。现在 H5 定格后只等点选;要放弃只能退出页面(返回/关闭)。 因此 H5 的提示文案也比 App 少一句,只有「已锁定 N 个二维码,点击选择」。
- 「抢跑」保护:手机在手里一晃、或者斜着拍屏幕时,某一帧可能只解出其中一个码。
如果这时按「画面里只剩 1 个码」立刻自动交付,用户明明看到有两个码、却已经被扫走了其中一个。
所以一旦见过多码,就把点选模式保持
MULTI_HOLD_MS(默认 1500ms); 只有持续只剩 1 个码超过这个窗口,才退回自动选中。 - 单码不留等待:画面里从头到尾只有一个码时直接交付 —— 不画标记、不切「识别中…」提示,
用户看到的就是「对准 → 立刻完成」。只保留
SINGLE_AUTO_MS(默认 200ms,约一个帧间隔) 这一层最小防抖:相机刚开的头一两帧,画面里的第二个码可能还没被解码器认出来, 这一帧就把唯一那个码交出去,用户明明对着两个码却被扫走一个。 防「多码抢跑」的主力是下面那条MULTI_HOLD_MS保持窗口,不是这里。 连这 0.2 秒也不想要,显式传multi: false即可(拿到第一个立刻交付,完全不等)。 - 「见过几个码」按近期累计算,不看当前这一帧:判据是「
SEEN_TTL_MS(1200ms)内出现过的 不同内容数 ≥ 2」,而不是「当前帧解出 ≥ 2 个」。 手机斜着拍显示器、码又小时,一帧往往只解得开一个码;只看当帧的话插件永远意识不到画面里有两个, 会把第一个直接交出去 —— 这正是「明明有 2 个码却直接扫成功了」的成因。 - 稳定判定用「首次出现」时间:每个内容记下它第一次出现在画面里的时刻,而不是最近一次。 否则某个码偶尔解码失败会让计时反复重置,按钮一直闪、永远点不了。
- 只有稳定约 300ms 后按钮才变为实色(未稳定时是半透明,暗示「还在识别,先别点」),避免按钮还在跳动时被误点。
- 位置做了指数滑动平均。同一张码在相邻帧算出的包围盒会有几像素抖动,平滑之后按钮看起来才像「跟住了」。
- 圆心会夹在画面内。码贴到屏幕边缘时不夹的话,圆会被裁掉一半、点不中。
- 点选是明确的用户意图,会跳过去重窗口。否则连续模式下反复点同一张码,2 秒内的第二次会被静默丢弃。
- 要回到「拿到第一个就交付」的快速模式,显式传
multi: false。
4.2 实时追踪 trace: true
开启后,每识别到一帧就通过 onTrace 上报当前画面里所有码的位置。
典型用途是「进区才响应」——例如只有把码放到画面正中区域才触发业务 (这个区域是业务侧自己的规则,跟插件的识别范围无关,插件识别是全屏的):
hlScan.startScan({
trace: true,
onTrace (res) {
if (!res.codes.length) return // 此刻画面里没码
const c = res.codes[0]
const cx = c.x + c.width / 2 // 码中心的归一化横坐标
const cy = c.y + c.height / 2
if (cx > 0.25 && cx < 0.75 && cy > 0.25 && cy < 0.75) {
console.log('码已进入画面中央 50% 区域')
}
}
})
trace与multi完全独立:可以只追踪不选择,也可以只选择不追踪。- 未开启
trace时不会注册追踪回调,onTrace一次都不会被触发。 - 上报频率与解码帧率一致(约 120ms 一帧)。位置是「最近一次解出结果时」的位置, 不是逐像素平滑的实时跟踪,所以别拿它做高帧率动画。
4.3 持续回调必须配 @UTSJS.keepAlive
如果你要拿这份源码去改,这一条千万别删。
HBuilderX 4.25 起,UTS 插件的回调默认在触发一次后就被回收(为了避免内存泄漏)。 于是「需要反复触发的回调」第二次调用就会失败,日志里是这句:
uts插件[FS 扫码插件] UTSSDKModulesHlScanIndexSwift.startScan success回调函数已释放,不能再次执行
本插件有三类回调都需要被反复调用,所以两端 index.uts 里的 startScan 都加了装饰器:
// ⚠️ 不能写成 export const startScan = function () {}
// 装饰器不支持这种写法,必须用 function 声明
@UTSJS.keepAlive
export function startScan(options: HlScanOptions | null): void { /* ... */ }
| 回调 | 为什么会反复触发 |
|---|---|
success |
continuous: true 时每扫一次都会调 |
onTrace |
trace: true 时每一帧都调 |
traceBegin / traceItem / traceEnd |
原生侧逐帧调用,由 UTS 侧组装成对象 |
两条硬性约束:
- app-android 与 app-ios 必须同时加,只加一端就会在那一端失效;
- 装饰器会让该方法的所有回调参数常驻内存,所以别高频反复调用
startScan。
4.4 两端实现差异
| Android | iOS | |
|---|---|---|
| 多码解码 | ZXing。先 QRCodeMultiReader 扫一遍二维码(直接扫描整幅图的定位图形,一次找齐全部二维码),再用 GenericMultipleBarcodeReader 兜底补齐一维码与混排画面;二维码一遍没解出任何码时再放宽一次 TRY_HARDER |
系统 AVCaptureMetadataOutput,一帧返回多个对象 |
| 单帧多码上限 | 8 个,超出丢弃 | 无硬上限 |
| 框坐标来源 | ZXing 的 ResultPoint 换算包围盒 |
transformedMetadataObject(for:) 得到的 bounds |
| 相册路径 | 图中解出全部码(decodeBitmapMulti,走同一套 QRCodeMultiReader → GenericMultipleBarcodeReader 逻辑);≥2 个就在图上点选 |
图中解出全部码(Vision 一次请求本就返回全部 observation);≥2 个就在图上点选 |
| 相册位置来源 | ZXing ResultPoint 换算的包围盒,相对原图像素 |
Vision boundingBox,相对原图归一化(使用时把左下原点翻成左上) |
⚠️ Android 的多码枚举为什么必须两遍:
GenericMultipleBarcodeReader的做法是 「解出一个码 → 按它的定位点裁掉那块 → 在剩下的图上接着解」,裁切范围是按第一个结果算出来的, 画面里的第二个二维码经常正好落在裁切范围外,于是它每帧只返回同一个码。 只靠它的话插件永远看不到「画面里有两个」,就会按单码场景把第一个直接交付 —— 这正是「两个码却直接扫成功了」的成因。iOS 侧的「画面里有几个码」完全取决于系统
AVCaptureMetadataOutput这一帧报了哪些对象。 码偏小、角度偏大时它可能只报得出其中一个,这时插件无从得知还有第二个, 只能靠MULTI_HOLD_MS保持窗口尽量给足机会(单码路径那 0.2 秒帮不上什么忙)。 若后续仍出现 iOS 上「两个码不提示选择」,下一步是给 iOS 加一层 Vision 全量枚举兜底。相册的多码点选:扫码页内的相册按钮选到含 ≥2 个码的图时,会把这张图本身铺在屏幕上, 并在图中每个码的位置画按钮等用户点选 —— 交互与相机那条链路是同一套(同一个浮层)。 不同的只有「画面从哪来」:相机是实时取景,相册是一张静止的图,所以相册这条路不需要任何等待 (没有「下一帧」可以等,图里有什么码解一次就是确定的),也就没有
SINGLE_AUTO_MS那层窗口。两处工程上的细节:
- 相册点选期间停采集会话(用户在看静态图,相机继续跑既费电又亮指示灯), 并且相机结果会被屏蔽(
albumFlowActive)——包括「已拉起相册、还没拿到结果」的那一段空档, 否则选完图回来时相机会先于结果出帧,画面里的码可能先被扫走。- iOS 侧先把相册原图「画正 + 缩到 1600 以内」再识别:EXIF 方向不落到像素上的话, Vision 的坐标与屏幕上显示的图会差一个旋转,按钮全落错地方。
scanImage()这个纯 API 仍然是「只返回第一个码」—— 它没有屏幕可以承载点选按钮。
五、接入步骤
-
把
uni_modules/fs-scan整个目录放进工程根目录的uni_modules下。uni_modules不是包管理器,是「目录约定 + 编译期扫描」—— 没有npm install、没有注册表, 文件放到位就算装好了。目录名必须是fs-scan(import 路径按目录名解析)。接入路径按端选:只在 App 用 →
@/uni_modules/fs-scan; 要同时跑 H5 / 小程序 →@/uni_modules/fs-scan/js_sdk/index.js(见「二、API」)。 -
不需要自己封装补默认值。 所有可选参数不传即按上面「默认值」列生效, 包括
multi/showAlbum/showTorch这三个默认true的开关。 直接startScan({ success (res) {} })就能拿到完整能力。 -
必须是 HBuilderX 工程。 UTS 插件不支持 cli 方式使用。
-
制作自定义调试基座(关键步骤):
- 菜单
运行 → 运行到手机或模拟器 → 制作自定义调试基座 - 原因:
utssdk/app-android/config.json里声明了第三方依赖com.google.zxing:core,这类依赖需要云端打包才能生效,标准基座跑不起来。
- 菜单
-
iOS 权限文案:插件
utssdk/app-ios/config.json的plists已经带了NSCameraUsageDescription/NSPhotoLibraryUsageDescription。 为了让 HBuilderX 的打包链路万无一失,建议同时在工程manifest.json → app-plus → distribute → ios → privacyDescription里再写一份。 -
Android 权限:插件
utssdk/app-android/AndroidManifest.xml已声明CAMERA与VIBRATE,云打包时自动合并。 运行时权限由插件内部自行申请,业务侧不用管。 -
验证顺序建议:先用标准基座跑一次普通页面确认 JS 逻辑没问题 → 再切自定义基座验证原生扫码。
跑 H5 要注意的八件事
跨端门面已经内建了 H5 实现,你不需要自己写回落代码。但有八个坑得知道:
-
必须是 HTTPS 或 localhost。
getUserMedia在 http 页面上会被浏览器直接拒绝, 表现为fail({ errCode: 1002 })。本地调试用localhost或127.0.0.1即可。 -
解码能力分两级:
- Chrome 83+ / Edge / 安卓 WebView / 安卓浏览器 → 有
window.BarcodeDetector,解码走它、速度最快 - iOS Safari(全部版本)、部分国产内核 → 没有 → 自动加载插件内置的 ZXing
(
js_sdk/vendor/zxing.min.js,随插件一起发布、同源请求,不依赖外网) - ZXing 的
decode()一次只返回一个码,插件用「把已找到的码涂白、再解一次」的办法把画面里其余的码 逐个抠出来(decodeZXingMulti,单帧上限 3 个)。所以 iOS Safari 上多码点选同样可用, 代价是每帧要多解 1~2 次、帧率略低于BarcodeDetector路径。
⚠️ ZXing 是静态引入的(不用动态
import():uni-app 各版本打包器对它的处理不一致, 而 iOS Safari 拿不到它就直接不可用)。因此 H5 产物固定多出 332 KB(gzip 后约 100 KB), 即使浏览器有BarcodeDetector也会下载这份文件(只是不用它解码)。换来的是「iOS 与离线环境一定能用」。 如果你的项目对 H5 首屏体积敏感,可以把js_sdk/vendor/排除、改为按需从 CDN 拉。zxingUrl只在内置 chunk 加载失败时兜底(默认 unpkg)。完全离线 / 内网部署可以指向自己的地址:startScan({ zxingUrl: '/static/zxing.min.js', success (res) {} }) - Chrome 83+ / Edge / 安卓 WebView / 安卓浏览器 → 有
-
手电筒不是处处都有。由
track.getCapabilities().torch决定,不支持的设备按钮会置灰, 点了提示「当前设备不支持手电筒」。 -
相册多码在 H5 也可用:选图后把图铺满屏幕、在每个码位置画按钮,和 App 端一致。
-
H5 的
scanImage也实现了,同样只返回第一个码(与 App 端语义一致)。 -
调试输出默认关闭,排查时用
setDebug(true)打开(H5 专用):import { setDebug, isDebug } from '@/uni_modules/fs-scan/js_sdk/index.js' setDebug(true) // 之后打开的扫码页显示调试行 + 打 console setDebug(false) // 关回去(默认就是关) isDebug() // 查询当前状态也可以只对一次调用生效:
startScan({ debug: true, success (res) {} })。 手机上没有控制台的话,URL 上加?fsscanDebug=1也能开(只给开发用)。打开后画面下方会多出一行小字:
v1.0.0 · ZXing · 本帧 2 码 · 累计 2 码 · 已定格 · 预览 canvas 390×844- 版本号:手机浏览器对 JS 的缓存很顽固,先确认跑的不是旧文件
- 引擎:
BarcodeDetector还是ZXing - 本帧:这一帧解出几个码。长期是 1 而画面上确实有两个码 → 解码器没抠出第二个 (把两个码分开些、避开反光、对着光更亮的一面)
- 累计:
SEEN_TTL_MS内见过几个不同内容。累计到 2 却不出按钮 → 才是状态机的问题 - 预览:
canvas(正常)/video(兜底)(这个内核画不出 canvas,退回浏览器渲染) - 顺带看一眼
version常量(js_sdk/fs-scan-h5.js顶部的H5_VERSION)是否与插件版本一致
控制台同步输出的是关键节点(不是每帧):
打开扫码页/相机已就绪/解码引擎/定格 N 个码/交付/canvas 预览退回 video,统一前缀[fs-scan]。 -
H5 的定格是「一次性」的,不会自己解开。定格后解码完全停止:画面、按钮、提示都静止, 移开手机、手抖、画面里没有码都不会解冻,也不会重新识别。只有点选才交付结果; 想放弃就退出页面。这与 App 端不同(App 有
UNLOCK_EMPTY_MS自动解锁), 原因见上文「识别到多个码后会定格」那条的说明。 -
H5 的预览恒定是插件自己用 canvas 画的。
<video>的呈现尺寸由浏览器排版 + 原生视频层决定,width:100%;height:100%与object-fit:cover在部分内核里不生效 —— 表现是「摄像头预览只有屏幕中间一小块」。 更麻烦的是getBoundingClientRect()会报满屏(它量的是 DOM 盒子,不是实际画面), 所以插件不等检测、进入页面立刻就用全屏 canvas 接管预览,并把<video>置opacity:0。 这样不会出现「先小窗、后跳全屏」的闪动。 万一某个内核画不出来(帧已就绪却连续 1.5 秒drawImage抛错),会退回原生渲染保底, 调试面板此时显示· 预览 video(兜底);正常情况一律是· 预览 canvas。
小程序 / 鸿蒙上的行为
门面会自动调 uni.scanCode,返回字段已经归一化成 { content, format, from }:
| 平台 | 原始内容字段 | 处理方式 |
|---|---|---|
| 微信 / QQ / 百度 / 快手 / 飞书 / 抖音 | res.result |
直接取 |
| 支付宝 | res.code(另有 qrCode / barCode) |
按顺序兜底取 |
用户取消统一走 cancel() 回调(各端 fail 里的 cancel 文案不一致,门面用正则兜住)。
onlyFromCamera 在支付宝上不支持,门面自动换成 hideAlbum。
multi / trace / onTrace 在小程序上不生效 —— 传了不报错,但不会有任何效果。
六、自定义扫码页说明
页面上没有居中的固定扫码框,识别范围就是整个预览画面。
| 元素 | 位置 | 说明 |
|---|---|---|
| 返回 | 左上 | 触发 cancel |
| 标题 | 顶部居中 | 来自 title |
| 手电筒 | 右上 | 设备无闪光灯自动置灰 |
| 渐变暗角 | 顶部 / 底部 | 由深到透的渐变,只为托住白字,不代表任何识别区域 |
| 提示文字 | 底部 | 来自 tips;只有多码场景才会被临时替换为「识别中… / 点击二维码即可选择 / 已锁定 N 个二维码,点击选择(移开画面可重新扫描)」。单码场景始终显示 tips 本身,然后直接交付 |
| 相册 | 底部 | 隐藏条件:showAlbum: false 或 onlyFromCamera: true |
| 点选按钮 | 定格后固定不动 | 每个码中心的圆按钮(内含白色右箭头,颜色由 scanColor 决定);未稳定时半透明,稳定后可点选 |
| 成功反馈 | - | 震动 + 系统提示音 |
细节行为:
- 整帧识别:不做任何区域裁剪,画面里任何位置的码都会被纳入解码范围。 (早期版本只解码中心 70%,会出现「画面里看得见的码认不出、框外的码反而被认出来」,与画面对不上。)
- 连续扫码去重:同一条码内容在 2 秒内不会重复自动回调,避免相机连续读同一张码刷屏。 (用户点选 / 相册识别不受此限制。)
- 帧节流:约 120ms 解码一帧,解码期间直接丢帧,避免 CPU 打满、预览卡顿。
- 解码兜底:标准解码失败后开
TRY_HARDER再试,最后再试一次反色(黑底白码)。
七、扩展指引
- 调整多码「稳定」判定时间:
HlScanActivity.STABLE_MS/HlScanTiming.stableMs(默认 300ms)。 - 调整「抢跑」保护的保持窗口:
HlScanActivity.MULTI_HOLD_MS/HlScanTiming.multiHoldMs(默认 1500ms)。 - 调整「见过的码」过期时间:
HlScanActivity.SEEN_TTL_MS/HlScanTiming.seenTtlMs(默认 1200ms)。 - 调整单码自动交付的最小防抖窗口:
HlScanActivity.SINGLE_AUTO_MS/HlScanTiming.singleAutoMs(默认 200ms)。 这是单码路径唯一的等待(防「相机刚开那一两帧第二个码还没被报出来」)。 调小 → 单码更快,但第一帧的假单码更容易抢跑;调大 → 更稳,但单码场景又会开始顿。 不建议超过STABLE_MS(300ms) —— 单码用户的等待感知就是从那个量级开始的。 - 调整定格后自动解锁的等待时间:
HlScanActivity.UNLOCK_EMPTY_MS/HlScanTiming.unlockEmptyMs(默认 800ms)。 它是「画面里一个码都没有」持续多久才解除定格;调小 → 手一抖就可能解锁、定格来回抖;调大 → 移开手机后要等更久才恢复。 - 调整点选按钮平滑强度:
HlScanOverlayView的SMOOTH/smoothFactor。 - 调整点选按钮大小与箭头粗细:
HlScanOverlayView.markerRadius(默认 22dp/pt → 直径 44)与ARROW_STROKE_RATIO/arrowStrokeRatio(默认 0.16,相对半径)。箭头形状在buildArrow/arrowPath里。 - 调整点选命中容差:
HlScanOverlayView.hitIndex(Androiddensity * 10f圆面容差、density * 56f就近; iOS 对应slop/near)。 - 调整单帧多码上限:
HlScanDecoder.MAX_MULTI_COUNT。 - 调整包围盒外扩比例:
HlScanDecoder.normalizeBounds里的0.16f。ZXing 给的是三个定位图形中心, 算出来的包围盒会比整个码面小一圈,所以按比例外扩。它决定的是圆按钮落在码的哪里。 - 调整预览旋转 / 缩放:
HlCamera2Controller.buildContentToViewMatrix。 ⚠️ 这个矩阵同时决定「预览怎么显示」和「点选按钮画在哪」,两边共用同一个矩阵。 改这里务必保证两者仍然一致,否则按钮会和码错位。 - 回调旧观感(居中扫码框 + 扫描线):把
HlScanMaskView的showsFrame设为true, 再用frameRatio调框的大小(默认 0.68)。插件本身不会打开它。 - 调整帧节流间隔:
HlCamera2Controller.frameIntervalMs。 - 去掉震动反馈:删除
HlScanActivity.playBeepAndVibrate的调用,并从AndroidManifest.xml移除VIBRATE权限。 - 强制竖屏扫码:在插件
AndroidManifest.xml的HlScanActivity上加android:screenOrientation="portrait"(默认不限制方向,横竖屏都可用)。

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 5
赞赏 0
下载 12694417
赞赏 1956
赞赏
京公网安备:11010802035340号