从此之後,你不会再碰到 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 是属於近年来相当火红的後端服务型态:无服务器运算服务 (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 CLI 来帮我们建立 Netlify Functions 吧!
安装 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。
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 要来帮我们变魔术了。
有了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 会进一步帮我们创造出适合的初始化样板。可能的选择包括有:
node-fetch
的函式当然,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
看一下 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"
}
path
:使用者请求的原始路径httpMethod
:使用者请求的方法 (GET, POST 等等)headers
:使用者请求的标头queryStringParameters
:使用者请求时的查询字串body
:使用者请求的主要内容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
。
好啦,都理解之後,我们就可以用 Netlify Dev 来布署看看。还记得要怎麽做吗?
netlify dev
用netlify dev
在本机端模拟 Netlify 布署结果。
图一、用netlify dev
在本机端模拟 Netlify 布署结果
咦,失败了吗?当然不是。我们根本就没有做什麽可以布署在前端的网页,Not Found 是再正常不过的结果了。但我们至少有做一个放在後端的无服务器运算函式,我们该向哪一个路径发出请求,才能呼叫 (触发) 这个函式呢?
答案是/.netlify/functions/你-Functions-的名字
!
图二、向/.netlify/functions/你-Functions-的名字
发送请求。在这个示范当中,我帮我的 Netlify Functions 取了一个好听的的名字helloWorld
。
这什麽奇怪的路径?没搞错吧。还记的我们阳春版 Netlify Functions 有一个查询路径的相关把戏,让我们用查询字串试试。/.netlify/functions/你-Functions-的名字?name=ithomemhjao
图三、向/.netlify/functions/你-Functions-的名字?name=你想试试看的-value
发送请求
原来是真的!我们的无服务器服务 Netlify Functions 就放在/.netlify/functions/你-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
图四、向/callback?name=你想试试看的-value
发送请求
这样是不是比较漂亮呢?
终於来到这里了。既然我们连 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】,如图五、图六,就可以输入想要用的环境变数了。
图五、【Build & deploy】->【Environment】
图六、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 个变数,包括body
、CHANNEL_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))
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 Bot。整个档案架构看起来如下:
netlify-functions-demo
├───netlify.toml
├───package.json
└───my_functions
└───lineBotWebhook
└───lineBotWebhook.js
这样我们就可以将这个专案资料夹布署到 Netlify 上了。同时,也要在 Line Developers 的 Line Bot 设定面板上调整 Webhook URL,如图七。详细要注意哪些,可以参考第 31 天的内容。
图七、设定好 Webhook URL
图八、而 Netlify 的回音荡漾着:唷~
这一篇文章当中,我们了解到如何撰写并布署无服务器服务 Netlify Functions,进一步透过无服务器服务来架设我们的 Line Bot Webhook Server。文章当中提供了阳春版 Line Bot 的程序码。剩下来的 2 篇文章,我们就要根据这个阳春版的 Line Bot,进行扩充和改装,希望最後能够完成我们的名片产生器 Line Bot。
➀ AWS Lambda event 官方文件
➁ Line Webhook Event Object 官方文件
<<: 不是使用专用的、标准化的设备清理命令的清除方法:消磁(Degaussing)
「将127.0.0.1改成内网IP」,这是上一篇的某个步骤,没浅浅扒一下网路基础,对学习有点影响~~...
还记得便利贴专案做到哪了吗?专案目前用的架构模式是 MVVM :Jetpack Compose 所做...
阵列(array) 阵列是一种资料结构, 可以储存相同类型的多个变数, 阵列中包含的变数称为阵列的元...
什麽是 Bootstrap ? 是一个框架系统 是一个UI的框架 (framework) 已提供现成...
前言 今日挑战的题目是115. Distinct Subsequences,虽然是hard,但因为有...