通过将 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
image.png
源代码库: https://github.com/Jeff-Tian/serverless-space

源代码库代码较多,主要是把 gatsby-js 的一个库 gatsby-recipes 拷贝过来做了一番魔改,以绕过 AWS lambda 环境中,不能写文件的问题。下面对主要的改造过程做个分解。

Serverless

Serverless 本意是去掉服务器,让开发者只需要关注业务逻辑,不用管基础设施,不同的云厂商对其有不同的实现。Serverless 框架做了个抽象,让开发者通过一个统一的 yaml 文件定义服务,它来对不同的云厂商做具体的适配。

安装


shell npm install -g serverless

定义服务

serverless.yml yaml 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: SERVERLESSEXPRESSPLATFORM: 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 应用代码,以及 nodemodules 分层大小,都有一个 250 M 的上限,这个上限是压缩前的大小。如果不采用全局安装,会导致 serverless 自动将插件安装在 nodemodules 里,导致增加 node_modules 文件夹的大小。

bash npm install -g serverless npm install -g serverless-plugin-layer-manager ...


部署

serverless 可以一键部署,自动搞定资源分配和建立关联、以及权限配置等等。在写好应用代码,配置好 serverless.yml 文件后,就能一键部署:

bash serverless deploy

项目大致目录结构


bash |---- layers |---- nodejs |---- .npmrc |---- nodemodules |---- nodemodules |---- src |---- gatsby.ts |---- gatsby-recipes |---- ... |---- serverless.yml |---- tsconfig.json |---- tsconfig.build.json |---- package.json

layers 目录是分层用的,注意它一定要包含一个 nodejs 目录,在部署前,必须的依赖就安装在这个目录下,所以可以在这个目录下建立一个文件 .npmrc,并配置为只安装生产必须的依赖:

.npmrc bash only=production

TypeScript 配置

TypeScript 是 JavaScript 的一个超集,解决了原生 JavaScript 饱受诟病的动态特性,建立了一个完善的类型系统。为了使用 TypeScript 开发的同时,部署成 JavaScript,需要配置指示 tsc 如何将 TypeScript 转译成 JavaScript。

tsconfig.json 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 里指定: 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 引用删除,然后改写:

diff

  • import {getConfigStore} from gatsby-core-utils
  • const getConfigStore = () => ({ get: () => yarn })

这样就能避免在 lambda 环境,该文件尝试写文件的错误。

gatsby.ts 入口文件


这是整个 lambda 的入口,详细参见源代码。主要工作是将 express 替换成 serverless/express,同时引入 bodyParser,否则会接受不到客户端传来的 GraphQL 查询。因为 GraphQL 本质上是一个 HTTP Post 请求。

typescript 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,应用在具体的场景上。