239
240

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PWAAdvent Calendar 2019

Day 1

Webの技術だけで作るQRコードリーダー

Last updated at Posted at 2019-12-01

この記事はPWA Advent Calendar 2019の1日目の記事です。

以前、PWA Night vol.9 で、Web技術だけで作るQRコードリーダーという内容で紹介したのですが、当日は時間の問題で概要程度だったのでもう少し技術的な詳細を書きます。(当日のスライドはこちら)

作ったもの

まずは、実際に見ていただくのがイメージつきやすいと思うので動作しているGIFです。

qr.gif

QRコードを読み込んで結果を表示するというものですが、これがブラウザで動いています。

実際に公開されていますのでよかったら使ってみてください。

サイト:https://simple-qr.netlify.com/
GitHub:https://github.com/KanDai/simple-qr-reader

確認している対応ブラウザは以下です。

  • Android Chrome
  • Android Firefox
  • iOS Safari

PWA化しているのでインストールして使うことができます。

QRリーダーの実装

全体像はこんなイメージです。

img.png

JavaScriptでデバイスのカメラにアクセス

MediaDevices.getUserMedia()というAPIを使用することで、JavaScriptで端末のカメラやマイクの入力ストリームを取得・操作することができます。

以下のコードだけで、Webページに端末のカメラの映像が映し出されます。
※ カメラやマイクが付いていない端末やこのAPIに対応していない環境では確認できません。

index.html
<div class="reader">
  <video id="js-video" class="reader-video" autoplay playsinline></video>
</div>
app.js
const video  = document.querySelector('#js-video')

navigator.mediaDevices
    .getUserMedia({
        audio: false,
        video: {
            facingMode: {
                exact: 'environment'
            }
        }
    })
    .then(function(stream) {
        video.srcObject = stream
        video.onloadedmetadata = function(e) {
            video.play()
        }
    })
    .catch(function(err) {
        alert('Error!!')
    })

getUserMedia() の引数の video の値を変えることで、リアカメラやフロントカメラなど使用するカメラを切り替えることができます。

app.css
*,
*:before,
*:after {
    box-sizing: border-box;
}

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    font-family: sans-serif;
}

.reader {
    width: 100vw;
    height: 100%;
    position: relative;
}

.reader-video {
    background-color: #000;
    width: 100%;
    height: 100%;
    object-fit: fill;
}

カメラの映像を表示するのに width: 100%; height: 100%; で画面全体を使えると思ってたのですが上手くいかず object-fit: fill も追加で指定することで画面全体を使えるようになりました。

取得した画像データからQRコードのデータを取得

getUserMedia() で受け取った入力ストリームを canvas を使って画像データに変換します。

次に、画像データを解析して、QRコードがあればデータを取得しますが、ここはjsQRというライブラリを使用しました。
以下のような簡単な記述で、画像にQRコードが含まれていればデータを取得できるのでとても助かりました。

const code = jsQR(imageData, width, height, options?);

if (code) {
  console.log("Found QR code", code);
}

実際のコードは以下。

index.html
<div style="display:none">
  <canvas id="js-canvas"></canvas>
</div>

canvas要素は、実際に画面上に表示される必要はないので、親要素を display:none で非表示にしてます。

app.js
const canvas = document.querySelector('#js-canvas')
const ctx = canvas.getContext('2d')

const checkImage = () => {
    // 取得している動画をCanvasに描画
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)

    // Canvasからデータを取得
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)

    // jsQRに渡す
    const code = jsQR(imageData.data, canvas.width, canvas.height)

    // QRコードの読み取りに成功したらモーダル開く
    // 失敗したら再度実行
    if (code) {
        openModal(code.data)
    } else {
        setTimeout(() => { checkImage() }, 200)
    }
}

変換した画像データを解析して、QRコードの読み取りがうまくいけばモーダルで取得結果を表示、エラーだったら再実行というのを0.2秒間隔で処理しています。
最初は負荷を気にして1秒とかで試していたのですが、それだとちょっと遅いなという感覚だったので調整していって0.2秒に落ち着きました。

モーダルを表示

モーダルの表示は受け取ったURLを書き換えて表示しているだけです。

index.html
<div id="js-modal" class="modal-overlay">
  <div class="modal">
    <div class="modal-cnt">
      <span class="modal-title">読み取り結果</span>
      <span id="js-result" class="modal-result"></span>
    </div>
    <a href="" id="js-link" class="modal-btn" target="_blank">開く</a>
    <button type="button" id="js-modal-close" class="modal-btn">閉じる</button>
  </div>
</div>
app.js
const openModal = function(url) {
    document.querySelector('#js-result').innerText = url
    document.querySelector('#js-link').setAttribute('href', url)
    document.querySelector('#js-modal').classList.add('is-show')
}

document.querySelector('#js-modal-close')
    .addEventListener('click', () => {
        document.querySelector('#js-modal').classList.remove('is-show')
        checkImage()
    })

モーダルを閉じた場合は、再度画像データを取得して解析する処理が走るようになっています。

.modal-overlay {
    display: none;
    position: fixed;
    justify-content: center;
    align-items: center;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100%;
    background-color: rgba(0, 0, 0, .7);
}

.modal-overlay.is-show {
    display: flex;
}

PWA化する

ここまででQRリーダー自体はできたので、次はPWA化について紹介します。

Web App Manifestの設定とPWACompat

manifest.json は特別なことはなく以下のような記述です。

manifest.json
{
  "name": "Simple QR Reader",
  "short_name": "Simple QR",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "display": "standalone",
  "orientation": "portrait",
  "Scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    // … その他のアイコンサイズ記載
  ]
}

Android向けには、これを読み込ませるだけでOKなのですが、iOS向けにPWA化する時は apple-mobile-web-app-capable などの独自のmetaタグを入れる必要があります。

そこで、今回はGoogleが提供するPWACompatというライブラリを使用してみました。
PWACompatは manifest.json の情報から、Web App Manifestを解釈しないブラウザのために、関連するmetaタグやlink要素を自動的に挿入してくれるので、iOS向けの記述などをする必要がありません!

HTMLの <head> 内に以下のタグを追記するだけです。

index.html
<link rel="manifest" href="manifest.webmanifest" />
<script async src="https://cdn.jsdelivr.net/npm/pwacompat@2.0.9/pwacompat.min.js" integrity="sha384-VcI6S+HIsE80FVM1jgbd6WDFhzKYA0PecD/LcIyMQpT4fMJdijBh0I7Iblaacawc" crossorigin="anonymous"></script>

実際にやってみましたが簡単すぎて感動しました。

Workboxを使用したキャッシュ

ServiceWorkerのキャッシュ制御にはWorkBoxを使用しました。
こちらも、Googleが提供するキャッシュの制御をサポートしてくれるライブラリです。

とはいえ、使用しているファイル数も少ないですし動的な処理もないので記述自体はこれだけです。

sw.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

workbox.routing.registerRoute(
    '/',
    new workbox.strategies.NetworkFirst()
)

workbox.routing.registerRoute(
    new RegExp(/(.*\.js|.*\.css|.*\.jpg|.*\.png|.*\.ico)/),
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'assets',
        plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 20
            }),
        ],
    })
)

TOPページのHTMLだけ NetworkFirst の設定でキャッシュして、それ以外の静的アセットは StaleWhileRevalidate の設定でキャッシュしています。
(プリキャッシュでもう少し細かく設定したりしようと思ったのですが少し楽をしました…)

iOSのPWAでカメラ使えない問題

アプリ自体はこれで完成ですが、冒頭で少し説明したように、iOSではgetUsermediaがSafariでしか動作しないため、standaloneで動作するPWAではカメラにアクセスすることが出来ません。これは今後のアップデートで是非とも対応してほしいところです…

caniuse.jpg

2020/4/30追記
2020/3/25に提供されたiOS13.4から動作するようになったみたいです!
実際に試してみたところ動作するのが確認できました。

非対応のブラウザにはHTML Media Captureを使う?

<input type="file"> の拡張でHTML Media Captureという機能があります。
通常の動作だと、端末に保存してある画像などを選択する画面になりますが、下記のように capture 属性を記述すると直接カメラを起動することが出来ます。

<input type="file" name="file" accept="image/*" capture="environment" />

capture 属性は user でフロントカメラ、 environment でリアのカメラが立ち上がります。
撮影した画像はFileListオブジェクトで取得できるので、それを使ってQRコードの解析をすることはできると思います。(今回のアプリではそこまでやってませんが…)

追記

ブラウザ標準のShape Detection APIというAPIを使って同じものを実装する記事を書きました。
続・Webの技術だけで作るQRコードリーダー

PWAのカンファレンスやります

コードはGitHubで公開しているので、コード全体が気になる方は覗いてみてください!

Webの技術だけでできることが、どんどん広がっているのでWebのエンジニアとしてはとても嬉しいですね!
ネイティブアプリでしか実現できなかったようなこともWebでできるようになってくるとなれば色々と夢が広がります!

そんな夢が広がるPWAですが、PWA NightというPWAをテーマにしたコミュニティを運営してます。
東京では毎月第3水曜日、大阪でも2ヶ月に1回のペースでイベントをやっています。

そして、2020年2月1日(土)にPWA Night CONFERENCE 2020というカンファレンスをやります!

豪華スピーカーを迎えて、PWAの事例やWebの最新技術の情報などを知れる1日になりますのでぜひご参加ください!

239
240
16

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
239
240

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?