【Firebase】好好用的Functions功能,自己做WebAPI

相信大家多少都有聽過Firebase是個JSON格式的資料庫吧?但是其實它還有一些其它的功能,比如說今天要介紹的這個Functoins的功能,就是拿來寫WebAPI用的,可以直接去讀取Firebase資料庫,相當的方便,它有支援iOS / Android / JavaScript / C++ / Unity的Framework - onCall(),但個人是要把它做成HTTP的形式 - onRequest(),為什麼呢?一方面是因為並不是每一個語言都有Firebase的支援,另一方面是比較容易看到輸出的結果。現在我們就來試試看吧。

安裝過程

Functions在哪裡?

  • 首先,我們先來看看Functions到底在哪裡呢?登入firebase console後,就可以看到了喲。

安裝Functions

node -v
npm install -g firebase-tools

登入CLI

firebase login

建立新專案

  • 利用CLI建立一個新專案,但記得一定要login,不然是沒有辦法產生的。
firebase init

功能實作

Helloworld

  • 主要的code都是放在index.js上面,它是functions的進入點。
// index.js
const functions = require('firebase-functions');

// helloWorld() => 印出中文字
exports.helloWorld = functions.https.onRequest((request, response) => {
    response.send("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
})
firebase deploy

部署程式

  • 將程式上傳到firebase上的時候,不一定要一次上傳全部,也可以一次上傳某幾個,比較要注意的是,如果使用「module.exports」建立多個function,而不是用「exports」建立單一個function的話,只有「module.exports」才有反應,就看看大家習慣哪一種了。
const functions = require('firebase-functions');

// helloWorld() => 印出中文字
exports.helloWorld = functions.https.onRequest((request, response) => {
    response.send("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
})

exports.helloWorld2 = functions.https.onRequest((request, response) => {
    response.send("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
})

// module.exports
module.exports = {
    helloWorld3: functions.https.onRequest((request, response) => {
        response.send("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
    }),
    helloWorld4: functions.https.onRequest((request, response) => {
        response.send("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
    })
}
firebase deploy                                             # 全部上傳
firebase deploy --only functions:<functionA>,<functionB>    # 上傳某幾個Function

匯入資料

  • 這個資料主要是來由於博客來的書籍資料來當成DEMO
{
  "eBook": {
    "9789861365404": {
      "name": "勇氣圖鑑:受挫、失敗都是成長的養分",
      "url": "https://www.books.com.tw/products/0010831766",
      "prices": [320, 253, 202],
      "order": 1
    },
    "9789578640856": {
      "name": "不插電 小學生基礎程式邏輯訓練繪本全套四冊",
      "url": "https://www.books.com.tw/products/0010818767",
      "prices": [1440, 1080],
      "order": 3
    },
    "9789869744560": {
      "name": "關於工作的9大謊言",
      "url": "https://www.books.com.tw/products/0010832091",
      "prices": [420, 332],
      "order": 2
    },
    "9789570853551": {
      "name": "全圖解!AI知識一本通:用故事讓你三小時輕鬆搞懂人工智慧",
      "url": "https://www.books.com.tw/products/0010830584",
      "prices": [280, 221],
      "order": 4
    },
    "9789863774938": {
      "name": "星座決定我愛你:用占星和塔羅幫你找到真命天子or女",
      "url": "https://www.books.com.tw/products/0010825318",
      "prices": [320, 253, 177],
      "order": 5
    }
  }
}

讀取資料 (Select)

根據路徑讀取資料

const functions = require('firebase-functions');
const admin = require("firebase-admin")

admin.initializeApp()

// MARK: - 輸出的Function名稱
module.exports = {
    helloWorld: functions.https.onRequest((request, response) => {
        let string = getHelloWorld()
        response.send(string)
    }),
    bookList: functions.https.onRequest((request, response) => {
        getBookList('eBook', (snapshot) => {
            response.send(snapshot)
        })
    })
}

// MARK: - 小工具
/// helloWorld() => 印出中文字
function getHelloWorld() {
    return ("哈囉你好嗎,衷心感謝,珍重再見,期待再相逢")
}

/// 取得全部的書籍資訊
function getBookList(path, callback) {

    let readType = 'value'

    admin.database().ref(path).once(readType, (snapshot) => {
        callback(snapshot)
    })
}

讀取排序後的資料

  • 這裡主要是利用「orderByChild()」來排序資料內的順序,此外要注意的是,因為是「排序過的」資料,所以要轉成「Array」才能得到正確的資料。
const functions = require('firebase-functions');
const admin = require("firebase-admin")

admin.initializeApp()

// MARK: - 輸出的Function名稱 (給Firebase Functions看的)
module.exports = {
    bookListOrderByChild: functions.https.onRequest((request, response) => {
        getBookListOrderByChild('eBook', request, (snapshot) => { response.send(snapshot) })
    }),
}

// MARK: - 主工具
/// 根據子Key排序後,取得全部的書籍資訊 => Array  (https://<host>/bookListOrderByChild?child=name)
function getBookListOrderByChild(path, request, callback) {
    
    _getBookListOrderByChild(path, request, (snapshot) => {
        
        let items = []

        snapshot.forEach(item => {
            items.push(item)
        })

        callback(items)
    })
}

// MARK: - 小工具
/// 根據子Key排序後,取得全部的書籍資訊
function _getBookListOrderByChild(path, request, callback) {

    let readType = 'value'
    let child = request.query.child

    admin.database().ref(path).orderByChild(child).once(readType, (snapshot) => {
        callback(snapshot)
    })
}

讀取範圍內的資料

  • 在這裡是將「排序過」的資料做範圍的截取,然後加了一欄叫做「order」用它來排序,它主要是利用「startAt() / endAt()」,可以去有效篩選部分的資料。在這裡大家可以在上一個例子中發現,排完序之後,key(ISBN)值就不見了,因為變成有順序的Array,而不是無順序的Object / Dictionary,所以在這裡我們再把key加進去裡面。
// MARK: - 輸出的Function名稱 (給Firebase Functions看的)
module.exports = {
    bookListOrderByKeyOfRange: functions.https.onRequest((request, response) => {
        getBookListOrderByKeyOfRange('eBook', request, (array) => { response.send(array) })
    }),
}

/// 根據子Key排序後,取得該範圍內的書籍資訊 => Array (https://<host>/bookListOrderByKeyOfRange?child=order&start=2&end=4)
function getBookListOrderByKeyOfRange(path, request, callback) {
    
    _getBookListOrderByKeyOfRange(path, request, (snapshot) => {
        let items = snapshotToArray(snapshot)
        callback(items)
    })
}

/// 根據子Key排序後,取得該範圍內的書籍資訊
function _getBookListOrderByKeyOfRange(path, request, callback) {

    let readType = 'value'
    let child = request.query.child
    let start = parseInt(request.query.start)
    let end = parseInt(request.query.end)

    admin.database().ref(path).orderByChild(child).startAt(start).endAt(end).once(readType, (snapshot) => {
        callback(snapshot)
    })
}

/// 將snapshot => Array
function snapshotToArray(snapshot) {
    
    let items = []
    let dict = snapshot.val()

    for (let key in dict) {
        let _item = dict[key]
        _item['identify'] = key // _item.identify = key
        items.push(_item)
    }

    return items
}

TypeScript的版本

  • 其實TypeScript也沒有想像中的難學,不過在JavaScript亂寫都會對的情形就不太會存在了。
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

admin.initializeApp()

const eventType: admin.database.EventType = 'value'

// MARK: - 輸出的Function名稱 (給Firebase Functions看的)
module.exports = {
    tsHelloWorld: helloWorld(),
    tsBookList: bookList(),
    tsBookListOrderByChild: bookListOrderByChild(),
    tsBookListOrderByKeyOfRange: bookListOrderByKeyOfRange(),
}

// MARK: - 主程式
/// 印出中文字 (https://<host>/tsHelloWorld)
function helloWorld(): functions.HttpsFunction {
    return functions.https.onRequest((request, response) => {
        response.send(getHelloWorld())
    })
}

/// 取得全部的書籍資訊 (https://<host>/tsBookList)
function bookList(): functions.HttpsFunction {
    return functions.https.onRequest((request, response) => {
        getBookList('eBook', (snapshot) => { response.send(snapshot) })
    })
}

/// 取得排序後的書籍資訊 (https://<host>/tsBookListOrderByChild?child=name)
function bookListOrderByChild() {
    return functions.https.onRequest((request, response) => {
        getBookListOrderByChild('eBook', request, (snapshot) => { response.send(snapshot) })
    })
}

/// 取得排序後在範圍內的書籍資訊 (https://<host>/tsBookListOrderByKeyOfRange?child=order&start=2&end=4)
function bookListOrderByKeyOfRange() {
    return functions.https.onRequest((request, response) => {
        getBookListOrderByKeyOfRange('eBook', request, (snapshot) => { response.send(snapshot) })
    })
}

// MARK: - 主工具
/// 取得中文字
function getHelloWorld(): Object {
    return ({ helloWorld: "哈囉你好嗎,衷心感謝,珍重再見,期待再相逢" })
}

/// 取得全部的書籍資訊
function getBookList(path: string, callback: (snapshot: admin.database.DataSnapshot) => void) {

    admin.database().ref(path).once(eventType).then((snapshot) => {
        callback(snapshot)
    }).catch((reason) =>
        console.log(reason)
    )
}

/// 根據子Key排序後,取得全部的書籍資訊
function getBookListOrderByChild(path: string, request: functions.https.Request, callback: (snapshot: any[]) => void) {
    
    _getBookListOrderByChild(path, request, (snapshot) => {
        callback(snapshotToArray(snapshot))
    })
}

/// 根據子Key排序後,取得該範圍內的書籍資訊
function getBookListOrderByKeyOfRange(path: string, request: functions.https.Request, callback: (snapshot: any[]) => void) {
    
    _getBookListOrderByKeyOfRange(path, request, (snapshot) => {
        callback(snapshotToArray(snapshot))
    })
}

// MARK: - 小工具
/// 根據子Key排序後,取得全部的書籍資訊
function _getBookListOrderByChild(path: string, request: functions.https.Request, callback: (snapshot: admin.database.DataSnapshot) => void) {
    
    const child = request.query.child as string

    admin.database().ref(path).orderByChild(child).once(eventType).then((snapshot) => {
        callback(snapshot)
    }).catch((error) =>
        console.log(error)
    )
}

/// 根據子Key排序後,取得該範圍內的書籍資訊
function _getBookListOrderByKeyOfRange(path: string, request: functions.https.Request, callback: (snapshot: admin.database.DataSnapshot) => void) {

    const parameter = {
        child: request.query.child as string,
        start: parseInt(request.query.start),
        end: parseInt(request.query.end),
    }

    admin.database().ref(path).orderByChild(parameter.child).startAt(parameter.start).endAt(parameter.end).once(eventType).then((snapshot) => {
        callback(snapshot)
    }).catch((error) =>
        console.log(error)
    )
}

/// 將snapshot => Array
function snapshotToArray(snapshot: admin.database.DataSnapshot): any[] {
    
    const items: any[] = []

    snapshot.forEach((item) => {
        const dict = item.val()
        dict.identify = item.key
        items.push(dict)
    })

    return items
}

console.log()

  • 大家有沒有發現到,萬一有錯的時候要怎麼debug呢?最基本的console.log()怎麼印不出來呢?其實可以利用「firebase functios」的記錄來看,或者是使用CLI的方式也是可以的。另外不知道有沒有發現,code裡面都沒有「分號;」結尾,這是為什麼呢?大家可以看看這篇文章就能理解了。
firebase functions:log                                  # 列出全部的log
firebase functions:log --only <function1>,<function2>   # 列出部分的log

網頁前端的console.log()

  • 到現在我才知道,console.log()有這麼多的功能,趕快記起來。
/* MARK: - 一般 */
console.log('我是中文字')                 // 列印中文字
console.log({ id: 1, name: 'Willam' })  // 列印Object
console.info('我是info')                 // 訊息列印
console.error('我是error')               // 錯誤列印
// console.clear()                      // 清除log
// console.trace()                      // 追踨軌跡

/* MARK: - 群組 */
console.group('Group')
    console.log('user: William')

    console.group('Group A')
        console.log('book: JS Book')
        console.log('ISBN: 978777555777')
    console.groupEnd()

    console.group('Group B')
        console.log('book: JS Book')
        console.log('ISBN: 978777555777')
    console.groupEnd()
console.groupEnd()

/* MARK: - 表格 */
console.table({ id: 1, name: 'Willam' })

/* MARK: - 時間差 */
console.time()
for (let index = 0; index < 100000; index++) {}
console.timeEnd()

範例程式碼下載

後記

  • Firebase Functions是一個做API滿友好的工具,又結合了Firebase Database,加上它也能夠有推播的功能、Hosting的網頁功能,等於是前後台全包了,Google真的是太可怕了呀。其實Firebase是用買的呀。另外呢,還發現了一個好用的字型 - fira code,大家也可以試用看看,尤其在寫JavaScript特別的明顯好用呢。此外,真心覺得JavaScript真的好寫難debug,寫錯了要到Run的時候才會發現,看來也許要用TypeScript的版本來寫寫看了,最近滿流行的,真的覺得強型別 - Strong Type的語言還是比較適合我呀。