一頓操作猛如虎,部署一個(gè)萬能 BFF
通過將 gatsby 的本地開發(fā) GraphQL 服務(wù)器 Serverless 化,借助其強(qiáng)大和豐富的插件系統(tǒng)以及生態(tài),實(shí)現(xiàn)一個(gè)萬能的 BFF 層。
Gatsby Js
Gatsby Js 最初的定位是一個(gè)靜態(tài)站點(diǎn)生成器,和一般的靜態(tài)站點(diǎn)生成器不同,它擁有豐富的源插件,可以從各種數(shù)據(jù)源同步數(shù)據(jù),通過 GraphQL Server 將這些數(shù)據(jù)暴露給客戶端。
由于它追求極致的性能和用戶體驗(yàn),因此其 GraphQL Server 只在站點(diǎn)生成階段運(yùn)行。也就是說,在本地開發(fā)和站點(diǎn)編譯時(shí),擁有一個(gè)動(dòng)態(tài)服務(wù)器,編譯階段,會(huì)讀取所有的數(shù)據(jù),最終生成靜態(tài)的 html 文件,并且分發(fā)到強(qiáng)大的 CDN 網(wǎng)絡(luò),從而實(shí)現(xiàn)頁面秒開效果。
gatsby 的生態(tài),幾乎集成了一切數(shù)據(jù)源,不管是調(diào)用 API、還是讀取數(shù)據(jù)庫、還是直接解析各種配置文件或者 markdown,都不用再寫代碼,只需要添加相關(guān)插件即可。盡管目前這一切只發(fā)生在編譯階段,但是只需要稍作魔改,就能將其部署成一個(gè)動(dòng)態(tài)服務(wù),變成一個(gè)萬能 BFF!
BFF 層
我不僅被它的極致用戶體驗(yàn)解決方案所吸引,還被它的本地 GraphQL Server 所吸引,憑借它豐富的插件,它這個(gè) GraphQL Server 就是一個(gè)天然優(yōu)秀的 BFF 層呀!
雖然其本地 GraphQL Server 只是用來生成靜態(tài)站點(diǎn)的,但是如果能將它部署到公網(wǎng),就可以實(shí)時(shí)為多端提供服務(wù)了,不僅網(wǎng)站可以使用其數(shù)據(jù)源,小程序,APP 都可以使用。
BFF 層的提出,本來是針對(duì)不同的端提供不同的 BFF 服務(wù),但由于使用了 GraphQL,將服務(wù)的聚合裁剪功能扔到前端,于是一個(gè)服務(wù)就能同時(shí)給到不同的端。
免費(fèi)的 AWS lambda
部署到公網(wǎng)很有吸引力,但是要花錢的話,就沒意思了。
于是,我把目光瞄準(zhǔn)了 AWS lambda,它的免費(fèi)額度夠我用的了。
說干就干
最終效果演示:https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql

源代碼庫:https://github.com/Jeff-Tian/serverless-space
源代碼庫代碼較多,主要是把 gatsby-js 的一個(gè)庫 gatsby-recipes 拷貝過來做了一番魔改,以繞過 AWS lambda 環(huán)境中,不能寫文件的問題。下面對(duì)主要的改造過程做個(gè)分解。
Serverless
Serverless 本意是去掉服務(wù)器,讓開發(fā)者只需要關(guān)注業(yè)務(wù)邏輯,不用管基礎(chǔ)設(shè)施,不同的云廠商對(duì)其有不同的實(shí)現(xiàn)。Serverless 框架做了個(gè)抽象,讓開發(fā)者通過一個(gè)統(tǒng)一的 yaml 文件定義服務(wù),它來對(duì)不同的云廠商做具體的適配。
安裝
npm install -g serverless定義服務(wù)
serverless.yml
service: serverless-spaceprovider:name: awsruntime: nodejs12.xlambdaHashingVersion: 20201221package:patterns:- '!node_modules/**'- '!layers/**'functions:gatsby:handler: dist/src/gatsby.handlerlayers:- {Ref: LibLambdaLayer}events:- http:method: ANYpath: gatsby/- http:method: ANYpath: 'gatsby/{proxy+}'environment:SERVERLESS_EXPRESS_PLATFORM: awsplugins:serverless-plugin-layer-managerserverless-offlineserverless-expresslayers:lib:path: layersname: space-libdescription: My dependenciesretain: true
從上面可以看到,定義中使用了一些插件,serverless-offline 和 serverless-express 是為了方便本地運(yùn)行用的,實(shí)現(xiàn) serverless offline 在本地環(huán)境下模擬 lambda。而 serverless-plugin-layer-manager 則是用來對(duì) lambda 分層。通過使用這個(gè)插件,只需要定義層就好,省去了手動(dòng)壓縮、上傳、關(guān)聯(lián)等等繁雜的工作,非常方便。
分層
分層的好處是把變動(dòng)不頻繁的 node_modules 部分與變動(dòng)頻繁的應(yīng)用業(yè)務(wù)邏輯代碼隔離,從而減小每次發(fā)布時(shí)的網(wǎng)絡(luò)傳輸大小,以及可以實(shí)現(xiàn)同一個(gè)層同時(shí)為多個(gè) lambda 服務(wù)。
在 serverless yaml 配置文件里,對(duì)于分層有個(gè)命名約定。比如你的分層命名為 xxx,那么在引用它時(shí),就要用 {Ref: XxxLambdaLayer},并且注意大小寫。
全局安裝 serverless 及其插件
這并不是必需的,但是推薦。原因是無論是對(duì)于 lambda 應(yīng)用代碼,以及 node_modules 分層大小,都有一個(gè) 250 M 的上限,這個(gè)上限是壓縮前的大小。如果不采用全局安裝,會(huì)導(dǎo)致 serverless 自動(dòng)將插件安裝在 node_modules 里,導(dǎo)致增加 node_modules 文件夾的大小。
npm install -g serverlessnpm install -g serverless-plugin-layer-manager...
部署
serverless 可以一鍵部署,自動(dòng)搞定資源分配和建立關(guān)聯(lián)、以及權(quán)限配置等等。在寫好應(yīng)用代碼,配置好 serverless.yml 文件后,就能一鍵部署:
serverless deploy項(xiàng)目大致目錄結(jié)構(gòu)
|---- layers|---- nodejs|---- .npmrc|---- node_modules|---- node_modules|---- src|---- gatsby.ts|---- gatsby-recipes|---- ...|---- serverless.yml|---- tsconfig.json|---- tsconfig.build.json|---- package.json
layers 目錄是分層用的,注意它一定要包含一個(gè) nodejs 目錄,在部署前,必須的依賴就安裝在這個(gè)目錄下,所以可以在這個(gè)目錄下建立一個(gè)文件 .npmrc,并配置為只安裝生產(chǎn)必須的依賴:
.npmrc
only=production
TypeScript 配置
TypeScript 是 JavaScript 的一個(gè)超集,解決了原生 JavaScript 飽受詬病的動(dòng)態(tài)特性,建立了一個(gè)完善的類型系統(tǒng)。為了使用 TypeScript 開發(fā)的同時(shí),部署成 JavaScript,需要配置指示 tsc 如何將 TypeScript 轉(zhuǎn)譯成 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 目錄保留原始項(xiàng)目結(jié)構(gòu)(即有 src 部分),不然 dist 目錄下會(huì)直接是 src 下的被轉(zhuǎn)譯后的文件。
為了排除不必要的文件,可以在 tsconfig.build.json 里指定:
{"extends": "./tsconfig.json","exclude": ["dist","test","**/*spec.ts"]}
這里還配置了 "jsx" 以支持 react-jsx,因?yàn)?gatsby-recipes 里需要。
魔改 gatsby-recipes
將 0.9.3 這個(gè)版本的 gatsby-recipes 源文件拷貝到項(xiàng)目,刪除其 dist 目錄,然后打開 src/gatsby-recipes/src/providers/npm/package.js 文件,將其原本的 getConfigStore 引用刪除,然后改寫:
- import {getConfigStore} from 'gatsby-core-utils'+ const getConfigStore = () => ({ get: () => 'yarn' })
這樣就能避免在 lambda 環(huán)境,該文件嘗試寫文件的錯(cuò)誤。
gatsby.ts 入口文件
這是整個(gè) lambda 的入口,詳細(xì)參見源代碼。主要工作是將 express 替換成 serverless/express,同時(shí)引入 bodyParser,否則會(huì)接受不到客戶端傳來的 GraphQL 查詢。因?yàn)?GraphQL 本質(zhì)上是一個(gè) HTTP Post 請(qǐng)求。
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 = 3000if (require.main === module) {console.log('called directly')app.listen(port, () => {console.log(`Example gatsby serverless app listening at http://localhost:${port}`)})}export default appconst bootstrap = async () => {return serverlessExpress({app})}let serverexport const handler: Handler = async (event: any,context: Context,callback: Callback,) => {server = server ?? (await bootstrap())return server(event, context, callback)}
以上代碼判斷 require.main,如果是本地直接運(yùn)行,就會(huì)監(jiān)聽 (3000) 端口,準(zhǔn)備提供服務(wù)。而如果是在 lambda 環(huán)境,那么 handler 函數(shù)會(huì)被觸發(fā)。
總結(jié)
通過對(duì) gatsby-recipes 的魔改,使得 gatsby 本地開發(fā)用的 GraphQL Server 可以運(yùn)行在 AWS lambda 上,從而實(shí)現(xiàn)了一個(gè)免費(fèi)又強(qiáng)大(萬能)的 BFF 層。
后面只需要添加不同的數(shù)據(jù)源插件,就能給不同的前端提供幾乎所有服務(wù)了。
有興趣的同學(xué)歡迎持續(xù)關(guān)注、點(diǎn)贊和在看,后面將持續(xù)更新,使用真實(shí)案例,分解如何利用該萬能 BFF,應(yīng)用在具體的場景上。
