iT邦幫忙

0

《賴田捕手:番外篇》第 39 天:探索 Netlify Functions 的暫存空間

《賴田捕手:番外篇》第 39 天:探索 Netlify Functions 的暫存空間

第 38 天的文章當中,我們透過 Netlify 所提供的後端服務 Netlify Functions,成功架設了一個最陽春的 Line Bot。最陽春的 Line Bot 基本上只有兩種功能:

  1. 通過 Webhook 認證
  2. 接收使用的傳送的文字訊息,照實傳回給使用者

雖然只有這兩樣基本中的基本,但基本上這告訴我們佈署在 Netlify Functions 上的 Line Bot 已經和 Line 官方平台成功建立起連結,接下來我們就可以按照心目中的藍圖,一步一步打造具有不同功能的 Line Bot 了。

陽春版 Line Bot

一開始還是先來複習一下我們的陽春版 Line Bot:

// 引入所需的 Line 模組
const line = require('@line/bot-sdk')

// Netlify Functions 的起點
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 Bot 也不例外。一開始要寫 Line Bot,最困難的莫過於了解各式各樣 Line Bot 與 Line 官方平台互動時所傳遞的請求和回應。幸好 Line 官方提供了相關的 Node.js 模組:line-bot-sdk-nodejs➀。該模組將這些請求和回應包裝了起來,並提供了各式各樣的函式,幫助我們更容易的實作出想要的功能。所以寫 Line Bot 第一件事情,就是引入這個模組。

  • 第四行:const handler = async (event) => {
    而寫 Netlify Functions,第一件事就是要實作出名稱為handler的這個函式。當使用者向相對應的路由作出請求時,該請求會被包裝成觸發事件➁,而觸發事件會被當作handler這個函式的第一個變數 (event),讓函式運作起來。

  • 第六行:const clientConfig = {
    從環境變數當中,取得代表 Line Bot 的個人資料,包括CHANNEL_ACCESS_TOKENCHANNEL_SECRET

  • 第十一行:const client = new line.Client(clientConfig);
    利用 Line Bot 個人資料來初始化我們的 Line Bot。

  • 第十三行:const signature = event.headers['x-line-signature'];
    當 Line 官方平台向我們的 Line Bot 送出請求時,會帶有一個特殊的檔案標頭 (headers) x-line-signature。這個檔案標頭是等一下要來驗證 Line Bot 身分所需要的資料。因此我們先在這邊把檔案標頭拿出來。

  • 第十四行:const body = event.body;
    同樣的,Line 官方平台送過來的請求內容 (body) 也很重要,裡面存放了使用者向 Line Bot 發送訊息的相關資料。因此我們先在這邊把內容拿出來。

  • 第十六行:const handleEvent = async (event) => {
    這一個函式handleEvent則是我們自己定義的 Line Bot 運作邏輯。使用者傳來怎麼樣的訊息,而 Line Bot 又該如何做出相對應的回覆,所有的運作邏輯都是寫在這一個函式當中。

  • 第三十一行:if (!line.validateSignature(body, clientConfig.channelSecret, signature)) {
    這邊,我們利用line-bot-sdk-nodejs所提供的函式validateSignature來驗證 Line Bot 的身分。詳細的運作方式可以參考 Line 官方文件➂對於 Webhook 的說明。不過大致上來說,利用 HMAC-SHA256 這一套編碼機制,我們的 Line Bot 將自身的CHANNEL_SECRET當成編碼金鑰 (secret),對請求內容作編碼,得到的結果應該要跟 Line 官方送過來的x-line-signature標頭相符。如此就完成身分認證的動作。

  • 第三十五行:const objBody = JSON.parse(body);
    完成身分認證之後,就可以開始處理使用者發送的訊息了。首先將帶有使用者訊息相關資料的body從 JSON 轉為 JavaScript 物件 (Object)。

  • 第三十七行:await Promise.all(objBody.events.map(handleEvent))
    該物件的內容大約如下:

{
  "destination":"代表 Line Bot id 的一串文字",
  "events":[
    {
      "type":"message",
      "message":{"type":"text","id":"代表訊息 id 的一串文字","text":"使用者傳來的文字內容"},
      "timestamp":1624805388143,
      "source":{"type":"user","userId":"代表使用者 id 的一串文字"},
      "replyToken":"0477f7971ebc4b1a998e196a9323a9f1",
      "mode":"active"
    }
  ]
}

詳細說明可以參考 Line 官方文件➃對於 Webhook Event Objects 的介紹。這邊我們只需要注意到events這個陣列裡的內容,就代表了使用者每一次發送過來的訊息。我們的 Line Bot 要根據這些訊息做出相對應的回應,也就是要透過前面提到的handleEvent來細心處理這些使用者訊息。

https://ithelp.ithome.com.tw/upload/images/20210704/20120178uL7kXotGzV.png
圖一、陽春版 Line Bot

查資料 Line Bot

各項初始化的工作以及驗證的工作都明瞭之後,唯一需要關心的,就是我們 Line Bot 的運作邏輯了。不過在大刀闊斧的為 Line Bot 加上五花八門的新功能之前,先看一下 Netlify Functions 給我們的資源以及支援。
我們在第 38 天的內容當中提過,Netlify Functions 其實背後真正在運作的是 AWS Lambda (加上 Amazon API Gateway,不過這邊就不解釋這麼多)。可以把 Netlify Functions 看作 AWS Lambda 的簡化版本,我們可以更專心在程式碼本身,而不需要處理 AWS 複雜的服務設定。換句話說,Netlify 為我們提供了一個簡便的橋樑 Netlify Functions,或者說是代理,讓我們可以更輕鬆地跟 AWS Lambda 溝通。
然而這個代理並不能夠給我們 AWS Lambda 的所有資源,而是有些許限制。這些限制會不會影響我們寫一個好用的 Line Bot 呢?大多數的情況是不會的。怎麼說呢?看看我們的 Netlify Functions 得到了哪些限制:

  • us-east-1 AWS Lambda region
  • 1024MB of memory
  • 10 second execution limit for synchronous serverless functions

第一個限制是,我們的 Line Bot 永遠是佈署在 AWS 美東第一區的伺服器上。
第二個限制是,我們的 Line Bot 永遠只能使用 1024MB 的記憶體來運作。
第三個限制是,我們的 Line Bot 永遠只能有最多 10 秒的運作時間。

第一個限制,造成資料往返上時間間隔較長,不過也許就是幾百毫秒內的差別,體感上應該還能接受。第二個限制,造成 Line Bot 運作速度被記憶體大小所控制。實際上影響會有多嚴重呢?我們等下來試試。最後一個限制,其實如果我們想要做一個具有良好使用者體驗的 Line Bot,10 秒內對使用者的訊息做出回應應該是必要的吧,所以這應該也不算是限制 (咦?)。事實上,Line 本身的設計,也是希望我們能夠盡快回覆使用者的。如果我們的 Line Bot 與使用者互動時,用的是回覆 (reply message) 而不是推送 (push message) 的話➄,會需要一個replyToken。這個replyToken會從使用者發送過來的訊息而產生,並且在 30 秒內失效。意思是說,Line 本身也在敦促我們去設計一個能夠盡快完成與使用者互動的 Line Bot。
恩,那用推送訊息的話會如何呢?答案是我們的帳單會變得不堪入目。Line 提供給每一個 Line Bot 每個月 500 次推送訊息的免費用量➅。再多,就要錢了。
好了,說了這麼多,那麼到底 Netlify Functions 給出的這些限制,會如何影響我們的 Line Bot 呢?我們試著寫一個網頁資料擷取的 Line Bot,看看運作起來會如何吧!
整個檔案架構看起來會像這樣:

netlify-line-bot-demo
├───netlify.toml
├───package.json
└───my_functions
    └───lineBotWebhook
        ├───lineBotWebhook.js
        └───custom_module
            └───qaisTalk.js
  • netlify-line-bot-demo/netlify.toml
[build]
  functions="my_functions"

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

在 Netlify 的專屬設定檔netlify.toml裡面,我們利用functions="my_functions"明確指出 Netlify Functions 的資料夾。同時加了一個 Netlify Redirects 的規則。

  • netlify-line-bot-demo/package.json
{
  "name": "netlify-line-bot-demo",
  "version": "1.0.0",
  "description": "netlify-line-bot-demo",
  "keywords": [
    "netlify",
    "line bot"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@line/bot-sdk": "^7.3.0",
    "cheerio": "^1.0.0-rc.10",
    "node-fetch": "^2.6.1"
  }
}

package.json當中,我們也明確寫下了需要用到的模組,包括@line/bot-sdknode-fetch、以及cheerio

  • netlify-line-bot-demo/my_functions/lineBotWebhook/lineBotWebhook.js
// 引入所需模組
const line = require('@line/bot-sdk');
const qaisTalk = require('./custom_module/qaisTalk.js');

/* 省略了 Line Bot 初始化的相關動作 */  

  const handleEvent = async (event) => {

    const { replyToken } = event;

    if (event.type !== 'message' || event.message.type !== 'text') {
      const response = qaisTalk.defaultTalk();
      await client.replyMessage(replyToken, response);
    } else {
      const response = await qaisTalk.dictTalk(event);
      await client.replyMessage(replyToken, response);
    }
  }

/* 省略了 Webhook 驗證的相關動作 */  

在這個lineBotWebhook.js檔案當中,我們把整個程式拆成兩個部分,第一部分是陽春版 Line Bot 必備的基本動作,包括 Line Bot 初始化和 Webhook 的驗證。由於並不會有任何改動,所以這邊我就省略不寫。第二部分則是資料擷取 Line Bot 的運作邏輯,也就是handleEvent這個函式內容。
我們把運作邏輯寫成:如果使用者傳來的不是文字訊息的話,那就讓 Line Bot 回覆一個預設的訊息 (罐頭訊息)。如果使用者傳來的是文字訊息的話,那就試著根據這個文字訊息查找相關資料。什麼樣的相關資料呢?答案是劍橋辭典 Cambridge Dictionary➆。使用者輸入英文詞彙,Line Bot 則上劍橋辭典找出詞彙的詞性、中英文定義等相關資料,完成後回傳給使用者。

而我們把相關的運作邏輯放在另一個檔案裡:

  • netlify-line-bot-demo/my_functions/lineBotWebhook/custom_module/qaisTalk.js
// 引入所需模組
const fetch = require('node-fetch');
const cheerio = require('cheerio');

// 罐頭訊息
const defaultTalk = () => {
  const response = {
    type: 'text',
    text: '而 Netlify 的回音盪漾~',
  };
  return response;
}

// 上劍橋辭典查詢指定詞彙
const dictTalk = async (event) => {
  // 用 node-fetch 抓取網頁原始檔
  const targetWord = event.message.text.trim().toLowerCase();
  const url = `https://dictionary.cambridge.org/zht/%E8%A9%9E%E5%85%B8/%E8%8B%B1%E8%AA%9E-%E6%BC%A2%E8%AA%9E-%E7%B9%81%E9%AB%94/${targetWord}`;
  const res = await fetch(url);
  const text = await res.text();

  // 用 cheerio 將原始檔解析為 DOM
  const $ = cheerio.load(text);

  // 尋找隱藏在網頁當中詞彙的相關資料
  const posHeader = $(".pos-header");
  const posBody = $(".pos-body");

  const definitionArr = posHeader.map((idx, el) => ({
    title: $(el).find(".di-title").text(),
    pos: $(el).find(".pos").text(),
    def: [...$(posBody[idx]).find(".def-block").map((_, d) => $(d).find(".def").text())],
    tran: [...$(posBody[idx]).find(".def-block").map((_, d) => $(d).find("span.trans:first-child").text())]
  }))

  const objToText = (obj) => {
    const defText = obj.def.map((x, i) => `${i + 1}. ${x}\n# ${obj.tran[i]}`);
    return `[ ${obj.title} ]\n( ${obj.pos} )\n${defText.join("\n")}`
  }

  const replyText = [...definitionArr].map(objToText).join("\n\n");
  const response = { 'type': 'text', 'text': replyText }

  return response
}

module.exports = { defaultTalk, dictTalk };

因為重點是 Netlify Functions 的佈署,所以這邊資料擷取的相關程式碼我也就不多做解釋。大致上來說,利用 node-fetch 來抓取網頁原始https://ithelp.ithome.com.tw/upload/images/20210707/20120178EZe5d2CzCz.png檔,並利用 cheerio 將原始檔解析為 DOM 結構,並尋找隱藏在網頁當中詞彙的相關資料。

完成之後佈署到 Netlify 上,看起來怎麼樣呢?這樣一個資料擷取的 Line Bot,在 Netlify Functions 記憶體 1024 MB、運作時間 10s 的限制下,恩,Excellent!

https://ithelp.ithome.com.tw/upload/images/20210704/2012017867oyswlY3Y.png
圖二、Excellent

光是看這樣一張圖片可能不夠嚴謹,我們可是學科學的人吶 (?),夠不夠 Excellent 要用數字來說話。不過,有什麼樣的數字可以讓我們來參考呢?
AWS Lambda 有提供函式的工作日誌,將每一筆觸發事件記錄下來供我們查閱。紀錄內容除了有程式運作時利用console.log()記錄下來的訊息,程式出錯時的錯誤訊息之外,還可以看到每一次觸發事件中函式的運行時間和記憶體用量。而這正是我們可以參考的。很棒的是,Netlify Functions 也保留下了這個功能。因此我們在 Netlify Functions 上,就可以查到函式的運行時間和記憶體用量,換句話說,即是使用者每次傳送訊息過來, Line Bot 回覆所需要的時間跟記憶體用量。
登入 Netlify,來到工作面板,選擇代表該 Line Bot 的 Site,切換到 Functions 分頁,接著在下面找到代表 Line Bot 的 Netlify Functions,如圖三。點下去,就會進入 Function Log 的頁面,也就是保留函式運作狀況的記錄。這時,我們再試著發送一次訊息給 Line Bot,Function Log 就會跳出我們想要知道的資料,包括運作時間以及記憶體用量。這邊,我特意用console.log()把使用者傳送過來的訊息,也就是Excellent,記錄下來,如圖四。恩,我們的 Line Bot 從接收到訊息開始 (函式被觸發),到上網抓取資料,到回傳訊息給使用者,總共花了約 600 ms,記憶體用量 97 MB。跟 Netlify Functions 給的限制,10 s 加 1024 MB,比起來,是不是顯得游刃有餘呢?

https://ithelp.ithome.com.tw/upload/images/20210707/20120178jwvH8sbq3U.png
圖三、Functions 分頁

https://ithelp.ithome.com.tw/upload/images/20210707/201201783pin6Yzfce.png
圖四、Duration: 611.06 ms Memory Usage: 97 MB

記訊息 Line Bot

洋洋灑灑寫了這麼多,還沒看到今天的重點呢?今天不是說要來「探索 Netlify Functions 的暫存空間」嗎?
先來說說動機吧,為什麼要使用暫存空間呢?這答案真是再簡單不過了,因為輕鬆免費無負擔。
多虧了 10 秒的運行時間跟 1024 MB 的記憶體,我們放在 Netlify Functions 上面的 Line Bot 可以很快速即時去回應使用者的訊息 (需求)。不過有些時候我們需要的不只是即時,還要把使用者曾經說過的話找出來,也就是要有紀錄訊息的功能。
紀錄訊息還不簡單,連接一個資料庫不就結了?是的,大多數情況這是唯一的作法。不過嘛,偶爾偶爾,我們不需要像資料庫一樣,死死的記住每一則儲存下來的訊息,偶爾偶爾,我們只需要記得使用者說過的 3、4 句話,然後做個總結,傳回相對應的內容給使用者,這樣就夠了。如果只是這樣子的需求,其實不需要用到資料庫,也就不用麻煩的去註冊帳號、學習資料庫的使用方式、建立 Line Bot 與資料庫的連結,等等。
有這種偶爾偶爾嗎?舉例來說,幫使用者建立一個客製化的名片,就是這種偶爾偶爾。
一張名片,上面的內容不外乎:姓名、職稱、公司名稱、電話、電子郵件、地址等資訊。要利用 Line Bot 幫使用者建立客製化的名片,就需要這些內容。那麼,該如何設計 Line Bot 與使用者的互動,引導使用者將這些內容傳送給 Line Bot 呢?是該設計得像圖五,還是圖六呢?當然,這種東西沒有所謂的標準答案,每個人心中都有自己的美學。不過,Line Bot 是為使用者服務的,設計 Line Bot 與使用者的互動,必須要把使用者體驗考慮進去比較好。而我個人認為,圖六是一種比較親切簡單的互動方式。

https://ithelp.ithome.com.tw/upload/images/20210709/20120178DzJnlYsORY.png
圖五、使用者一次傳出所有需要的資料

https://ithelp.ithome.com.tw/upload/images/20210709/20120178feZNg46gh7.png
圖六、隨著 Line Bot 的引導,使用者依次傳出需要的資料

要做到如圖六這樣的互動,我們需要用到資料庫嗎?其實不必。只要記住使用者輸入的 5、6 個訊息,接著送出一張名片,就雙方兩清,誰也不欠誰了。在這樣的情況下,還要建立資料庫,就顯得有點大材小用了。但話又說回來,不把訊息存在資料庫,還能夠存到哪兒呢?

答案是,Netlify Functions 的暫存空間。

Netlify Functions,或說 AWS Lambda,在執行的時候,其實是創造出了一個容器 (container),這個容器是一個簡單的 Linux 作業系統,擁有 1024 MB 的記憶體 (對 Netlify Functions 而言),以及執行這個函式所需要的相關套件。函式執行完畢,該容器會短暫存在一段時間,直到 AWS 伺服器判斷這個容器已經處於閒置狀態了,就會把這個容器消滅掉➇。
既然在執行 Netlify Functions 的時候,會有一個相應的 Linux 作業系統產生,那麼我們在這個作業系統當中執行的函式,理應具有能夠將檔案寫入作業系統某處的能力,而這個某處,正是 Linux 的暫存空間/tmp
如果仔細研究 Netlify Functions 執行時,作業系統的檔案架構,應該會發現根目錄 (/) 內的檔案架構是這麼回事:

/
├───bin
├───boot
├───dev
├───etc
├───home
├───lib
├───lib64
├───media
├───mnt
├───opt
├───proc
├───root
├───run
├───sbin
├───srv
├───sys
├───tmp
├───usr
└───var
    └───task
        ├───lineBotWebhook.js
        ├───my_functions
        └───node_modules

常常使用 Linux 的朋友們應該會露出會心一笑:熟悉的檔案架構最對味。可以儲存資料的資料夾/tmp就在這邊。如果還想再仔細研究的話,我們上傳到 Netlify 的 Netlify Functions 就放在/var/task裡面。太好了,知道這樣就簡單多了,只要利用 Node.js 所內建的檔案系統 (File System) 模組,就能夠簡單存取需要的資料了:

// 引入 Node.js 內建的檔案系統模組
const fs = require('fs');

// read file synchronously
fs.readFileSync('<filename>', '<encoding>')

// write file synchronously
fs.writeFileSync('<filename>', data)

// delete file synchronously
fs.unlinkSync('<filename>')

想要使用 Netlify Functions 的暫存空間,就是這麼簡單!

下面提供示範的程式碼以及 Line Bot 互動的效果:

  • netlify-line-bot-demo/my_functions/lineBotWebhook/lineBotWebhook.js
// 引入需要的模組
const line = require('@line/bot-sdk')
const qaisTalk = require('./custom_module/qaisTalk.js');

const handler = async (event) => {

/* 省略了 Line Bot 初始化的相關動作 */  

    if (event.type !== 'message' || event.message.type !== 'text') {
      const response = qaisTalk.defaultTalk();
      await client.replyMessage(replyToken, response)
    } else {
      const response = qaisTalk.cardTalk(event);
      await client.replyMessage(replyToken, response);
    }
  }

/* 省略了 Webhook 驗證的相關動作 */

}

module.exports = { handler };

我們把回覆使用者的相關運作邏輯放在qaisTalk.js這個檔案當中:

  • netlify-line-bot-demo/my_functions/lineBotWebhook/custom_module/qaisTalk.js
// 引入需要的模組
const qaisFlex = require('./qaisFlex.js');

// 製造罐頭訊息
const defaultTalk = () => {
  // Create a new message.
  const response = {
    type: 'text',
    text: '而 Netlify 的回音盪漾'
  };
  return response;
}

// 為使用者生產客製化的名片
const cardTalk = (event) => {

  const readRecord = (userId) => {
    try {
      // 讀取暫存空間當中的檔案
      return JSON.parse(fs.readFileSync(`/tmp/${userId}.txt`, 'utf8'))
    } catch (err) {
      return { stage: 0, userReply: [] }
    }
  }

  const updateRecord = (userId, record, message) => {
    record.userReply.push(message)
    record.stage += 1
    if (record.stage < 4) {
      // 將檔案寫入暫存空間
      fs.writeFileSync(`/tmp/${userId}.txt`, JSON.stringify(record))
    } else {
      // 將暫存空間的檔案刪除
      fs.unlinkSync(`/tmp/${userId}.txt`)
    }
    return record
  }

  const userId = event.source.userId

  record = readRecord(userId)
  record = updateRecord(userId, record, event.message.text)

  let botReply

  // 製作客製化名片的流程
  switch (record.stage) {
    case 1:
      botReply = "嗨~歡迎建立 flexMessage 名片!\n[1] 請輸入姓名"
      break
    case 2:
      botReply = "[2] 請輸入電話"
      break
    case 3:
      botReply = "[3] 請輸入信箱"
      break
    case 4:
      botReply = qaisFlex.flexCard(record.userReply)
      break
  }

  if (record.stage !== 4) {
    const response = {
      type: 'text',
      text: botReply
    }

    return response;
  } else {
    const response = {
      type: 'flex',
      altText: 'Present Your Name Card',
      contents: botReply
    }

    return response;
  }
}

module.exports = { defaultTalk, cardTalk };

為了製作漂亮的名片,我們搬出壓箱寶,Line 所提供的 FlexMessage➈:

  • netlify-line-bot-demo/my_functions/lineBotWebhook/custom_module/qaisFlex.js
const colorDefault = "#666666"
const colorNetlify = "#00ad9f"

const flexCard = (userReply) => {
  console.log(userReply);
  const [_, name, phone, email] = userReply;

  return {
    "type": "bubble",
    "body": {
      "type": "box",
      "layout": "vertical",
      "contents": [flexNameContent(name), flexDetailContent(phone, email)],
      "paddingAll": "0px"
    }
  }
}

const flexNameContent = (name) => {
  return {
    "type": "box",
    "layout": "horizontal",
    "contents": [
      flexImage(),
      {
        "type": "box",
        "layout": "vertical",
        "contents": [
          flexFiller(),
          flexText("迷途小書僮", colorNetlify, "xs", "bold"),
          flexText(name, colorDefault, "xl", "bold"),
          flexBar(colorNetlify)
        ]
      }
    ],
    "spacing": "xl",
    "paddingTop": "20px",
    "paddingStart": "20px",
    "paddingEnd": "20px"
  }
}

const flexDetailContent = (phone, email) => {
  return {
    "type": "box",
    "layout": "vertical",
    "contents": [
      {
        "type": "box",
        "layout": "horizontal",
        "contents": [
          flexText("Phone", colorNetlify, "md", "bold"),
          flexText(phone, colorDefault, "md", "regular", 2),
        ]
      },
      {
        "type": "box",
        "layout": "horizontal",
        "contents": [
          flexText("Email", colorNetlify, "md", "bold"),
          flexText(email, colorDefault, "md", "regular", 2)
        ]
      }
    ],
    "paddingBottom": "20px",
    "paddingStart": "20px",
    "paddingEnd": "20px"
  }
}

const flexImage = () => ({
  "type": "box",
  "layout": "vertical",
  "contents": [
    {
      "type": "image",
      "url": "https://raw.githubusercontent.com/githubmhjao/netlify_function_practice/main/line_bot_on_netlify.png",
      "aspectMode": "cover",
      "size": "full"
    }
  ],
  "cornerRadius": "100px",
  "width": "72px",
  "height": "72px"
})

const flexText = (text, color, size, weight, flex = 1) => ({
  "type": "text",
  "text": text,
  "color": color,
  "size": size,
  "weight": weight,
  "flex": flex
});

const flexFiller = () => ({ "type": "filler" });

const flexBar = (color) => ({
  "type": "box",
  "layout": "vertical",
  "contents": [],
  "height": "3px",
  "backgroundColor": color
})

module.exports = { flexCard };

https://ithelp.ithome.com.tw/upload/images/20210709/20120178IoPHDyFdFV.png
圖七、在此遞上迷途小書僮華安的小小名片

參考資料

➀ line-bot-sdk-nodejs 模組官方文件
➁ Netlify Functions 基本介紹
➂ Line Webhook 基本介紹
➃ Line Webhook Event Objects 基本介紹
➄ Line message 種類介紹
➅ Line 官方帳號費率介紹
➆ 讓您的詞彙有意義,劍橋辭典
➇ AWS Lambda wiki 說明
➈ FlexMessage 官方文件


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

尚未有邦友留言

立即登入留言