🦁

React+TypeScriptなWebアプリで、QRコード読み込みしてみる。(jsQRで)

2023/12/13に公開

Abstract

React+TypescriptでQRコードを読み込む話をなるだけ簡素に。
"出来たよ!!"って記事は出てくるのに、React+Typescriptでってなると全然出てこなくって。

結論

今回の成果物はココ↓結論だけでよければ。
https://github.com/aaaa1597/react-qrcode

前提

手順

1.プロジェクト生成 -> VSCodeで開く

めんどいから、プロジェクト雛形をgithubから持ってくる。React+TypeScriptのプロジェクト雛形をgithubに置いとく話。

2.生成したプロジェクトの場所でjsqrをインストール

$ cd プロジェクトのフォルダ
$ npm install jsqr --save

2.QRCodeScanner.tsxを作成

VSCodeで、src配下に、componentsフォルダを生成 -> QRCodeScanner.tsxを生成。
中身は以下で。

QRCodeScanner.tsx
import React from 'react';

const QRCodeScanner = () => {
  return (
    <div className="App">
      hello QRCodeScanner
    </div>
  );
}

export default QRCodeScanner;

呼び元のApp.tsxは、2行追加

App.tsx
import React from 'react';
import './App.css';
+import QRCodeScanner from './components/QRCodeScanner';

function App() {
  return (
    <div className="App">
      hello world!!
+     <QRCodeScanner />
    </div>
  );
}

export default App;

3.後は、QRCodeScanner.tsxを作り込んでいく。

後はもう、ココのコードを感謝しながらコピペする。ありがとうございます。


で、完了したんだけど、分かんないところがチラホラあって。
以下、まとめ。

3-1. MediaStreamConstraintsって何者?

Webカメラ操作する時に使用するCameraパラメータのクラス。何もしなくても、使える。(組み込み型?)
- 音声追加
- 撮像サイズ指定
- フレームレート指定
- フロントカメラ指定 とか

3-2. useRefって?

React hooksのひとつ。タグ要素への参照を保持する。以降、保持った参照で、タグ要素を操作できる。例:<input ret={aaa}>ってすると、aaa.currentに<input>の参照が設定されて、input要素をaaa.currentで操作できるようになると。

3-3. HTMLVideoElement って?

動画を web ページに取り込むなんか。詳細は→HTMLVideoElement

3-4. useEffectって?

React hooksのひとつ。コンポーネントのレンダー前後のイベントで、処理をさせることが出来る。詳細は→ useEffectをちゃんと理解する #React

4. QRCodeScanner.tsxの完成コード

QRCodeScanner.tsx
import React, { useEffect, useRef, useState } from 'react';
import jsQR from 'jsqr';

const videoWidth  = 500;
const videoHeight = 500;
const videoFrameRate = 5;

const constraints: MediaStreamConstraints = {
  audio: false,
  video: {
    width:  { min: 1280, ideal: 1920, max: 2560 },
    height: { min: 720 , ideal: 1080, max: 1440 } ,
    frameRate: {
      max: videoFrameRate,
    }//,
    // facingMode: {
    //   exact: 'environment',
    // },
  },
};
  
const QRCodeScanner = () => {
  const videoRef    = useRef<HTMLVideoElement>(null);
  const intervalRef = useRef<number>();
  const canvasRef   = useRef<HTMLCanvasElement>(null);
  const [isContinue, setIsContinue] = useState(false);
  const [qrCodeData, setQrCodeData] = useState<string[]>([]);

  useEffect(() => {
    const openCamera = async () => {
      const video = videoRef.current;
      if (video) {
        video.addEventListener('play', (event) => { console.log(event); console.log("(w,h)=(", video.videoWidth, ", " + video.videoHeight, ")") })
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
      }
    };
    openCamera();
  }, [])

  useEffect(() => {
    if (!isContinue) {
      return;
    }

    const decodeQRCode = () => {
      const context = canvasRef?.current?.getContext('2d');
      const video = videoRef?.current;

      if (!context || !video) {
        return;
      }

      context.beginPath();
      context.fillStyle = 'rgb( 0, 0, 0)';
      context.fillRect(0, 0, 100, 200);

      context.drawImage(video, 0, 0, videoWidth, videoHeight);
      const imageData = context.getImageData(0, 0, videoWidth, videoHeight);
      const code = jsQR(imageData.data, videoWidth, videoHeight);

      return code?.data;
    };

    const intervalId = window.setInterval(() => {
      const decodedValue = decodeQRCode();

      if (!decodedValue || qrCodeData.includes(decodedValue)) {
        return;
      }

      setQrCodeData([...qrCodeData, decodedValue]);
    }, 1_000 / videoFrameRate);
    intervalRef.current = intervalId;

    return () => {
      clearInterval(intervalRef.current);
    };
  }, [isContinue, qrCodeData]);
  
  const handleStart = () => {
    setIsContinue(true);
  };

  const handleStop = () => {
    setIsContinue(false);
  };

  return (
    <div className="App">
      <p>QR Code Scanner</p>
      <div style={{ display: 'grid' }}>
        <div>
          <video autoPlay playsInline={true} ref={videoRef} style={{ width: '100%' }}>
            <canvas width={videoWidth} height={videoHeight} ref={canvasRef} />
          </video>
        </div>
        <div>
          <p>{qrCodeData.join('\n')}</p>
        </div>
        <div>
          <button onClick={handleStart}>Start Scan</button>
          <button onClick={handleStop}>Stop Scan</button>
        </div>
      </div>
      hello QRCodeScanner
    </div>
  );
}

export default QRCodeScanner;

で、実行。


出来た!!
けど、感度あんまりよくないかも。

Discussion