QRCode Scanner with VueJS

使用 web 技術製作 QRCode scanner

這次的活動我們採取了一個技術用於解決工作人員用手機的方式作兌換票券動作,去年活動中,由於 iOS 並不支持存取 Camera 的權限而使用 PC 去作兌換,但今年年初 apple 終於宣布支持 iOS Safari 11 now supports WebRTC | Streaming Video WebRTC server and SIP gateway for browsers and mobile apps ,因此終於可以拿手機的方式來兌換票券了。

排除掉商業邏輯和規則外,主要流程如下圖:

就 Scanner 狀態而言,粗略會有三種狀態:

由於需要應付瞬間流量,這次的技術選型偏向於 serverless,並且採用 firestore 作為基礎,VueJS 為前端框架進行實作,關於 firestore 與 fulltext search 等技術細節將會在日後談到,本篇專注在 Scanner 的實作上,但就整體結構來說會是如下圖:

就核心項目而言,理論上來說我們會有個組件調用其 Camera,並且將資料發送 request 到 firestore 裡面進行兌換更改狀態。所幸已經有大神寫了 GitHub — gruhn/vue-qrcode-reader: A set of Vue.js components for detecting and decoding QR codes. 這個 QRCode reader,將其採用後開始調用客製化部分。

Installation

$ npm install — save vue-qrcode-reader

接著到 main.js 將組件進行註冊

import VueQrcodeReader from 'vue-qrcode-reader'
Vue.use(VueQrcodeReader);

就可以在各個組件進行調用,像是這樣:

<qrcode-reader
@decode="onDecode"
></qrcode-reader>

註:在 1.3.0 當中作者已經將其拆分成三個組件,讓功能更加細分化,詳情可以看 repo 及文檔 Split qrcode-reader in 3 different components by gruhn · Pull Request #73 · gruhn/vue-qrcode-reader · GitHub

接著我們到底下的 Vue instance 內,新建 methods: onDecode,用於掃描後要觸發的動作。
根據我們的一些情境與現場狀況,我做了一些參數和功能的調整,原則上會以”現場人員能第一時間得知狀況和票券狀態為出發項目”,這個組件預設同一組 QRCode 並不支持重複掃描,會將其存放在 cache 內,但實際上我們會比較希望現場工作人員馬上得知結果反應,所以做了一些修改:

<qrcode-reader
@decode="onDecode"
:camera="cameraSettings"
:track="false"
:paused="paused"
></qrcode-reader>

以下針對各個 property 進行說明:

@decode // 掃描後觸發
:camera // 鏡頭設定
:track // 追蹤,偵測 QRCode 後會有紅框標記出來
:paused // 暫停鏡頭

鏡頭設定可以參考 MediaDevices.getUserMedia() — Web APIs | MDN ,就我們活動而言不需用到前鏡頭(user),所以直接設定 facing mode 為 environment,也不需要調用到麥克風,因此也設定關閉。

data() {
return {
cameraSettings: {
audio: false,
video: {
facingMode: { ideal: 'environment' }
}
},
}

接著掃描後我會希望將鏡頭暫停,因此用到 雙向綁定參數 paused

data() {
return {
...
paused: false
}
}

最後在 onDecode 這邊我會希望有順序執行,用到了 async await 以及 Promise 來控制整體流程。

methods: {
async onDecode (content) {
try {
this.content = content;
this.pauseCamera(); // 暫停鏡頭準備調用

// 調用 redeem 進行兌換
let message = await this.redeem(content);
// 兌換成功後彈出訊息並重新啟用鏡頭
Swal('Good job!',
message,
'success').then(() => {
this.unPauseCamera()
});
} catch (error) {
Swal('Whoops!',
error.message,
'error').then(() => {
this.unPauseCamera()
});
}
},
pauseCamera () {
this.paused = true
},
unPauseCamera () {
this.paused = false
},
redeem (content) {
return new Promise((resolve, reject) => {
// 兌換票券請求
if (content) {
resolve('Success');
} else {
reject('failed');
}
}
}

其成果流程大概是這樣子:

往後我們將會談論到關於 Firestore 的一些細節以及與 fulltext search with algolia 的技術,Cheers! 🎉