ReactでQRコードを表示したり読み取ったり
アプリ内でQRコードをしようする機会があったので備忘録と復習のためにまとめることにした。
実装する機会はそこまで多くないと思うが、いざ実装するときのため。
はじめは全てライブラリで実装しようと思ったが、iPhoneでの問題があったりでスキャン部分だけjsQRを使用して実装することにした。
準備
- QRコード表示
- 読み取り(カメラ)
- 読み取り(画像)
環境
- Next14(を使っているが細かくコンポーネントを分けないのでクライアントコンポーネントで作っちゃう)
- React
- typescript
- tailwind
ライブラリ
qrcode.react(表示)
jsQR(読み取り) react-dropzone(一応対応)実装
ライブラリのインストール
pnpm add jsqr qrcode.react react-dropzone
QRコードの表示
まずは簡単な方から。
QRCodeSVG
をインポートしてvalueに文字列を入力するだけでQRコードを作成してくれる簡単なライブラリ。
import { NextPage } from 'next'
import { QRCodeSVG } from 'qrcode.react'
const Home: NextPage = () => {
return (
<main className='flex min-h-screen flex-col items-center justify-between p-24'>
<div className='aspect-square h-72 w-72 rounded-md bg-white p-4'>
<QRCodeSVG value={`http://localhost:3000/result`} size={224} />
</div>
</main>
)
}
export default Home
今回は読み取り→遷移という流れにしたいのでURLを入れておく。
あとはサイズをちょこちょこ直す。
これで完成。簡単。
読み取り(カメラ)
QRコードを使用できるアプリでよく見る、カメラで読み取る機能の実装。
PCのインカメなので表示隠ししています
スキャン後。携帯に先程のQRコードの画像を写してスキャンさせた。
'use client'
import jsQR from 'jsqr'
import Link from 'next/link'
import React, { useRef, useState, useEffect, FC } from 'react'
type Props = {}
const QrCodeScanner: FC<Props> = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [result, setResult] = useState('')
const [error, setError] = useState('')
useEffect(() => {
const constraints = {
video: {
facingMode: 'environment',
width: { ideal: 300 },
height: { ideal: 300 },
},
}
// デバイスのカメラにアクセスする
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
// デバイスのカメラにアクセスすることに成功したら、video要素にストリームをセットする
if (videoRef.current) {
videoRef.current.srcObject = stream
videoRef.current.play()
scanQrCode()
}
})
.catch((err) => console.error('Error accessing media devices:', err))
const currentVideoRef = videoRef.current
// コンポーネントがアンマウントされたら、カメラのストリームを停止する
return () => {
if (currentVideoRef && currentVideoRef.srcObject) {
const stream = currentVideoRef.srcObject as MediaStream
const tracks = stream.getTracks()
tracks.forEach((track) => track.stop())
}
}
}, [])
const scanQrCode = () => {
const canvas = canvasRef.current
const video = videoRef.current
if (canvas && video) {
const ctx = canvas.getContext('2d')
if (ctx) {
// カメラの映像をcanvasに描画する
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// QRコードをスキャンする
const qrCodeData = jsQR(imageData.data, imageData.width, imageData.height)
if (qrCodeData) {
// スキャンされた内容を確認する
if (qrCodeData.data !== 'http://localhost:3000/result') {
setError('対応していないQRコードです')
setTimeout(scanQrCode, 100) // スキャンの頻度を制限
return
}
setResult(qrCodeData.data)
return
}
setTimeout(scanQrCode, 100)
}
}
}
return (
<div>
{!result && (
<div className='flex justify-center'>
<div className='relative h-[300px] w-[300px]'>
<video ref={videoRef} autoPlay playsInline className='absolute left-0 top-0 -z-50 h-[300px] w-[300px]' />
<canvas ref={canvasRef} width='300' height='300' className='absolute left-0 top-0' />
</div>
</div>
)}
{result && (
<div className='flex justify-center'>
<Link href={result}>
<button>push</button>
</Link>
</div>
)}
{error && <p className='text-center text-xs text-red-500'>{error}</p>}
</div>
)
}
export default QrCodeScanner
何をしているか簡単に説明すると
カメラ機能
useEffect
の中身。
-
navigator.mediaDevices.getUserMedia
でメディアデバイスのアクセス許可を取る - 許可された場合に
video
に指定しているrefのsrcObjectにストリームを指定してvideo
で再生する - クリーンアップ時にはsrcObjectのトラックを停止してリソースの解放とカメラの使用の終了をする
という流れ。
読み取り
scanQrCode
の中身。
- キャンバスの2Dレンダリングコンテキスト(ctx)を取得
- ビデオのスナップショットをキャンバスに表示
-
ctx.getImageData
でピクセルデータを取得 -
jsQR
関数で画像データからQRコードをスキャン - QRコードのデータがあるときにスキャンされた内容が正しければ
result
にセットする - QRコードのデータがなかったり内容に間違いがあれば、エラーを表示して100ミリ秒後にscanQrCode関数を再度実行してスキャンを継続
という流れ。
あとはresultがあればボタン表示してーなどの表示を調整。
今回は読み取り後にボタンを表示させているが、ここで遷移させたりなど読み取ったあとにやりたい処理を書けばいい。
問題発生と解決方法
始めはvideo
要素をhidden(display: none
)で隠して実装していた(カメラの表示に必要なのは1つなので)が、
iPhoneのsafariで起動したら動かない……!(正確に言うと一回目のキャプチャで映像が止まってしまう)
またかと。いつもいつもiOS、safariで問題が起きるんですよね。
そこで調べたところ
-
video
要素はdisplay: none
やvisibilty: hidden
で隠してはいけない -
video
要素にautoPlay
とplaysInline
を指定する
でvideo
をcanvas
の後ろに隠す実装にしたら解決できた。
参考
読み取り(画像)
カメラが有るときは画像選択もできるということで。
スキャン後(下のやつ)。
カメラとだいたいやっていることは同じ。カメラではなく画像にしただけ。
'use client'
import jsQR from 'jsqr'
import Link from 'next/link'
import { FC, useState } from 'react'
import { useDropzone } from 'react-dropzone'
type Props = {}
const QrReaderImage: FC<Props> = () => {
const [result, setResult] = useState<string | undefined>()
const [error, setError] = useState<string | undefined>()
const onDrop = async (files: File[]) => {
if (files.length < 1) return setError('ファイルを選択されます')
const file = files[0]
// 画像の判定
if (!file.type.includes('image')) return setError('画像じゃありません')
if (file) {
const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => {
if (!e.target) return
const img = new Image()
img.onload = () => {
// キャンバス作成
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (ctx) {
// キャンバスに画像を描画
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// QRコードをスキャン
const qrCodeData = jsQR(imageData.data, imageData.width, imageData.height)
if (!qrCodeData) {
setResult(undefined)
setError('QRコードを読み込めませんでした')
return
}
// スキャンされた内容の確認
if (qrCodeData.data !== 'http://localhost:3000/result') {
setResult(undefined)
setError('対応していないQRコードです')
return
}
setResult(qrCodeData.data)
}
}
img.src = e.target.result as string
}
reader.readAsDataURL(file)
}
}
const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple: false })
return (
<>
{!result && (
<div className='h-72 w-72 bg-white rounded-sm border border-dashed border-gray-600' {...getRootProps()}>
<input {...getInputProps()} />
{error && <p className='text-center text-xs text-red-500'>{error}</p>}
</div>
)}
{result && (
<div className='flex justify-center text-black'>
{' '}
<Link href={result}>
<button>push</button>
</Link>
</div>
)}
</>
)
}
export default QrReaderImage
何をしているか
画像の取得
react-dropzoneで実装。
ドラッグアンドドロップでも画像選択できますよ。に対応。
読み取り
onDrop
の中身。
- 画像を取得
-
reader.readAsDataURL
でreader.onload
を発火させる - 画像の読み込みが終わったら
canvas
要素を処理内で作成 - 画像をキャンバスに表示
-
jsQR
関数で画像データからQRコードをスキャン - QRコードのデータがあるときにスキャンされた内容が正しければ
result
にセットする
画像の取得先が違うだけでQRコードを読み取る箇所についてはほぼ同じ。
これで完成。
ちなみにpageの最終コード。
import QrCodeScanner from '~/components/QrReader'
import { NextPage } from 'next'
import { QRCodeSVG } from 'qrcode.react'
import QrReaderImage from '~/components/QrReaderImage'
const Home: NextPage = () => {
return (
<main className='flex min-h-screen flex-col items-center justify-between p-24'>
<div className='aspect-square h-72 w-72 rounded-md bg-white p-4'>
<QRCodeSVG value={`http://localhost:3000/result`} size={224} />
</div>
<p className='mt-8 text-center text-sm text-gray-500'>QRコードをスキャン</p>
<div className='bg-white p-4 rounded-md'>
<QrCodeScanner />
</div>
<p className='mt-8 text-center text-sm text-gray-500'>QRコードをスキャン</p>
<div className='bg-white p-4 rounded-md'>
<QrReaderImage />
</div>
</main>
)
}
export default Home
まとめ
スキャンに関する部分は同じ流れなので、QRコードの画像取得さえできればだいたい対応できる。
ただミソとなる部分はやっぱり画像の取得なのでそこは要件に合わせて実装する必要がある。
パフォーマンス面の調整など必要になると思うが、スキャン前、スキャン後を自作にする分エラー処理なんかも直感的に書きやすくなるので、フルでライブラリ使わずに作っても案外悪くないなという印象だった。
Discussion