一顿操作猛如虎,部署一个万能 BFF
通过将 gatsby 的本地开发 GraphQL 服务器 Serverless 化,借助其强大和丰富的插件系统以及生态,实现一个万能的 BFF 层。
Gatsby Js
Gatsby Js 最初的定位是一个静态站点生成器,和一般的静态站点生成器不同,它拥有丰富的源插件,可以从各种数据源同步数据,通过 GraphQL Server 将这些数据暴露给客户端。
由于它追求极致的性能和用户体验,因此其 GraphQL Server 只在站点生成阶段运行。也就是说,在本地开发和站点编译时,拥有一个动态服务器,编译阶段,会读取所有的数据,最终生成静态的 html 文件,并且分发到强大的 CDN 网络,从而实现页面秒开效果。
gatsby 的生态,几乎集成了一切数据源,不管是调用 API、还是读取数据库、还是直接解析各种配置文件或者 markdown,都不用再写代码,只需要添加相关插件即可。尽管目前这一切只发生在编译阶段,但是只需要稍作魔改,就能将其部署成一个动态服务,变成一个万能 BFF!
BFF 层
我不仅被它的极致用户体验解决方案所吸引,还被它的本地 GraphQL Server 所吸引,凭借它丰富的插件,它这个 GraphQL Server 就是一个天然优秀的 BFF 层呀!
虽然其本地 GraphQL Server 只是用来生成静态站点的,但是如果能将它部署到公网,就可以实时为多端提供服务了,不仅网站可以使用其数据源,小程序,APP 都可以使用。
BFF 层的提出,本来是针对不同的端提供不同的 BFF 服务,但由于使用了 GraphQL,将服务的聚合裁剪功能扔到前端,于是一个服务就能同时给到不同的端。
免费的 AWS lambda
部署到公网很有吸引力,但是要花钱的话,就没意思了。
于是,我把目光瞄准了 AWS lambda,它的免费额度够我用的了。
说干就干
最终效果演示:https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql
源代码库:https://github.com/Jeff-Tian/serverless-space
源代码库代码较多,主要是把 gatsby-js 的一个库 gatsby-recipes 拷贝过来做了一番魔改,以绕过 AWS lambda 环境中,不能写文件的问题。下面对主要的改造过程做个分解。
Serverless
Serverless 本意是去掉服务器,让开发者只需要关注业务逻辑,不用管基础设施,不同的云厂商对其有不同的实现。Serverless 框架做了个抽象,让开发者通过一个统一的 yaml 文件定义服务,它来对不同的云厂商做具体的适配。
安装
npm install -g serverless
定义服务
serverless.yml
service: serverless-space
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
package:
patterns:
- '!node_modules/**'
- '!layers/**'
functions:
gatsby:
handler: dist/src/gatsby.handler
layers:
- {Ref: LibLambdaLayer}
events:
- http:
method: ANY
path: gatsby/
- http:
method: ANY
path: 'gatsby/{proxy+}'
environment:
SERVERLESS_EXPRESS_PLATFORM: aws
plugins:
serverless-plugin-layer-manager
serverless-offline
serverless-express
layers:
lib:
path: layers
name: space-lib
description: My dependencies
retain: true
从上面可以看到,定义中使用了一些插件,serverless-offline 和 serverless-express 是为了方便本地运行用的,实现 serverless offline 在本地环境下模拟 lambda。而 serverless-plugin-layer-manager 则是用来对 lambda 分层。通过使用这个插件,只需要定义层就好,省去了手动压缩、上传、关联等等繁杂的工作,非常方便。
分层
分层的好处是把变动不频繁的 node_modules 部分与变动频繁的应用业务逻辑代码隔离,从而减小每次发布时的网络传输大小,以及可以实现同一个层同时为多个 lambda 服务。
在 serverless yaml 配置文件里,对于分层有个命名约定。比如你的分层命名为 xxx,那么在引用它时,就要用 {Ref: XxxLambdaLayer},并且注意大小写。
全局安装 serverless 及其插件
这并不是必需的,但是推荐。原因是无论是对于 lambda 应用代码,以及 node_modules 分层大小,都有一个 250 M 的上限,这个上限是压缩前的大小。如果不采用全局安装,会导致 serverless 自动将插件安装在 node_modules 里,导致增加 node_modules 文件夹的大小。
npm install -g serverless
npm install -g serverless-plugin-layer-manager
...
部署
serverless 可以一键部署,自动搞定资源分配和建立关联、以及权限配置等等。在写好应用代码,配置好 serverless.yml 文件后,就能一键部署:
serverless deploy
项目大致目录结构
|---- layers
|---- nodejs
|---- .npmrc
|---- node_modules
|---- node_modules
|---- src
|---- gatsby.ts
|---- gatsby-recipes
|---- ...
|---- serverless.yml
|---- tsconfig.json
|---- tsconfig.build.json
|---- package.json
layers 目录是分层用的,注意它一定要包含一个 nodejs 目录,在部署前,必须的依赖就安装在这个目录下,所以可以在这个目录下建立一个文件 .npmrc,并配置为只安装生产必须的依赖:
.npmrc
only=production
TypeScript 配置
TypeScript 是 JavaScript 的一个超集,解决了原生 JavaScript 饱受诟病的动态特性,建立了一个完善的类型系统。为了使用 TypeScript 开发的同时,部署成 JavaScript,需要配置指示 tsc 如何将 TypeScript 转译成 JavaScript。
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"allowJs": true,
"jsx": "react-jsx",
},
"include": [
"src/**/*",
"README.md"
]
}
注意这里在 "include" 部分除了包含必要的 src 目录下的文件外,还额外引入了根目录下的 READ.md 文件,这只是为了让生成的 dist 目录保留原始项目结构(即有 src 部分),不然 dist 目录下会直接是 src 下的被转译后的文件。
为了排除不必要的文件,可以在 tsconfig.build.json 里指定:
{
"extends": "./tsconfig.json",
"exclude": [
"dist",
"test",
"**/*spec.ts"
]
}
这里还配置了 "jsx" 以支持 react-jsx,因为 gatsby-recipes 里需要。
魔改 gatsby-recipes
将 0.9.3 这个版本的 gatsby-recipes 源文件拷贝到项目,删除其 dist 目录,然后打开 src/gatsby-recipes/src/providers/npm/package.js 文件,将其原本的 getConfigStore 引用删除,然后改写:
- import {getConfigStore} from 'gatsby-core-utils'
+ const getConfigStore = () => ({ get: () => 'yarn' })
这样就能避免在 lambda 环境,该文件尝试写文件的错误。
gatsby.ts 入口文件
这是整个 lambda 的入口,详细参见源代码。主要工作是将 express 替换成 serverless/express,同时引入 bodyParser,否则会接受不到客户端传来的 GraphQL 查询。因为 GraphQL 本质上是一个 HTTP Post 请求。
import express from 'serverless-express/express'
import {graphqlHTTP} from "express-graphql"
import cors from "cors"
import bodyParser from "body-parser"
const app = express()
app.use(cors())
app.use(bodyParser.json())
app.use(`/graphql`, graphqlHTTP({
schema,
graphiql: true,
context: {root: directory}
}))
const port = 3000
if (require.main === module) {
console.log('called directly')
app.listen(port, () => {
console.log(`Example gatsby serverless app listening at http://localhost:${port}`)
})
}
export default app
const bootstrap = async () => {
return serverlessExpress({app})
}
let server
export const handler: Handler = async (
event: any,
context: Context,
callback: Callback,
) => {
server = server ?? (await bootstrap())
return server(event, context, callback)
}
以上代码判断 require.main,如果是本地直接运行,就会监听 (3000) 端口,准备提供服务。而如果是在 lambda 环境,那么 handler 函数会被触发。
总结
通过对 gatsby-recipes 的魔改,使得 gatsby 本地开发用的 GraphQL Server 可以运行在 AWS lambda 上,从而实现了一个免费又强大(万能)的 BFF 层。
后面只需要添加不同的数据源插件,就能给不同的前端提供几乎所有服务了。
有兴趣的同学欢迎持续关注、点赞和在看,后面将持续更新,使用真实案例,分解如何利用该万能 BFF,应用在具体的场景上。