寫入資料至 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();

appIdapiKeyindexName 皆可以從 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"
    // ...
}

查詢結果的頁面有了,接下來要提供搜尋框讓使用者輸入關鍵字查詢。同樣透過 .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>
Last Updated: 8/9/2019, 6:50:41 PM