《赖田捕手:番外篇》第 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 官方文件


<<:  Microsoft Azure Pass 学习日志 Day 3

>>:  进击的软件工程师之路-软件战斗营 第十九周

用React刻自己的投资Dashboard Day17 - Dashboard 2.0版路由功能

tags: 2021铁人赛 React 如Day15的wireframe,为了要加上更多的功能,因此...

用 Python 畅玩 Line bot - 17:Template message

Line bot API 中有一种只有 line bot 专属的讯息种类,叫做 Template m...

DAY21: NPM模块管理工具

NPM是Node Package Manager的缩写,中文直接翻的话就是Node包管理工具, 比较...

Day.10 Stack

Stack(堆叠)是一种後进先出(LIFO)的资料结构 看一下图 注:图源 你可以想像一下在厨房洗碗...

D3JsDay27What's the tree?Let me see—树状图(tree diagram)

树状图介绍 以下节录自维基百科树状结构 树状结构(英语:Tree structure),又译树形结构...