Обложка: «кто-то сканирует штрих-код упаковки своим смартфоном, карикатура» — DALL-E mini
EDIT: Теперь я опубликовал это в новом пакете NPM, как react-zxing.
Справочная информация
Это моя первая статья. Меня побудило написать ее то, что я обнаружил, что это особенно сложно реализовать, я не смог найти никакого хорошего учебника, а мой пост на 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} />;
};
По соображениям производительности важно инстанцировать BrowserMultiFormatReader
только один раз с помощью хука useRef
и очищать useEffect
вызовом метода reset()
этого экземпляра.
Использование пользовательского хука
Глядя на базовую реализацию, можно заметить несколько областей, требующих улучшения:
- Логика связана с рендерингом нашего видеоэлемента.
- Мы не обрабатываем результат или ошибки
- Мы не допускаем никакой конфигурации потребителем
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} />;
};
Что вы думаете?
Пожалуйста, дайте мне знать, оставив комментарий!