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

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


<<:  不是使用专用的、标准化的设备清理命令的清除方法:消磁(Degaussing)

>>:  Day 23 (Js)

DAY7 浅扒网路 - 估计被扒皮的是我不是网路

「将127.0.0.1改成内网IP」,这是上一篇的某个步骤,没浅浅扒一下网路基础,对学习有点影响~~...

Firebase Firestore

还记得便利贴专案做到哪了吗?专案目前用的架构模式是 MVVM :Jetpack Compose 所做...

C# 一些特性

阵列(array) 阵列是一种资料结构, 可以储存相同类型的多个变数, 阵列中包含的变数称为阵列的元...

33岁转职者的前端笔记-DAY 17 Bootstrap 介绍及使用方法

什麽是 Bootstrap ? 是一个框架系统 是一个UI的框架 (framework) 已提供现成...

[Day 14] Leetcode 115. Distinct Subsequences (C++)

前言 今日挑战的题目是115. Distinct Subsequences,虽然是hard,但因为有...