Expo构建Web移动端时BarcodeScanner扫描回调无法触发求助
Expo Web端条码扫描回调不触发的解决方案
问题原因
你当前使用的expo-camera的onBarCodeScanned回调仅在Android/iOS原生平台生效,Web环境下该功能未实现,导致部署后无法触发扫描逻辑。同时单独的BarCodeScanner组件也不支持Web,这是Expo官方依赖的平台限制。
解决步骤
1. 安装Web兼容的条码扫描库
使用@zxing/library(专门适配Web的条码识别工具),执行安装命令:
npm install @zxing/library # 或 yarn add @zxing/library
2. 修改组件代码,实现平台适配
通过Platform判断当前运行环境,原生平台保留原有expo-camera逻辑,Web平台切换为@zxing/library实现:
import React, { useContext, useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, interpolate, withTiming, withRepeat, Extrapolate } from 'react-native-reanimated'; import { Camera, CameraType } from 'expo-camera'; import { BarCodeScanner } from 'expo-barcode-scanner'; import { Context as MainContext } from '../context/MainContext'; import { MaterialIcons } from 'react-native-vector-icons'; import AwesomeAlert from 'react-native-awesome-alerts'; import { BrowserMultiFormatReader, NotFoundException } from '@zxing/library'; const PeachyCamera = () => { const { state: { showCamera }, toggleCamera, getProductData } = useContext(MainContext); const [showAlert, setShowAlert] = useState(false); const [alertMsg, setAlertMsg] = useState(''); const [requestObject, setRequestObject] = useState(null); const [animationLineHeight, setAnimationLineHeight] = useState(0); const [hasPermission, setHasPermission] = useState(null); const videoRef = useRef(null); const codeReaderRef = useRef(null); // 动画逻辑保留 const bar = useSharedValue(0); const barStyle = useAnimatedStyle(() => { const yValue = interpolate(bar.value, [0, 1], [0, animationLineHeight]); return { transform: [{ translateY: yValue }] }; }); useEffect(() => { bar.value = withRepeat(withTiming(1, { duration: 800 }), -1, true); }, []); // 权限处理:区分平台 useEffect(() => { const getPermissions = async () => { if (Platform.OS !== 'web') { const { status } = await Camera.requestCameraPermissionsAsync(); setHasPermission(status === 'granted'); } else { // Web端摄像头权限由zxing请求,这里默认设为true,后续可根据实际状态调整 setHasPermission(true); initWebScanner(); } }; getPermissions(); }, []); // Web端初始化扫描器 const initWebScanner = () => { if (!videoRef.current) return; const codeReader = new BrowserMultiFormatReader(); codeReaderRef.current = codeReader; codeReader.decodeFromVideoElement( videoRef.current, (result, err) => { if (result) { handleBarCodeScanned({ data: result.text }); } // 忽略NotFound异常,持续扫描 if (err && !(err instanceof NotFoundException)) { console.error(err); } } ); }; // 统一的扫描处理逻辑 const handleBarCodeScanned = async ({ data }) => { if (data) { setRequestObject({ upcCode: data }); console.log(data); // 可选:扫描到后停止Web端扫描 if (Platform.OS === 'web' && codeReaderRef.current) { codeReaderRef.current.reset(); } } }; const handleConfirmClick = () => { toggleCamera(false); getProductData(requestObject); }; // 权限未获取时的提示 if (hasPermission === null) { return <View />; } if (hasPermission === false) { return <Text>No access to camera</Text>; } return ( <> {Platform.OS !== 'web' ? ( // 原生平台保留原有Camera组件 <Camera style={StyleSheet.absoluteFillObject} onBarCodeScanned={handleBarCodeScanned} barCodeScannerSettings={{ barCodeTypes: [BarCodeScanner.Constants.BarCodeType.upc_a, BarCodeScanner.Constants.BarCodeType.upc_e, BarCodeScanner.Constants.BarCodeType.upc_ean] }} > <TouchableOpacity onPress={() => toggleCamera(false)} style={{ position: 'absolute', top: '6%', left: '6%', zIndex: 9999 }}> <MaterialIcons name="cancel" color="#fff" size={34} style={{ opacity: 1 }} /> </TouchableOpacity> <View style={styles.layerTop} /> <View style={styles.layerCenter}> <View style={styles.layerLeft} /> <View style={styles.focused}> <View onLayout={e => setAnimationLineHeight(e.nativeEvent.layout.height)} style={styles.focusedContainer}> <Animated.View style={[styles.animationLineStyle, barStyle]} /> </View> </View> <View style={styles.layerRight} /> </View> <View style={styles.layerBottom} /> </Camera> ) : ( // Web平台使用video元素配合zxing <View style={StyleSheet.absoluteFillObject}> <video ref={videoRef} style={StyleSheet.absoluteFillObject} autoPlay playsInline /> <TouchableOpacity onPress={() => toggleCamera(false)} style={{ position: 'absolute', top: '6%', left: '6%', zIndex: 9999 }}> <MaterialIcons name="cancel" color="#fff" size={34} style={{ opacity: 1 }} /> </TouchableOpacity> <View style={styles.layerTop} /> <View style={styles.layerCenter}> <View style={styles.layerLeft} /> <View style={styles.focused}> <View onLayout={e => setAnimationLineHeight(e.nativeEvent.layout.height)} style={styles.focusedContainer}> <Animated.View style={[styles.animationLineStyle, barStyle]} /> </View> </View> <View style={styles.layerRight} /> </View> <View style={styles.layerBottom} /> </View> )} <AwesomeAlert show={showAlert} title='' message={alertMsg} closeOnTouchOutside={true} closeOnHardwareBackPress={false} showConfirmButton={true} confirmText={'Continue'} confirmButtonColor="#009688" onDismiss={() => setShowAlert(false)} onConfirmPressed={handleConfirmClick} actionContainerStyle={{ flexDirection: 'row-reverse' }} /> </> ); }; const opacity = 'rgba(0, 0, 0, .6)'; const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column' }, layerTop: { flex: 2, backgroundColor: opacity }, layerCenter: { flex: 1, flexDirection: 'row' }, layerLeft: { flex: 1, backgroundColor: opacity }, focused: { flex: 10 }, layerRight: { flex: 1, backgroundColor: opacity }, layerBottom: { flex: 2, backgroundColor: opacity }, container1: { flex: 1, position: 'relative' }, overlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, unfocusedContainer: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)' }, middleContainer: { flexDirection: 'row', flex: 1.5 }, focusedContainer: { flex: 6 }, animationLineStyle: { height: 2, width: '100%', backgroundColor: 'red' }, rescanIconContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' }, }); export default PeachyCamera
3. 额外注意事项
- Web端部署时,需要确保网站使用HTTPS协议(摄像头权限仅在HTTPS环境下生效,本地开发的
localhost除外)。 - 可以根据需求调整
@zxing/library的扫描配置,比如指定条码类型(对应你原来的UPC类型),避免不必要的识别开销。
内容的提问来源于stack exchange,提问作者Ido Levi
相关产品推荐
相关产品推荐

