更新记录

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
  • 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 仍然是「只返回第一个码」—— 它没有屏幕可以承载点选按钮。


五、接入步骤

  1. 把 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」)。

  2. 不需要自己封装补默认值。 所有可选参数不传即按上面「默认值」列生效, 包括 multi / showAlbum / showTorch 这三个默认 true 的开关。 直接 startScan({ success (res) {} }) 就能拿到完整能力。

  3. 必须是 HBuilderX 工程。 UTS 插件不支持 cli 方式使用。

  4. 制作自定义调试基座(关键步骤):

    • 菜单 运行 → 运行到手机或模拟器 → 制作自定义调试基座
    • 原因:utssdk/app-android/config.json 里声明了第三方依赖 com.google.zxing:core,这类依赖需要云端打包才能生效,标准基座跑不起来。
  5. iOS 权限文案:插件 utssdk/app-ios/config.json 的 plists 已经带了 NSCameraUsageDescription / NSPhotoLibraryUsageDescription。 为了让 HBuilderX 的打包链路万无一失,建议同时在工程 manifest.json → app-plus → distribute → ios → privacyDescription 里再写一份。

  6. Android 权限:插件 utssdk/app-android/AndroidManifest.xml 已声明 CAMERA 与 VIBRATE,云打包时自动合并。 运行时权限由插件内部自行申请,业务侧不用管。

  7. 验证顺序建议:先用标准基座跑一次普通页面确认 JS 逻辑没问题 → 再切自定义基座验证原生扫码。

跑 H5 要注意的八件事

跨端门面已经内建了 H5 实现,你不需要自己写回落代码。但有八个坑得知道:

  1. 必须是 HTTPS 或 localhost。getUserMedia 在 http 页面上会被浏览器直接拒绝, 表现为 fail({ errCode: 1002 })。本地调试用 localhost 或 127.0.0.1 即可。

  2. 解码能力分两级:

    • 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) {} })
  3. 手电筒不是处处都有。由 track.getCapabilities().torch 决定,不支持的设备按钮会置灰, 点了提示「当前设备不支持手电筒」。

  4. 相册多码在 H5 也可用:选图后把图铺满屏幕、在每个码位置画按钮,和 App 端一致。

  5. H5 的 scanImage 也实现了,同样只返回第一个码(与 App 端语义一致)。

  6. 调试输出默认关闭,排查时用 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]。

  7. H5 的定格是「一次性」的,不会自己解开。定格后解码完全停止:画面、按钮、提示都静止, 移开手机、手抖、画面里没有码都不会解冻,也不会重新识别。只有点选才交付结果; 想放弃就退出页面。这与 App 端不同(App 有 UNLOCK_EMPTY_MS 自动解锁), 原因见上文「识别到多个码后会定格」那条的说明。

  8. 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(Android density * 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"(默认不限制方向,横竖屏都可用)。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

权限说明 Android: 1. android.permission.CAMERA —— 相机取流,用于实时扫描二维码/条形码(必需) 2. android.permission.VIBRATE —— 扫码成功时的震动反馈(可选) iOS: 1. NSCameraUsageDescription —— 需要使用相机扫描二维码/条形码 2. NSPhotoLibraryUsageDescription —— 需要访问相册以识别其中的二维码/条形码 H5: 浏览器摄像头权限(getUserMedia),仅获取视频流,不请求麦克风。 要求页面为 HTTPS 或 localhost。 小程序 / 鸿蒙: 无额外权限,使用平台原生 uni.scanCode。 数据说明: 插件仅在设备本地完成图像解码,不上传、不收集、不存储任何用户数据; 不集成任何统计、广告或第三方数据 SDK;不写入相册、不读取通讯录与位置信息。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。