寫入資料至 Firestore
這個專案跟上一個活動派票流程上是連續的。活動開始前,我們產生 QRCode 票券並派送出去;活動當天,工作人員開始掃 QRCode 並即時更新報到資訊。
現場的掃票系統是以 Firebase 開發的,資料也是從 Firebase 的 Firestore 中讀取,為此活動開始前我們要將票券資料從 MySQL 資料庫寫入 Firestore 中。
Google 提供多種方式將資料寫入 Firestore,其中當然也包括 PHP,但必須安裝額外擴充;由於票券資料不算太多,我們選擇了另一種方式:將票券先匯出 JSON 檔,再藉由 NodeJs 寫入 Firestore。
Step1. Initialize
首先引入必要的套件及憑證檔,進行初始化設定:
const serviceAccount = require('path/to/serviceAccountKey.json');
const admin = require("firebase-admin");
const db = admin.firestore();
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
Step2. Read Json File
接著讀取先前匯出的票券 JSON 檔,JSON 的第一層 index 名稱將作為 Firestore 中的 collection 名稱,傳入一隻負責寫入資料的函式。:
const fs = require('fs')
const file = process.cwd() + '/tickets.json'
fs.readFile(file, 'utf8', (err, data) => {
udpateCollection(JSON.parse(data)).then(() => {
process.exit()
})
})
async function udpateCollection (dataArray) {
for (const index in dataArray){
const collectionName = index;
for (const doc in dataArray[index]){
if (dataArray[index].hasOwnProperty(doc)){
await startUpading(collectionName, doc, dataArray[index][doc]);
}
}
}
}
Step3. Write data to Firestore
寫入資料的函式主要呼叫 Firestore 的 .set()
方法,除了帶寫入的資料當第一個參數之外,也可以帶第二個參數指定更新時是否要合併資料。merge
為 true 時,若待更新的資料還有寫入資料所沒有的屬性時,該屬性會被保留,僅更新寫入資料所包含的屬性欄位;若 merge
為 false,則更新後的資料將完全被寫入的資料所取代。
function startUpading (collectionName, doc, data){
return new Promise(resolve => {
docRef = db.collection(collectionName).doc(doc).set(data, {merge: true}).then(() => {
resolve('Data wrote!');
});
});
}
Cloud Functions 同步資料
儘管已經有掃票機制,但為了避免客戶的票券遺失或出問題,現場仍須一個即時查詢系統,依各種搜尋條件查詢客戶的票券資料。
我們的搜尋機制是透過 Algolia 進行,為此必須隨時監聽 Firestore 的資料變動,同步更新至 Algolia。這件事情交給 Cloud Functions for Firebase 處理。
Step1. Initialize
首先透過 npm 安裝 Firebase CLI,以便開發 Cloud Functions:
$ npm install -g firebase-tools
安裝後,登入自己的 Firebase 帳號,選擇開發專案:
$ firebase login
接著進到專案資料夾,執行初始化指令,便會產生 functions/ 目錄:
$ firebase init functions
Step2. Write Functions
目錄產生之後便可以開始撰寫代碼。打開 index.js,引入 firebase-functions 這個 SDK,它提供多種 firebase 的監聽事件,接著引入 firebase-admin,讓程式可以存取 firetore 的資料。
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
接著開始撰寫代碼:在這次活動中,一旦 firestore 有所變動,我們便將資料同步至 Algolia
exports.函式名稱 = functions.firestore.document('tickets/{ticketId}').onWrite((change, context) => {
if (change.after.exists) { // on create or update
// 將變更後的資料儲存至 Algolia
} else if (change.before.exists) { // on delete
// 將 Algolia 的資料刪除
}
});
Step3. Deploy functions
寫好代碼後,執行 deploy 指令,便可以進行部署,之後到 Firebase Console 確認函式有沒有正常執行便可。
$ firebase deploy --only functions
注意
若要刪除 Cloud Functions,Firebase CLI 並未提供指令,須把代碼給刪除或註解,重新部署上去,系統才會將它註銷。
設定 Cloud Functions 環境變數
在 Firebase 寫 Cloud Functions 時,偶爾會需要一些敏感資訊,例如某項服務的 id 跟 key 值,一般這些資訊會寫在類似 .env 的檔案,但在 Cloud Functions 沒有這些。
設定環境變數須透過指令完成:
$ firebase functions:config:set someservice.key="THE API KEY" someservice.id="THE CLIENT ID"
若忘記自己設過哪些變數,也須透過指令查詢:
$ firebase functions:config:get
至於在程式中如何取得變數,可參考以下範例:
const SERVICE_ID = functions.config().someservice.id;
const SERVICE_KEY = functions.config().someservice.key;
Algolia 全文本即時查詢
透過 Algolia 進行即時全文本查詢,我們可以用 InstantSearch.js 這個 Algolia 的 JS API Client 來呼叫查詢,並接收回傳回來的資料進行渲染。
Step1. Initialize
首先透過 npm 安裝:
$ npm install instantsearch.js
安裝後,開始初始化設定:
const instantsearch = require('instantsearch.js');
const search = instantsearch({
appId: 'Application ID',
apiKey: 'Search API Key',
indexName: 'IndexName'
});
search.start();
appId
、apiKey
、indexName
皆可以從 Algolia 後台的 dashboard 取得,要注意的是 apiKey
共有 Search / Write / Admin 三種,各有不同權限,若只是前端查詢,不需要寫入,更不需要刪除,那就只用權限最小的 Search API Key 即可。
Step2. Display Results
安裝跟設定完後,準備接收查詢結果,並進行渲染。InstantSearch.js 提供許多 widget 使用,其中 hits
表示資料被「擊中」(即被查詢到、符合查詢條件的意思),使用 .addWidget
加入:
<div id="hits">
<!-- Hits widget will appear here -->
</div>
search.addWidget(
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: 'No results',
item: '<em>Hit {{objectID}}</em>:{{{_highlightResult.name.value}}}'
}
})
);
search.start();
加入 hits widget 後,每次查詢都會依有沒有結果套上各自的 template,並被 append 進 container。從 Algolia 回傳的資料格式為 JSON,內容大致如下:
{
"results":[
{
"hits":[
{
"name": "Betty Jane Mccamey",
"company": "Vita Foods Inc.",
"email": "betty@mccamey.com",
"objectID": "6891Y2usk0",
"_highlightResult": {
"name": {"value": "Betty <b>Jan</b>e Mccamey", "matchLevel": "full"},
"company": {"value": "Vita Foods Inc.", "matchLevel": "none"},
"email": {"value": "betty@mccamey.com", "matchLevel": "none"}
}
}
],
"page": 0,
"nbHits": 1,
"nbPages": 1,
"hitsPerPage": 20,
"processingTimeMS": 1,
"query": "van",
"params": "query=van",
"index": "index1"
// ...
}
Step3. Add a SearchBox
查詢結果的頁面有了,接下來要提供搜尋框讓使用者輸入關鍵字查詢。同樣透過 .addWidget
加入:
<div id="search-box">
<!-- SearchBox widget will appear here -->
</div>
search.addWidget(
instantsearch.widgets.searchBox({
container: '#search-box',
placeholder: 'Search for products'
})
);
search.start();
設定好後,基本的即時搜尋功能便完成了;若想要有更多功能,可再陸續加入其他 widget。
VueJs 與 InstantSearch.js 的結合
InstantSearch.js 已經幫我們處理掉很多複雜流程,但要一一加入 widget 仍稍嫌麻煩,社群上有大神將 VueJS 與 InstantSearch.js 結合,只要 import 對方寫好的組件,填上自己的憑證資訊,也可以輕鬆完成功能。
import InstantSearch from 'vue-instantsearch';
Vue.use(InstantSearch);
<template>
<ais-index
app-id="Application ID"
api-key="Search API Key"
index-name="IndexName"
>
<ais-search-box></ais-search-box>
<ais-results>
<template slot-scope="{ result }">
<h2>
<ais-highlight :result="result" attribute-name="name">
</ais-highlight>
</h2>
</template>
</ais-results>
</ais-index>
</template>