iT邦幫忙

1

《賴田捕手:番外篇》第 38 天:用 Netlify Functions 佈署 Line Bot

  • 分享至 

  • xImage
  •  

《賴田捕手:番外篇》第 38 天:用 Netlify Functions 佈署 Line Bot

從此之後,你不會再碰到 AWS Lambda ,他已經展開自己生命中的新篇章了。我們讓 AWS Lambda 在這邊接受最完善的照顧,享有最充裕的資源,用愛與包容的方式完成 AWS Lambda 各式各樣的設定。

「我想要 AWS Lambda。」他嘶吼著:「我想要親手完成那些設定!」

你確定你想嗎?

~節錄自《而 Netlify 的回音蕩漾》

  經過了 2 篇文章的深入淺出 (?) 的討論,相信大家對於 Netlify 這個提供雲端運算的公司是越來越熟悉了,在學會使用 Netlify 各種佈署方式之後,我們也將要迎來這一個系列文章的重頭戲:佈署 Line Bot 囉。前面的介紹當中,我們都是用 Netlify 來佈署前端網頁 / 服務。然而,要讓 Line Bot 順利運作,我們需要的,卻是一個位於後端的 Webhook 伺服器 (詳見第 36 天)。Netlify 有提供能夠架設在後端的伺服器服務嗎?有的,Netlify 所給出的答案就是 Netlify Functions。

介紹 Netlify Functions

  要說 Netlify Functions 是屬於位於後端的伺服器服務,似乎不太精確。正確來說,Netlify Functions 是屬於近年來相當火紅的後端服務型態:無伺服器運算服務 (Serveless Framework),或者說,就是所謂的函式即服務 (Function as a Service, FaaS)。同樣屬於後端的服務,伺服器服務跟這種新崛起的函式即服務,最大的差別就是資源的利用率以及我們程式設計師收到的帳單上面。絕大多數的情況下,和傳統的伺服器服務相比,函式即服務可以說是輕薄短小且免費。
  而 Netlify 搭著這一陣函式即服務的潮流,順勢推出了以 AWS Lambda 為主體的 Netlify Functions。或許有人會好奇:什麼是 AWS Lambda?AWS Lambda 是亞馬遜雲端服務 (AWS) 所推出的函式即服務功能。那或許大家又會接著問下去:為什麼不用 AWS Lambda 就好,反而要特地跑來 Netlify,用 Netlify 包裝起來的 Netlify Functions 呢?AWS Lambda 首先是 AWS 所提出的功能,接著又被 Netlify 包裝起來,變成了 Netlify Functions。能不能給我翻譯翻譯,什麼叫做包裝?
  包裝就是,從此以後,我們不用再碰 AWS 過於繁瑣複雜的設定流程和收費方式,只要用 Netlify 的規則來撰寫 / 發佈函式就行!
  當然,也不是說 AWS 自身所提供的架構不好。AWS 提供了一個富有彈性,可根據需求進行擴充的完善架構。AWS Lambda 具有可調整函式使用資源 (記憶體大小、執行時間) 的自由,同時也可再連接資料庫,設定定時事件等等。有捨就有得,我們捨去了 AWS Lambda 的彈性,換得 Netlify Functions 的簡潔。

【註】
很可惜的一點是,雖然 AWS Lambda 提供以 Python 撰寫程式碼的選項,但 Netlify Functions 目前只支援 JavaScript、TypeScript、以及 Go 這三種語言。也因此《賴田捕手:番外篇》當中是以 JavaScript 來寫 LineBot。

  好的,說了這麼多,讓我們試著來佈署 Netlify Functions 吧。

建立 Netlify Functions

  這邊就讓我們試著用功能完善,親切易懂的 Netlify CLI 來幫我們建立 Netlify Functions 吧!

1. 準備 Netlify CLI

  安裝 Netlify CLI:

npm install netlify-cli -g

  若各位的電腦當中,還沒安裝好 Netlify CLI,可以透過上述指令進行全域安裝。安裝完成後,可以透過netlify --version來查看目前版本:

netlify --version
netlify-cli/3.37.17 win32-x64 node-v14.15.5

  我的 Netlify CLI 版本號是 3.37.17。

2. 準備netlify.toml

  還記得 Netlify 專屬的參數設定檔netlify.toml嗎?假設我們目前的專案資料夾是netlify-functions-demo,為了要告訴 Netlify 我們想要佈署成無伺服器運算的檔案放在哪裡,在專案資料夾當中的netlify.toml需要這麼寫:

  • netlify-functions-demo/netlify.toml
[build]
  functions="my_functions"

  這樣寫的意思是,我們要作為 Netlify Functions 佈署成無伺服器服務的函式,就放在my_functions這個資料夾當中。
  整個專案資料夾的架構看起來如下:

netlify-functions-demo
└───netlify.toml

  好的,前置作業都做完了。看起來很空虛是嗎?沒問題,Netlify CLI 要來幫我們變魔術了。

3. 初始化 Netlify Functions

  有了netlify.toml檔案,Netlify 就知道該去哪邊找我們寫下的 Netlify Functions 並進一步去佈署它們了。不過等等,我們根本還沒寫下任何 Netlify Functions 啊?試著輸入指令netlify functions:create 你-Functions-的名字

netlify functions:create 你-Functions-的名字
◈ functions directory netlify-functions-demo\my_functions does not exist yet, creating it...
◈ functions directory netlify-functions-demo\my_functions created

  看到了嗎?在讀了netlify.toml檔案之後,Netlify CLI 很警覺的發現我們還沒有netlify-functions-demo/my_functions這個資料夾,於是 Netlify CLI 很貼心的幫我們創造了一個。

  接著,Netlify CLI 會繼續詢問我們需要的 Netlify Functions 是哪一種。根據我們的回答,Netlify CLI 會進一步幫我們創造出適合的初始化樣板。可能的選擇包括有:

  • 陽春版的 Async/Await 函式
  • GraphQL 函式
  • 加上了 REST API 的 GraphQL 函式
  • 用了node-fetch的函式
  • 創建 Netlify Identity 的函式
  • 操作 Fauna DB 資料庫的函式

  當然,Netlify CLI 所提供的選擇可能因為版本號不同而相異。不過,顯而易見,陽春版不管是哪一個版本號都應該要有的,對我們寫 Line Bot 來說也挺足夠的,因此選陽春版就可以了。

netlify functions:create 你-Functions-的名字
◈ functions directory netlify-functions-demo\my_functions does not exist yet, creating it...
◈ functions directory netlify-functions-demo\my_functions created
? Pick a template js-hello-world
◈ Creating function 你-Functions-的名字
◈ Created netlify-functions-demo\my_functions\你-Functions-的名字\你-Functions-的名字.js

現在,我們的專案資料夾看起來會像這樣:

netlify-functions-demo
├───netlify.toml
└───my_functions
    └───你-Functions-的名字
        └───你-Functions-的名字.js

4. 陽春版 Netlify Functions

  看一下 Netlify CLI 幫我們創造的陽春版 Netlify Functions 長什麼樣子吧:

【註】
此樣板亦可能因 Netlify CLI 版本號不同而有所差別。

  • netlify-functions-demo/my_functions/你-Functions-的名字/你-Functions-的名字.js
// Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method
const handler = async (event) => {
  try {
    const subject = event.queryStringParameters.name || 'World'
    return {
      statusCode: 200,
      body: JSON.stringify({ message: `Hello ${subject}` }),
      // // more keys you can return:
      // headers: { "headerName": "headerValue", ... },
      // isBase64Encoded: true,
    }
  } catch (error) {
    return { statusCode: 500, body: error.toString() }
  }
}

module.exports = { handler }

  對於函式即服務不太熟悉的朋友,可能不知道這些程式碼是在做什麼,又是如何取代傳統伺服器的服務。因此我稍微說明一下。一般所謂的伺服器,最主要的工作就是處理前端使用者發送過來各式各樣的請求。而無伺服器運算服務,就是改以函式取代伺服器,使用者發送過來的請求 (request) 就等同於準備輸入函式的變數,而使用者應該要拿到的回應 (response),就等同於函式根據變數內容運算的結果。因此,當使用者向某個網址發送請求時,該請求會被轉為變數,呼叫代表該網址的函式,函式根據變數內容運算,運算結果轉為回應的方式傳回給使用者。
  注意到我這邊說了個「轉為」,因為跟傳統的伺服器還是有所不同,使用者發送過來的請求被「轉為」事件 (event),而事件才是真正輸入函式的變數。此外,由於 Netlify Functions 的真實身分,其實是 AWS Lambda (其實是還要再加上 Amazon API Gateway,不過這邊我們就不說的這麼複雜),所以這份程式碼的撰寫方式,要遵守 AWS Lambda 的規範。規範有哪些呢?

  • AWS Lambda 預設函式名稱及變數
      在 AWS Lambda 當中,使用者發出請求,來到 AWS Lambda 後,會觸發 (呼叫) 的函式,其預設名稱是handler。所以不要動到代表函式的變數名稱handler。此外,該函式接收三個變數,詳細大家可以研究 AWS Lambda 的官方文件。但大多數的情況下,最重要的只有第一個變數,也就是包含了使用者請求資料的event這個變數。

  • AWS Lambda 事件內容
      那麼,包含了使用者請求資料的event這個變數,實際上有哪些資料呢➀:

{
    "path": "Path parameter",
    "httpMethod": "Incoming request’s method name"
    "headers": {Incoming request headers}
    "queryStringParameters": {query string parameters }
    "body": "A JSON string of the request payload."
    "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode"
}
    1. path:使用者請求的原始路徑
    1. httpMethod:使用者請求的方法 (GET, POST 等等)
    1. headers:使用者請求的標頭
    1. queryStringParameters:使用者請求時的查詢字串
    1. body:使用者請求的主要內容
    1. isBase64Encoded:請求內容是否採用 Base64 編碼。

  了解了這些背景之後,我們再回頭來看一下陽春版 Netlify Functions 的預設內容:

const handler = async (event) => {
  try {
    const subject = event.queryStringParameters.name || 'World'
    return {
      statusCode: 200,
      body: JSON.stringify({ message: `Hello ${subject}` }),
      // // more keys you can return:
      // headers: { "headerName": "headerValue", ... },
      // isBase64Encoded: true,
    }
  } catch (error) {
    return { statusCode: 500, body: error.toString() }
  }
}

module.exports = { handler }
  • 第一行:const handler = async (event) => {
    建立 Async/Await 函式handler,並接收代表事件的event這個函式變數。

  • 第三行:const subject = event.queryStringParameters.name || 'World'
    利用event.queryStringParameters來取得查詢字串。若使用者請求時,有查詢字串鍵 (key) 為name的值,將該值賦予變數subject,若無,則將'World'賦予變數subject

  • 第五行:statusCode: 200,
    若一切順利,傳回代表成功的 HTTP 狀態碼200

  • 第六行:body: JSON.stringify({message: Hello ${subject}}),
    並且用 JSON 當作回應的內容。

  • 第十二行:return { statusCode: 500, body: error.toString() }
    若出了任何狀況,傳回代表伺服器出錯的 HTTP 狀態碼500

5. 佈署第一個 Netlify Functions

  好啦,都理解之後,我們就可以用 Netlify Dev 來佈署看看。還記得要怎麼做嗎?

netlify dev

  用netlify dev在本機端模擬 Netlify 佈署結果。

https://ithelp.ithome.com.tw/upload/images/20210628/20120178EItAI7f5Ca.png
圖一、用netlify dev在本機端模擬 Netlify 佈署結果

  咦,失敗了嗎?當然不是。我們根本就沒有做什麼可以佈署在前端的網頁,Not Found 是再正常不過的結果了。但我們至少有做一個放在後端的無伺服器運算函式,我們該向哪一個路徑發出請求,才能呼叫 (觸發) 這個函式呢?
  答案是/.netlify/functions/你-Functions-的名字

https://ithelp.ithome.com.tw/upload/images/20210628/201201782nxiEd1u66.png
圖二、向/.netlify/functions/你-Functions-的名字發送請求。在這個示範當中,我幫我的 Netlify Functions 取了一個好聽的的名字helloWorld

  這什麼奇怪的路徑?沒搞錯吧。還記的我們陽春版 Netlify Functions 有一個查詢路徑的相關把戲,讓我們用查詢字串試試。/.netlify/functions/你-Functions-的名字?name=ithomemhjao

https://ithelp.ithome.com.tw/upload/images/20210628/20120178e8e90lqgjX.png
圖三、向/.netlify/functions/你-Functions-的名字?name=你想試試看的-value發送請求

  原來是真的!我們的無伺服器服務 Netlify Functions 就放在/.netlify/functions/你-Functions-的名字這裡了。

6. 修飾一下 Netlify Functions

  想必不少人覺得/.netlify/functions/你-Functions-的名字這是什麼獵奇的路徑,居然要向這種路徑發出請求才能找到我們佈署的 Netlify Functions。當然,這可能會方便 Netlify 進行管理以及佈署,不過嘛,這樣子的路徑不符合大家的審美觀嘛。
  沒有問題。還記得 Netlify 所推出路由相關的服務 Netlify Redirects 嗎。可以說 Netlify Functions 就是為此而生的。讓我們用netlify.toml檔案來示範:

  • netlify-functions-demo/netlify.toml
[build]
  functions="my_functions"

[[redirects]]
  from = "/callback"
  to = "/.netlify/functions/你-Function-的名字"
  status = 200

https://ithelp.ithome.com.tw/upload/images/20210628/201201786kPsiow6ll.png
圖四、向/callback?name=你想試試看的-value發送請求

  這樣是不是比較漂亮呢?

用 Netlify Functions 架設 Line Bot

  終於來到這裡了。既然我們連 Netlify Functions 的運作方式都清楚了,那麼就來寫一個可以放在 Netlify Functions 上面的 Line Bot 吧!

  • netlify-functions-demo/my_functions/lineBotWebhook/lineBotWebhook.js
const line = require('@line/bot-sdk')

const handler = async (event) => {
  // 取得環境變數
  const clientConfig = {
    channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
    channelSecret: process.env.CHANNEL_SECRET,
  };
  // 用 CHANNEL_ACCESS_TOKEN 和 CHANNEL_SECRET 初始化 Line Bot
  const client = new line.Client(clientConfig);

  // 為 Webhook 驗證做準備
  const signature = event.headers['x-line-signature'];
  const body = event.body;

  // Line Bot 運作邏輯
  const handleEvent = async (event) => {
    if (event.type !== 'message' || event.message.type !== 'text') {
      return Promise.resolve(null)
    }
    const { replyToken } = event;
    const { text } = event.message;

    // Create a new message.
    const response = {
      type: 'text',
      text: `而 Netlify 的回音盪漾著:${text}~`,
    };
    await client.replyMessage(replyToken, response)
  }

  try {
    // 用 CHANNEL_SECRET 來驗證 Line Bot 身分
    if (!line.validateSignature(body, clientConfig.channelSecret, signature)) {
      throw new line.exceptions.SignatureValidationFailed("signature validation failed", signature)
    }

    // 將 JSON 轉為 JavaScript 物件
    const objBody = JSON.parse(body);
    // 將觸發事件交給 Line Bot 做處理
    await Promise.all(objBody.events.map(handleEvent))

    return {
      statusCode: 200,
      body: JSON.stringify({ message: "Hello from Netlify" }),
    }
  } catch (error) {
    console.log(error)
    return { statusCode: 500, body: error.toString() }
  }
}

module.exports = { handler }
  • 第一行: const line = require('@line/bot-sdk')
    引入需要用的模組line

  • 第三行:const clientConfig = {
    準備好 Line Bot 的身分證,包括CHANNEL_ACCESS_TOKEN以及CHANNEL_SECRET。這兩個變數,通常不會用明碼的方式直接寫在程式碼當中。後端服務的好處是,像這種不想寫在程式碼當中的資料,可以考慮放在環境變數 (Environment variables) 當中。Netlify 當然也有提供儲存環境變數的方法。只要來到各位的 Netlify 控制面板,【Deploy】分頁,【Deploy settings】,【Build & deploy】->【Environment】,如圖五圖六,就可以輸入想要用的環境變數了。

https://ithelp.ithome.com.tw/upload/images/20210628/20120178ZAD78F610t.png
圖五、【Build & deploy】->【Environment】

https://ithelp.ithome.com.tw/upload/images/20210628/20120178wI3BC3pqTX.png
圖六、Environment variables

  • 第七行:const client = new line.Client(clientConfig);
    用 CHANNEL_ACCESS_TOKEN 和 CHANNEL_SECRET 初始化 Line Bot。

  • 第八行:const signature = event.headers['x-line-signature'];
    event.headers當中拿出 Line 請求所特有的標頭'x-line-signature'。這個標頭是稍後要跟CHANNEL_SECRET做交互對照的,既是 Line Bot 要驗明正身,同時也要確認請求來自 Line 官方平台。

  • 第十行:const handleEvent = async (event) => {
    我們 Line Bot 的運作邏輯,這邊就不詳細說明了。

  • 第二十三行:if (!line.validateSignature(body, clientConfig.channelSecret, signature)) {
    前面提過,我們除了需要先確認 Line Bot 的身分之外,也要確定請求是從 Line 官方平台發送而來。而驗證的方法,就是利用validateSignature()這一個函式。該函式接收 3 個變數,包括bodyCHANNEL_SECRET、以及signature。大致上的運作方式是,body經過CHANNEL_SECRET的編碼之後,需要等於 Line 官方送過來的signature

  • 第二十六行:const objBody = JSON.parse(body);
    通過身分認證後,就將送過來的事件內容交給 Line Bot 處理。不過,首先要將 JSON 轉成 JavaScript 可以處理的物件 (Object),大致上看起來會像下面這樣,而詳細內容可以參考 Line 官方文件➁。

{
  "destination":"代表 Line Bot id 的一串文字",
  "events":[
    {
      "type":"message",
      "message":{"type":"text","id":"代表訊息 id 的一串文字","text":"使用者傳來的文字內容"},
      "timestamp":1624805388143,
      "source":{"type":"user","userId":"代表使用者 id 的一串文字"},
      "replyToken":"0477f7971ebc4b1a998e196a9323a9f1",
      "mode":"active"
    }
  ]
}
  • 第二十七行:await Promise.all(objBody.events.map(handleEvent))
    將事件內容交給 Line Bot 做處理。至於會怎麼處理呢?這就看我們剛才的handleEvent這個函式怎麼寫了。

  再重新檢查一下我們的netlify.toml檔案:

  • netlify-functions-demo/netlify.toml
[build]
  functions="my_functions"

[[redirects]]
  from = "/callback"
  to = "/.netlify/functions/lineBotWebhook "
  status = 200

  因為我們需要安裝line套件,所以記得要寫好package.json檔案:

  • netlify-functions-demo/package.json
{
  "name": "netlify-line-bot",
  "version": "1.0.0",
  "description": "",
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@line/bot-sdk": "^7.3.0"
  }
}
  • 第九行:"@line/bot-sdk": "^7.3.0"
    其他東西都不太重要,重點是,請幫我們安裝好 Line 的 npm 套件。

  這是一個非常簡單的 Line Bot。整個檔案架構看起來如下:

netlify-functions-demo
├───netlify.toml
├───package.json
└───my_functions
    └───lineBotWebhook
        └───lineBotWebhook.js

  這樣我們就可以將這個專案資料夾佈署到 Netlify 上了。同時,也要在 Line Developers 的 Line Bot 設定面板上調整 Webhook URL,如圖七。詳細要注意哪些,可以參考第 31 天的內容。

https://ithelp.ithome.com.tw/upload/images/20210628/2012017844abXSPZ9u.png
圖七、設定好 Webhook URL

https://ithelp.ithome.com.tw/upload/images/20210628/20120178mtepyAJEjq.png
圖八、而 Netlify 的回音盪漾著:唷~

  這一篇文章當中,我們了解到如何撰寫並佈署無伺服器服務 Netlify Functions,進一步透過無伺服器服務來架設我們的 Line Bot Webhook Server。文章當中提供了陽春版 Line Bot 的程式碼。剩下來的 2 篇文章,我們就要根據這個陽春版的 Line Bot,進行擴充和改裝,希望最後能夠完成我們的名片產生器 Line Bot。

參考資料

➀ AWS Lambda event 官方文件
➁ Line Webhook Event Object 官方文件


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言