intro to serverless functions

intro to serverless functions

五月 22, 2021

最近喵了下 Introduction to Serverless Functions 视频感觉还不错。下面主要根据视频作者Jason Lengstorfppt 简单做下笔记,最底下会列出相关链接。

示例代码 示例网站

前言

serverless 一般分为bass(Backend as a Service)和fass(Function as a Service)两种,今天我们主要学习一下fass,国内云厂商一般称为云函数。简单来说,就是依赖云厂商的平台建设,使我们只关注业务代码,不需要考虑服务器相关内容,也更容易的进行资源的弹性扩容,基于事件驱动,避免闲时资源浪费。

当然有利也有弊,fass基于Data-shipping架构,即:将数据传输到代码所在处执行,而不是将代码传输到数据所在处执行,因为数据一般都比代码的传输量要大得多。而目前主流的 Faas 平台都是『将数据传输到代码所在处执行』这种架构,所以这是 Faas 的最大缺陷,同时也不好维护状态,事务啥的。

所以,一般就作为一些公共基础建设,比如图片预处理,API网关数据拉取,这种无状态独立功能函数。

准备工作

今天我们主要使用node.js构建我们的函数应用,所以你要先安装下node.js v12 or higher

需要云服务商NetlifyHasura提供服务,所以需要你有这两个网站的账号,都提供免费额度使用,所以无需担心费用问题。

还需要,omdbapi,提供模拟数据,所以创建一个API KEY即可,

另外有的网站可能需要科学上网,这个需要你自己解决了。

搭建本地环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# install the Netlify CLI for local development
npm install -g netlify-cli@latest
# clone the starting point for development
git clone --branch start https://github.com/jlengstorf/frontendmasters-serverless.git
# recommended use vscode
code frontendmasters-serverless
# create functions dir
cd frontendmasters-serverless
mkdir functions
# cofnig functions location
vim netlify.toml
[build]
command = "npm run build"
publish = "_site"
functions = "functions"

最终目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
.eleventy.js
.env # 环境变量
.gitignore
_site
data # 静态数据
functions # 函数目录
netlify.toml # 配置文件
node_modules
package.json
package-lock.json
README.md
src # 网站代码

这个看你使用哪个云厂商提供服务了,目录跟配置项可能不一样,本示例使用的是Netlify Functions

我们现在就来使用serverless functions 搭建一个简单的我看过的电影展示列表,网站静态页面跟样式已经有了,你可以启动服务看下效果

1
ntl dev

Boop

1
2
cd functions
vim boop.js
1
2
3
4
5
6
7
8
9
10
11
12
13
exports.handler = (event, context, callback) => {
// "event" has information about the path, body, headers, etc. of the request
console.log('event', event)
// "context" has information about the lambda environment and user details
console.log('context', context)
// The "callback" ends the execution of the function and returns a response back to the caller
return callback(null, {
statusCode: 200,
body: JSON.stringify({
data: 'Boop!'
})
})
}

可以看到,serverless functions 函数结构很简单,就是声明一个方法,预处理,然后执行回调函数。

当然你直接使用return

1
2
3
4
5
6
exports.handler = async () => {
return {
statusCode: 200,
body: 'Boop!'
}
}

然后,我们访问http://localhost:8888/.netlify/functions/boop,即可看到返回的数据了

获取本地数据

现在我们要从本地,data/movies.json 获取数据,然后渲染在页面上,functions 目录下创建movies.js

1
2
3
4
5
6
7
8
//  get movies from local json
const movies = require('../data/movies.json')
exports.handler = async () => {
return {
statusCode: 200,
body: JSON.stringify(moviesWithRatings),
}
}

然后src/index.html 请求数据,渲染页面即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
async function initialize() {
const movies = await fetch('/.netlify/functions/movies').then((response) =>
response.json(),
);

const container = document.querySelector('.movies');
const template = document.querySelector('#movie-template');

movies.forEach((movie) => {
const element = template.content.cloneNode(true);

const img = element.querySelector('img');
img.src = movie.poster;
img.alt = movie.title;

element.querySelector('h2').innerText = movie.title;
element.querySelector('.tagline').innerText = movie.tagline;

container.appendChild(element);
});
}
</script>

获取请求参数

用过event参数获取,functions 目录下创建movie-by-id.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const movies = require('../data/movies.json')

exports.handler = async({ queryStringParameters }) => {
const { id } = queryStringParameters;
const moive = movies.find(m => m.id === id);

if(!moive) {
return {
statusCode: 404,
body: 'Not Found'
}
}

return {
statusCode: 200,
body: JSON.stringify(moive)
}
}

然后访问http://localhost:8888/.netlify/functions/movie-by-id?id=tt2975590

当然event还包含很多其他信息,有兴趣的话可以喵下官方文档,或者,把event返回出来看看。

拉取OMDBAPI数据

获取第三方接口,一般需要API-KEY提供凭证,一般配置在环境变量里面,

1
2
vim .env
OMDB_API_KEY=

这边使用node-fetch请求第三方接口

1
npm install node-fetch

我们这里,通过OMDBAPI获取影片的评分信息,调整functions/movies.js代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

const { URL } = require('url')
const fetch = require('node-fetch')

// get movies from local json
const movies = require('../data/movies.json')

exports.handler = async () => {

const movieScoreApi = new URL("https://www.omdbapi.com/");
// add the secret API key to the query string
movieScoreApi.searchParams.set('apiKey', process.env.OMDB_API_KEY)

const promises = movies.map(movie => {
movieScoreApi.searchParams.set('i', movie.id);
return fetch(movieScoreApi)
.then(response => response.json())
.then(data => {
const scores = data.Ratings;
return {
...movie,
scores
}
})
})

// awaiting all Promises lets the requests happen in parallel
// see: https://lwj.dev/blog/keep-async-await-from-blocking-execution/
const moviesWithRatings = await Promise.all(promises);

return {
statusCode: 200,
body: JSON.stringify(moviesWithRatings),
}
}

src/index.html 代码页面也做相应调整,这里就不多赘述。

Hasura Graphql

movies.json的数据到达一定量的话,一般会存到数据库便于管理,这边使用Hasura Graphql,创建一张表movies表存储,并将movies.json数据填充几条到movies表中。

所以这边也要配置下Hasura Graphql 请求的环境变量:

1
2
3
vim .env
HASURA_API_URL=
HASURA_ADMIN_SECRET=

关于Hasura Graphql的使用,语法等可以参考官方文档hasura graphql

HASURA_ADMIN_SECRET 其实就是NEW ENV VARS即可,

首先我们写一个工具方法,用来调用Hasura Graphql接口,functions/util/hasura.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fetch = require('node-fetch')

async function query({ query, variables = {} }) {
const result = await fetch(process.env.HASURA_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hasura-Admin-Secret': process.env.HASURA_ADMIN_SECRET,
},
body: JSON.stringify({ query, variables }),
})
.then(response => response.json())
.catch(function(e) {
console.log(e);
});
// TODO send back helpful information if there are errors

console.info(result)
return result.data;
}

exports.query = query;

然后再调整一下,functions/movies.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

const { query } = require('./util/hasura');

exports.handler = async () => {

// get movies from db
const { movies } = await query({
query: `
query {
movies {
id
title
tagline
poster
}
}
`,
});

...other code

return {
statusCode: 200,
body: JSON.stringify(moviesWithRatings),
}
}

然后再写个添加方法functions/add-movie.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { query } = require('./util/hasura')

exports.handler = async function(event) {
const { id, title, tagline, poster } = JSON.parse(event.body);

const result = await query({
query: `
mutation CreateMovie($id: String!, $poster: String!, $tagline: String!, $title: String!) {
insert_movies_one(object: {id: $id, poster: $poster, tagline: $tagline, title: $title}) {
id
poster
tagline
title
}
}
`,
variables: { id, title, tagline, poster },
});

return {
statusCode: 200,
body: JSON.stringify(result),
};
}

调整下添加页面,src/admin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
async function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
const result = await fetch('/.netlify/functions/add-movie', {
method: 'POST',
body: JSON.stringify({
id: data.get('id'),
title: data.get('title'),
tagline: data.get('tagline'),
poster: data.get('poster'),
}),
}).then((response) => {
document.querySelector(
'.message',
).innerText = `Response: ${response.status}${response.statusText}`;
});
}

document.querySelector('#add-movie').addEventListener('submit', handleSubmit);
</script>

Netlify Identify

网站一般要进行身份验证,然后不同身份认证有不同的操作权限。比如我的影片列表,我可以进行添加编辑操作,其他人只能进行浏览。所以,一般要引入身份认证,如果从头自己搞登录逻辑,可能比较繁琐,一般也是独立出一个认证的微服务。

这边简单的使用Netlify Identify来进行网站的身份认证,如果你使用其他云厂商,这部分可以略过

此时需要我们先部署一个网站,你可以直接使用netlify-cli

1
ntl init

我这边不知道是因为网络原因还是啥的,netlify-cli认证不了,所以我直接登录app.netlify操作了,部署后,可以直接在线访问

app.netlify 对应站点管理,对我们刚部署的站点启用Identify

Netlify Identify 已经集成了UI界面,所以我们要引入 netlify-identity-widget

1
2
<!-- include the widget -->
<script type="text/javascript" src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script >

这里,我只在src/admin.html src/login.html引入了,

这边我想要实现的效果是,用户登录才能访问src/admin页面进行添加电影的操作,

首先,我们在src/login.html 添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div data-netlify-identity-button></div>
<script
type="text/javascript"
src="https://identity.netlify.com/v1/netlify-identity-widget.js"
></script>

<script>
function handleLogin(user) {
if (!user || !user.token) {
return;
}

// if we get here, we have an active user; redirect to the admin page!
window.location.pathname = '/admin/';
}

window.netlifyIdentity.on('init', handleLogin);
window.netlifyIdentity.on('login', handleLogin);
</script>

调整src/admin.html 代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script
type="text/javascript"
src="https://identity.netlify.com/v1/netlify-identity-widget.js"
></script>
<script>

function handleIdentityEvent(user) {
if (user && user.token) {
return;
}

window.location.pathname = '/login/';
}

netlifyIdentity.on('init', handleIdentityEvent);
netlifyIdentity.on('logout', handleIdentityEvent);

document.querySelector('.logout').addEventListener('click', (event) => {
event.preventDefault();
netlifyIdentity.logout();
});

....

腾讯云函数

这里简单举个小例子,比如做一个返回json数据的接口,触发管理为API网关触发器,可以看到语言规范基本一致。

1
2
3
4
5
6
7
8
9
'use strict';

exports.main_handler = (event, context, callback) => {
console.log("Hello World")
console.log(event)
console.log(event["non-exist"])
console.log(context)
callback(null, require('data/fooddata.js'))
};

跟据自己的需要选择合适的云平台,当然也有很多完善云平台使用的serverless框架,

本篇仅作为一个简单的入门demo,回调方法也不仅仅是返回JSON,也有很多OUTPUT形式,各个云平台也会跟据自己现有的服务,提供各种集成机制,感兴趣的自己自行扩展阅读。

相关链接

github:serverless

github:awesome-serverless

Introduction to Serverless Functions

gihub:frontendmasters-serverless

netlify

hasura