最近喵了下 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 即可,
另外有的网站可能需要科学上网,这个需要你自己解决了。
搭建本地环境 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 搭建一个简单的我看过的电影展示列表,网站静态页面跟样式已经有了,你可以启动服务看下效果
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 ) => { console .log('event' , event) console .log('context' , context) 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 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 提供凭证,一般配置在环境变量里面,
这边使用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' )const movies = require ('../data/movies.json' )exports .handler = async () => { const movieScoreApi = new URL("https://www.omdbapi.com/" ); 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 } }) }) 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); }); 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 () => { 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
我这边不知道是因为网络原因还是啥的,netlify-cli
认证不了,所以我直接登录app.netlify 操作了,部署后,可以直接在线访问 ,
app.netlify 对应站点管理,对我们刚部署的站点启用Identify
,
Netlify Identify 已经集成了UI界面,所以我们要引入 netlify-identity-widget
1 2 <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 ; } 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