最近喵了下 Introduction to Serverless Functions 视频感觉还不错。下面主要根据视频作者Jason Lengstorf
的ppt
简单做下笔记,最底下会列出相关链接。
前言
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,
需要云服务商Netlify跟Hasura提供服务,所以需要你有这两个网站的账号,都提供免费额度使用,所以无需担心费用问题。
还需要,omdbapi,提供模拟数据,所以创建一个API KEY即可,
另外有的网站可能需要科学上网,这个需要你自己解决了。
搭建本地环境
# 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"
最终目录结构如下:
.eleventy.js
.env # 环境变量
.gitignore
_site
data # 静态数据
functions # 函数目录
netlify.toml # 配置文件
node_modules
package.json
package-lock.json
README.md
src # 网站代码
这个看你使用哪个云厂商提供服务了,目录跟配置项可能不一样,本示例使用的是Netlify Functions,
我们现在就来使用serverless functions 搭建一个简单的我看过的电影展示列表,网站静态页面跟样式已经有了,你可以启动服务看下效果
ntl dev
Boop
cd functions
vim boop.js
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
exports.handler = async () => {
return {
statusCode: 200,
body: 'Boop!'
}
}
然后,我们访问http://localhost:8888/.netlify/functions/boop
,即可看到返回的数据了
获取本地数据
现在我们要从本地,data/movies.json
获取数据,然后渲染在页面上,functions
目录下创建movies.js
// get movies from local json
const movies = require('../data/movies.json')
exports.handler = async () => {
return {
statusCode: 200,
body: JSON.stringify(moviesWithRatings),
}
}
然后src/index.html
请求数据,渲染页面即可
<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
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提供凭证,一般配置在环境变量里面,
vim .env
OMDB_API_KEY=
这边使用node-fetch
请求第三方接口
npm install node-fetch
我们这里,通过OMDBAPI获取影片的评分信息,调整functions/movies.js
代码示例如下:
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 请求的环境变量:
vim .env
HASURA_API_URL=
HASURA_ADMIN_SECRET=
关于Hasura Graphql的使用,语法等可以参考官方文档hasura graphql,
HASURA_ADMIN_SECRET
其实就是NEW ENV VARS
即可,
首先我们写一个工具方法,用来调用Hasura Graphql接口,functions/util/hasura.js
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
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
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
<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
ntl init
我这边不知道是因为网络原因还是啥的,netlify-cli
认证不了,所以我直接登录app.netlify操作了,部署后,可以直接在线访问,
app.netlify 对应站点管理,对我们刚部署的站点启用Identify
,
Netlify Identify 已经集成了UI界面,所以我们要引入 netlify-identity-widget
<!-- 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
添加如下代码:
<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
代码,
<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网关触发器,可以看到语言规范基本一致。
'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
形式,各个云平台也会跟据自己现有的服务,提供各种集成机制,感兴趣的自己自行扩展阅读。
相关链接
Introduction to Serverless Functions
- Post link: https://jianchengwang.info/2020/11/26/web/serverless/intro-to-serverless-functions/
- Copyright Notice: All articles in this blog are licensed under unless stating additionally.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues