背景
这将是我的第一篇文章。我之所以有动力写这个,是因为我发现它特别难实现,我找不到任何很好的教程,而且我的reddit帖子似乎引起了一些兴趣。
我们在建造什么?
我想为我的副业Snxbox使用一个条形码扫描仪。我的标准是:
- 将用户的设备摄像头输出流到一个视频元素,这样用户就可以看到他们的摄像头所瞄准的东西。
- 从流中准确地检测QR和EAN码并发出结果。
替代方案
我开始寻找可以使用的与React兼容的包。我立即找到的包是react-qr-barcode-scanner ,它提供了一个简单的滴入式反应组件。
react-qr-barcode-scanner
react-qr-barcode-scanner 依赖于zxing 来解码条形码。我使用了一段时间,直到我发现了一个由读取EAN码的结果不一致引起的错误。我在zxing上发现了一个问题,它似乎已经被修复。然而,react-qr-barcode-scanner 使用的是旧版本的zxing ,在那里这仍然是一个问题。
quokka2
这是另一个扩展了zxing 的软件包。我找到了一个关于如何用React使用它的例子,但说实话,这似乎很艰巨。
html5-qrcode
这是另一个扩展zxing 的包。虽然这个包似乎也使用了一个旧版本的zxing ,但实现起来还是比较容易的,所以我对使用它有点谨慎。
使用条形码检测API
有一个实验性的API用于扫描条形码,但不幸的是,它的支持似乎还很有限。
重构的尝试
我最终分叉了react-qr-barcode-scanner ,试图更新它的依赖性,但发现这个实现一开始就很直接。
另外,react-qr-barcode-scanner 使用 react-webcam将摄像机的数据流传输到一个视频元素上,它以一定的间隔拍摄快照,然后由zxing ,实际上它并没有对视频流本身进行解码。
我们实际上可以用zxing 直接从视频流中读取*,并*在视频元素中预览视频流,这使得react-webcam 的依赖性变得多余了。
方法
观察发现,大多数替代品都使用zxing 进行解码,所以它可能是一个安全的选择。
因此,我们安装@zxing/library 包。然后,创建一个阅读器实例:
import { BrowserMultiFormatReader } from '@zxing/library';
const reader = new BrowserMultiFormatReader();
然后我们可以使用它的方法 decodeFromConstraints来连续检测视频流中的代码,并将其显示在视频元素中。第一个参数是一个配置对象,第二个参数是我们要传输的视频元素,第三个参数是处理解码结果的回调函数:
import { BrowserMultiFormatReader } from '@zxing/library';
let videoElement: HTMLVideoElement;
reader.decodeFromConstraints(
{
audio: false,
video: {
facingMode: 'environment',
},
},
videoElement,
(result, error) => {
if (result) console.log(result);
if (error) console.log(error);
}
);
React的实现
我们可以在引用中持有视频元素,使用useRef 钩子,用useEffect 开始解码。最基本的实现会是这样的:
const BarcodeScanner = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const reader = useRef(new BrowserMultiFormatReader());
useEffect(() => {
if (!videoRef.current) return;
reader.current.decodeFromConstraints(
{
audio: false,
video: {
facingMode: 'environment',
},
},
videoRef.current,
(result, error) => {
if (result) console.log(result);
if (error) console.log(error);
}
);
return () => {
reader.current.reset();
}
}, [videoRef]);
return <video ref={videoRef} />;
};
由于性能原因,重要的是只使用useRef 钩子实例化一次BrowserMultiFormatReader ,并通过调用该实例的reset() 方法来清理useEffect 。
使用一个自定义的钩子
看一下基本的实现,我们注意到有几个地方需要改进:
- 该逻辑与我们的视频元素的渲染相耦合
- 我们没有处理结果或错误
- 我们不允许由
BarcodeScanner消费者进行任何配置
我们可以通过把它提取到一个自定义的钩子来改进它,这样我们就可以把逻辑与我们想在我们的应用程序中渲染视频元素的方式解耦。
这将是最终的实现:
import { BrowserMultiFormatReader, DecodeHintType, Result } from '@zxing/library';
import { useEffect, useMemo, useRef } from 'react';
interface ZxingOptions {
hints?: Map<DecodeHintType, any>;
constraints?: MediaStreamConstraints;
timeBetweenDecodingAttempts?: number;
onResult?: (result: Result) => void;
onError?: (error: Error) => void;
}
const useZxing = ({
constraints = {
audio: false,
video: {
facingMode: 'environment',
},
},
hints,
timeBetweenDecodingAttempts = 300,
onResult = () => {},
onError = () => {},
}: ZxingOptions = {}) => {
const ref = useRef<HTMLVideoElement>(null);
const reader = useMemo<BrowserMultiFormatReader>(() => {
const instance = new BrowserMultiFormatReader(hints);
instance.timeBetweenDecodingAttempts = timeBetweenDecodingAttempts;
return instance;
}, [hints, timeBetweenDecodingAttempts]);
useEffect(() => {
if (!ref.current) return;
reader.decodeFromConstraints(constraints, ref.current, (result, error) => {
if (result) onResult(result);
if (error) onError(error);
});
return () => {
reader.reset();
};
}, [ref, reader]);
return { ref };
};
然后我们可以像这样在一个组件中消费它:
export const BarcodeScanner: React.FC<BarcodeScannerProps> = ({
onResult = () => {},
onError = () => {},
}) => {
const { ref } = useZxing({ onResult, onError });
return <video ref={ref} />;
};