You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 22:57:27