News

お知らせ

2023年1月23日 投稿:motoyama

Zxingでバーコードリーダーを作ってみた

こんにちは!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-useuseDebounceを利用して、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リポジトリの連携 + デプロイが簡単に行えます。

デプロイ方法はこちらでは記載しませんが、私は下記記事を参考にしました。

GitHubのリポジトリとNetlifyを接続して、ホスティングする

https://www.newt.so/docs/tutorials/connect-to-netlify


実際に動作を確認してみます!

1次元バーコードの読み取りがちゃんと行えてますね🙌

良かったらみなさんもぜひ使ってみてください!

URL:https://online-barcode-scanner.netlify.app/

最後に

今回はバーコードリーダーアプリを作成してみました!

とても簡単にアプリを作成出来ました!(ほとんどライブラリ頼りですが。。)

このバーコードリーダーを使って、在庫管理業務を効率化出来そうです🙌

ソースコードを公開しているので、ぜひ参考にしてみてください。

次回以降も、目的を持って何かアプリを作ってみたいと思います!

最後までご覧いただきありがとうございました。


【アプリを作ってほしいというご要望があれば、ぜひご連絡お待ちしております】

stak, Inc. お問い合わせ先

【Twitterのフォローお願いします】

stak, Inc. Twitter

stakの最新情報を受け取ろう

stakはブログやSNSを通じて、製品やイベント情報など随時配信しています。
メールアドレスだけで簡単に登録できます。