一顿操作猛如虎,部署一个万能 BFF

共 5558字,需浏览 12分钟

 ·

2021-09-22 12:40

通过将 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 serverlessnpm 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,应用在具体的场景上。


浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报