こんにちは!stak, Inc.のフロントエンドエンジニア本山です。
今回は バーコードリーダーアプリ(Web版)を作ってみようと思います!!
なぜバーコードリーダーを作ろうと思いたったのかというと、「stakデバイスの管理業務を効率化したい!」という理由です。
通常、シリアルナンバーは缶の中に内包されてあるQRコードを読み取って、取得します。
そのため、缶を開けるという作業が発生してしまいます。通常利用であれば、この方法でも問題ありません。
しかし、在庫全てのシリアルナンバーを読み取ろうとすると、塵も積もってかなりの時間を要することとなります。
「何か方法は無いのか…」と考えているとき、ふと缶の外側にバーコードがあることに気が付きました。
このバーコードを読み取れば、シリアルナンバーが取得でき、なおかつ缶を開ける作業も省くことが出来ると考えたのです。
以上の背景から、私バーコードリーダーアプリ作ります!!
開発環境
今回は React × TypeScript で開発を行います。
ルーティングも無いですし、わざわざNext.jsを使うほどでも無いかなと思い、Reactで開発することにしました。
主な使用技術
- React v18.2.0
- TypeScript v4.9.4
- zxing/browser v.0.19.1
- zxing/library v0.1.1
- Material UI v5.11.4
- react-use v17.4.0
環境構築
npxではなくyarnでReactプロジェクトを作成します。
yarn create react-app online-barcode-scanner
続いて、react-use を導入します。
yarn add react-use
react-useとは、便利hooksが定義されているライブラリです。
便利なことはさることながら、hooks の実装や設計から学べることは多いです。
後ほど、クリップボードコピー機能などに使用していきます。
バーコード読み取り機能実装
@zxing/browser , @zxing/libraryを導入して、バーコード読み取り機能実装を行っていきます。
ZXing(ゼブラクロッシング)は、Javaで実装されたオープンソースのマルチフォーマット1次元・2次コード画像処理ライブラリです。
.NETやC++など様々な言語の展開がされています。
今回はJavaScript版を使用します。
yarn add @zxing/browser @zxing/library
他にもquaggaJSというライブラリもありますが、TypeScriptで書かれていないのもあり、ZXingにしました。
次にScannerコンポーネントを作成します。
src/components/Scanner.tsx
import { BrowserMultiFormatReader } from '@zxing/browser'
import { Result } from '@zxing/library'
import { useMemo, useRef } from 'react'
import { useDebounce } from 'react-use'
type ScannerProps = {
onReadCode?: (text: Result) => void
}
export const Scanner = ({ onReadCode }: ScannerProps) => {
const videoRef = useRef<HTMLVideoElement>(null)
const codeReader = useMemo(() => new BrowserMultiFormatReader(), [])
useDebounce(async () => {
if (!videoRef.current) return
await codeReader.decodeFromVideoDevice(undefined, videoRef.current, (result, error) => {
if (!result) return
if (error) {
console.log('ERROR!! : ', error)
return
}
onReadCode?.(result)
})
}, 2000)
return <video style={{ width: '100%' }} ref={videoRef} />
}
BrowserMultiFormatReader
は1次元・2次元どちらのバーコードも読み取れます。
当初の目的では、1次元バーコードのみ読み取れたら良しとしていました。
しかし、@zxing/browser
に1次元のみを読み取るオブジェクトは、現在ありません。
そのため、BrowserMultiFormatReader
を使用してます。
2次元専用はあるんですけどね。。
実際に読み取りを実行しているのは、decodeFromVideoDevice
関数です。
第1引数をundefined
にしているのは、背面カメラ利用を優先させるためです。
第2引数にはvideo
要素に仕掛けるDOMのrefを指定している。
第3引数が結果受け取りのコールバックとなっており、ここで受け取った結果を利用している。
react-use
のuseDebounce
を利用して、2秒毎に処理を実行しております。
src/components/App.tsx
import { useState } from 'react'
import { Scanner } from './Scanner'
export const App = () => {
const [codes, setCodes] = useState<string[]>([])
return (
<div>
<Scanner
onReadCode={(result) => setCodes((codes) => Array.from(new Set([...codes, result.getText()])))}
/>
<textarea value={codes.join('\n')} />
<button type={'button'}>コピー</button>
</div>
)
}
これで読み取りして、表示するところまでできました。
取得したコードは、<textarea>
内で改行して表示するようにしてます。
コピー機能は後ほど実装します。
次に見た目を整えていきます。レスポンシブにもちゃっかり対応していきます!
UI対応
video, textarea, buttonのみのなので、大したUIにはなりません😇
シンプルで直感的が良いんです!!
UIはMaterial UIを使用します。
CSSで直接スタイルしていくでも良かったのですが、Mateial UI使ってみたい + Snackbarがあるので、導入しました!
後ほど、Snackbarを使って通知UIを作ります。
yarn add @mui/material @emotion/react @emoion/style`
App.tsx
を修正します。
import { useState } from 'react'
import { Scanner } from './Scanner'
import { Box, Button, Container, Stack, TextField, useMediaQuery, useTheme } from '@mui/material'
export const App = () => {
const [codes, setCodes] = useState<string[]>([])
const theme = useTheme()
const matches = useMediaQuery(theme.breakpoints.up('md'))
return (
<Container>
{matches ? (
<Box display={'flex'} gap={2}>
<Box flex={1}>
<Scanner
onReadCode={(result) => setCodes((codes) => Array.from(new Set([...codes, result.getText()])))}
/>
</Box>
<Stack width={300} spacing={2}>
<TextField fullWidth multiline rows={10} value={codes.join('\n')} />
<Button variant={'contained'} fullWidth>
コピー
</Button>
</Stack>
</Box>
) : (
<Stack spacing={2}>
<Scanner
onReadCode={(result) => setCodes((codes) => Array.from(new Set([...codes, result.getText()])))}
/>
<TextField fullWidth multiline rows={5} value={codes.join('\n')} />
<Button variant={'contained'} fullWidth>
コピー
</Button>
</Stack>
)}
</Container>
)
}
PC版UI
モバイル版UI
次にクリップボードコピー機能を実装します。
クリップボードコピー
素のJavascriptでも実装は可能なのですが、せっかくreact-use
を導入したので、提供されているuseCopyToClipboard
を使用します。
App.tsx
に追加します。
import { useState } from 'react'
import { Scanner } from './Scanner'
import { useCopyToClipboard } from 'react-use'
import { Box, Button, Container, Stack, TextField, useMediaQuery, useTheme } from '@mui/material'
export const App = () => {
const [codes, setCodes] = useState<string[]>([])
const [, copyToClipboard] = useCopyToClipboard()
return(
// 省略
<Button variant={'contained'} fullWidth onClick={() => copyToClipboard(codes.join('\n'))}>
コピー
</Button>
)
}
第1引数は、コピーした値やエラー情報が入ったオブジェクトです。今回は特に必要がないので省略しています。
第2引数は、コピーを実行する関数です。ボタンをクリックするとコピーを実行します。
通知
機能実装の最後は、バーコードの読み取りが成功したら、画面上に通知を表示するようにしたいと思います。
Material UIのSnackbarを使用します。
src/hooks/useToast.tsx
import { Snackbar, SnackbarProps } from '@mui/material'
import { ReactElement, useCallback, useState } from 'react'
import { SnackbarOrigin } from '@mui/material/Snackbar/Snackbar'
type ToastProps = {
autoHideDuration?: SnackbarProps['autoHideDuration']
children: ReactElement
horizontal?: SnackbarOrigin['horizontal']
isOpen: boolean
onClose?: () => void
vertical?: SnackbarOrigin['vertical']
}
export const useToast = () => {
const [isOpen, setIsOpen] = useState(false)
const handleOnOpen = useCallback(() => {
setIsOpen(true)
}, [])
const handleOnClose = useCallback(() => {
setIsOpen(false)
}, [])
const Toast = useCallback(
({ autoHideDuration = 2000, children, horizontal = 'center', isOpen, onClose, vertical = 'top' }: ToastProps) => {
return (
<Snackbar
anchorOrigin={{ horizontal, vertical }}
open={isOpen}
autoHideDuration={autoHideDuration}
onClose={onClose}
>
{children}
</Snackbar>
)
},
[]
)
return [
Toast,
{
handleOnClose,
handleOnOpen,
isOpen,
},
] as const
}
今回はhooksとして実装しています。
Toast
コンポーネント、Open関数、Close関数、開閉ステータスの4点を返すようにしています。
App.tsx
でhooksを呼び出します。
import { useState } from 'react'
import { Scanner } from './Scanner'
import { useCopyToClipboard } from 'react-use'
import { Alert, Box, Button, Container, Stack, TextField, useMediaQuery, useTheme } from '@mui/material'
// 追加
import { useToast } from '../hooks/useToast'
export const App = () => {
const [codes, setCodes] = useState<string[]>([])
const [, copyToClipboard] = useCopyToClipboard()
const theme = useTheme()
const matches = useMediaQuery(theme.breakpoints.up('md'))
// 追加
const [Toast, { handleOnClose, handleOnOpen, isOpen }] = useToast()
return (
<Container>
{matches ? (
<Box display={'flex'} gap={2}>
<Box flex={1}>
<Scanner
onReadCode={(result) => {
setCodes((codes) => Array.from(new Set([...codes, result.getText()])))
handleOnOpen()
}}
/>
</Box>
<Stack width={300} spacing={2}>
<TextField fullWidth multiline rows={10} value={codes.join('\n')} />
<Button variant={'contained'} fullWidth onClick={() => copyToClipboard(codes.join('\n'))}>
コピー
</Button>
</Stack>
</Box>
) : (
<Stack spacing={2}>
<Scanner
onReadCode={(result) => {
setCodes((codes) => Array.from(new Set([...codes, result.getText()])))
handleOnOpen()
}}
/>
<TextField fullWidth multiline rows={5} value={codes.join('\n')} />
<Button variant={'contained'} fullWidth onClick={() => copyToClipboard(codes.join('\n'))}>
コピー
</Button>
</Stack>
)}
// 追加
<Toast isOpen={isOpen} onClose={handleOnClose}>
<Alert onClose={handleOnClose} severity={'success'} sx={{ width: '100%' }}>
バーコードの読み取りに成功しました!
</Alert>
</Toast>
</Container>
)
}
Toast
コンポーネントのchildren
には、Alert
コンポーネントを使用してます。
useToast
が提供するisOpen
がtrueになれば、Toast
コンポーネントが表示される仕組みです。
簡単ですね!
Alert
コンポーネントの✗ボタンで、通知が閉じるようにもしています。
デプロイ
最後に、みなさんに使ってもらえるように、デプロイをしていきます。
Netlifyというサービスを使用してます。
GitHubリポジトリの連携 + デプロイが簡単に行えます。
デプロイ方法はこちらでは記載しませんが、私は下記記事を参考にしました。
https://www.newt.so/docs/tutorials/connect-to-netlify
実際に動作を確認してみます!
1次元バーコードの読み取りがちゃんと行えてますね🙌
良かったらみなさんもぜひ使ってみてください!
URL:https://online-barcode-scanner.netlify.app/
最後に
今回はバーコードリーダーアプリを作成してみました!
とても簡単にアプリを作成出来ました!(ほとんどライブラリ頼りですが。。)
このバーコードリーダーを使って、在庫管理業務を効率化出来そうです🙌
ソースコードを公開しているので、ぜひ参考にしてみてください。次回以降も、目的を持って何かアプリを作ってみたいと思います!
最後までご覧いただきありがとうございました。
【アプリを作ってほしいというご要望があれば、ぜひご連絡お待ちしております】
【Twitterのフォローお願いします】