ViteでQRリーダーアプリを作る

7 min read読了の目安(約6300字

はじめに

こちらの記事で既にQRリーダーアプリの作り方は載っていますが、Vue.jsで作りたかったのでそれで作った時の内容をまとめたいと思います。またせっかくなので最近流行りのViteで環境構築し、それをGitHub Pagesで公開しました。

https://qiita.com/kan_dai/items/4331aae12f5f2d3ad18d

ViteでVue.js 3が動くようにする

Viteでの環境構築はかなり楽で、公式サイトに書かれている通りにすると簡単に構築できます。

$ yarn create @vitejs/app my-vue-app --template vue-ts

追加で僕はpugとsassを使いたかったのでyarn addします。

$ yarn add --dev pug sass

QRリーダーアプリの実装

最初に共有した記事を参考に、QRリーダーコンポーネントを実装します。

QRReader.vue
<template lang="pug">
div
  video.video(ref="elVideoRef")
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onBeforeUnmount } from 'vue';
import jsQR from 'jsqr';

/** スキャンする間隔 */
const INTERVAL = 200;

export default defineComponent({
  name: 'QRReader',
  emits: {
    scan: (code: string) => {
      return !!code;
    },
  },
  setup(props, context) {
    let intervalId: number;
    const elVideoRef = ref<HTMLVideoElement>();
    const elInternalCanvas = document.createElement('canvas');

    const scanQRCode = () => {
      const elVideo = elVideoRef.value;

      if (!elVideo) {
        return;
      }

      const ctx = elInternalCanvas.getContext('2d');
      if (ctx == null) {
        return;
      }

      ctx.drawImage(elVideo, 0, 0, elInternalCanvas.width, elInternalCanvas.height);
      const imageData = ctx.getImageData(0, 0, elInternalCanvas.width, elInternalCanvas.height);

      const code = jsQR(imageData.data, elInternalCanvas.width, elInternalCanvas.height);
      if (code) {
        context.emit('scan', code.data);
      }
    };

    onMounted(async () => {
      if (!elVideoRef.value) {
        return;
      }
      const elVideo = elVideoRef.value;
      elVideo.srcObject = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          facingMode: 'environment',
        },
      });
      elVideo.setAttribute('playsinline', 'true');
      elVideo.onloadedmetadata = () => {
        elVideo.play();
        console.log(`video resolution: ${elVideo.videoWidth} x ${elVideo.videoHeight}`);
        elInternalCanvas.width = elVideo.videoWidth;
        elInternalCanvas.height = elVideo.videoHeight;
      };

      intervalId = setInterval(() => {
        scanQRCode();
      }, INTERVAL);
    });

    onBeforeUnmount(() => {
      clearInterval(intervalId);
      elInternalCanvas.remove();

      if (elVideoRef.value && elVideoRef.value.srcObject) {
        if ('getVideoTracks' in elVideoRef.value.srcObject) {
          elVideoRef.value.srcObject.getVideoTracks()[0].stop();
        }
      }
    });

    return {
      elVideoRef,
    };
  },
});
</script>

<style lang="scss" scoped>
.video {
  width: 100%;
}
</style>

コンポーネントができたら後は呼び出して表示するだけです。

App.vue
<template lang="pug">
div
  .box
    QRReader(
      @scan="onScan"
    )
    .info
      div QRデータ:
      template(v-if="isUrl")
        a(:href="state.qrCode", target="_blank") {{ state.qrCode }}
      template(v-else)
        div {{ state.qrCode }}
</template>

<script lang="ts">
import { defineComponent, reactive, computed } from 'vue';
import QRReader from './components/QRReader.vue';

interface IState {
  /** QRコードデータ */
  qrCode: string;
}

export default defineComponent({
  name: 'App',
  components: {
    QRReader,
  },
  setup() {
    const state = reactive<IState>({
      qrCode: '',
    });

    const isUrl = computed(() => {
      return /^https?:\/\//.test(state.qrCode);
    });

    return {
      state,
      isUrl,
      onScan: (code: string) => {
        console.log(code);
        state.qrCode = code;
      },
    };
  },
});
</script>

<style lang="scss" scoped>
.box {
  max-width: 600px;
  margin: 0 auto;
}

.info {
  word-break: break-all;
}
</style>

PWA化する

最後にPWA化します。まずviteにはvite-plugin-pwaがあるのでそれをinstallします。

$ yarn add --dev vite-plugin-pwa

manifest.jsonの設定

そしてvite.config.tsにPWAの設定を追記します。
GitHub Pagesではサブディレクトリに成果物が配置されるため、その辺を考慮して設定する必要があります。
iconファイルは自動で配置する方法が分からなかったので、public/app-iconsにファイルを配置し、ビルド後の出力パスと合うようにsrcを指定しています。

vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

// https://vitejs.dev/config/
export default defineConfig({
  // GitHub Pagesはサブディレクトリに配置されるため相対パスでファイルアクセスする
  base: './',
  plugins: [
    vue(),
    VitePWA({
      manifest: {
        lang: 'ja',
        name: 'QR Reader',
        short_name: 'QR',
        background_color: '#fff',
        theme_color: '#3cb371',
        display: 'standalone',
	// GitHub Pagesにpushする場合はリポジトリ名を入れる必要がある
        scope: '/repository-name/',
        start_url: '/repository-name/',
        icons: [
          {
            src: 'app-icons/72x72.png',
            sizes: '72x72',
            type: 'image/png'
          },
          // 他のサイズも入れる
        ]
      }
    })
  ]
})

参考

https://github.com/vitejs/vite/issues/238
https://vitejs.dev/guide/assets.html#the-public-directory

service workerの登録

上の設定に加えて、service workerも起動するように設定する必要があります。
main.tsregisterSWを実行します。

main.ts
import { createApp } from 'vue'
import App from './App.vue'

// service workerの登録
import { registerSW } from 'virtual:pwa-register';
registerSW();

createApp(App).mount('#app')

ただここで注意なのが、virtual:pwa-registerというモジュールの型情報が入っていないためTypeScriptだとエラーになってしまいます。tsconfig.jsonincludesに型情報を追加します。

tsconfig.json
{
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    // ↓追加する
    "node_modules/vite-plugin-pwa/client.d.ts"
  ]
}

参考

https://github.com/antfu/vite-plugin-pwa/issues/38#issuecomment-812039285

GitHub Pagesにデプロイする

後はGitHub Actionsでgh-pagesブランチにpushしたら完了です。

.github/workflows/gh-pages.yml
name: github pages

on:
  push:
    branches:
      - main

jobs:
  build-deploy:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@master

      - name: Setup
        uses: actions/setup-node@v1
        with:
          node-version: '12.16.x'

      - name: Install
        run: yarn install

      - name: Build
        run: yarn build

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

終わりに

以上がViteを使ってQRリーダーアプリを作る流れでした。
ソースは以下に置いていますので、興味がある方は見てください。

https://github.com/wintyo/qr-reader