在小程序里接入 GraphQL
背景
在前幾篇文章里,一直在講 GraphQL。分別是:
? ? 《一頓操作猛如虎,部署一個萬能 BFF》,利用 Gatsjs 開發(fā)用的 GraphQL 服務(wù)器,結(jié)合其豐富的數(shù)據(jù)源插件,部署了一個幾乎萬能的 BFF 層。
? ? 《使用萬能 BFF,將語雀文章 GraphQL 服務(wù)化》,萬能 BFF 層的實例,利用現(xiàn)有插件,直接把語雀的服務(wù) GraphQL 化了。
BFF 是 Backend For Frontend 的簡稱,是為前端服務(wù)的后端。要發(fā)揮真正的用處,還得通過前端體現(xiàn)?,F(xiàn)在就給個實例,講解如何在小程序里集成萬能 BFF。
前端主要有 Web、小程序以及 Native App 等。要在前端集成 GraphQL,一般采用 Apollo Client。為什么本文選擇小程序作為例子呢?因為小程序是中國特色,國外沒有。對于如何在 Web 端或者 Native App 端集成 GraphQL,只需要按照 https://www.apollographql.com/docs/ 官方文檔的指導(dǎo)去做即可。

采用小程序作為例子,不僅彌補了官方文檔的空白,而且由于小程序的一些限制,在集成 GraphQL 的過程中,還面臨一些額外的挑戰(zhàn)。因為更困難,所以更加符合本公眾號(哈德韋,即 Hard Way 的音譯)的初衷。
最終成果演示
? ? ? Web 版:https://taro.pa-ca.me/
? ? ? ?微信小程序(體驗版):
??????可以微信掃碼打開小程序體驗版(由于還沒有發(fā)布,因此只能以體驗的形式),申請體驗。我看到申請請求后會第一時間通過,有 100 名的限制哦,如果因為人數(shù)超限不能體驗,那么請等待我的下一篇文章,如果哈德韋微信小程序正式發(fā)布上線,我會再發(fā)文通知大家。
項目源代碼
? ? ? ?https://github.com/Jeff-Tian/weapp
Taro Js
盡管這里只做了微信小程序,但是采用了多端統(tǒng)一開發(fā)框架 Taro Js,從而可以打包到不同的平臺。
挑戰(zhàn)一:在小程序里生成 Apollo Client 實例
基本可以參考官方文檔的 React 示例,但是對于小程序,卻不能照搬。如果只做 Web 端,可以完全參考官網(wǎng)文檔的 React 示例,只需要傳入一個 GraphQL 服務(wù)的 url 即可。但是對于小程序,只穿 url 會報錯,原因是,對于小程序,缺少默認(rèn)的全局 fetch 函數(shù),因此在生成 GraphQL 客戶端實例時,要額外傳遞自定義的 fetch。當(dāng)然,對于使用了 Taro js 的項目,只需要將 Taro.request 封裝一下就好。
從而最終的 GraphQL 客戶端實例的生成是這樣的:
import {ApolloClient, HttpLink, InMemoryCache} from "@apollo/client"import Taro from "@tarojs/taro"export const client = new ApolloClient({link: new HttpLink({uri: `https://uniheart.pa-ca.me/proxy?url=${encodeURIComponent('https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql')}`,async fetch(url, options) {const res = await Taro.request({url: url.toString(),method: 'POST',header: {'content-type': 'application/json'},data: options?.body,success: console.log})return {text: async () => JSON.stringify(res.data)} as any}}),cache: new InMemoryCache()})
挑戰(zhàn)二:允許小程序訪問 GraphQL 服務(wù)
從上面的代碼中可以看到這個 URL:https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql,這就是上一篇文章《使用萬能 BFF,將語雀文章 GraphQL 服務(wù)化
》的最終成果,它將語雀博客作為數(shù)據(jù)源,通過 AWS lambda 暴露成為一個 GraphQL 服務(wù)。
而這個長長的 URL
https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql 就是利用 serverless 自動創(chuàng)建的 AWS API Gateway 的默認(rèn) URL。
直接使用 Taro.request 訪問它,在打包成為小程序后,執(zhí)行到這里就會報錯,原因是沒有把這個域名配置在白名單里。
這可以嘗試通過小程序后臺配置 request 合法域名解決:

挑戰(zhàn)三:域名備案
一個偷懶的做法,就是將 AWS API Gateway 的默認(rèn)域名填進去,結(jié)果發(fā)現(xiàn)通不過域名備案檢查!

挑戰(zhàn)四:Serverless 自定義域名
當(dāng)然沒有辦法給 AWS 生成的域名去備案,但是可以不要用 AWS API Gateway 自動生成的域名,而是指定一個自定義域名,將這個自定義域名備案。
由于我們的 lambda 使用了 serverless 框架自動化,要使用自定義域名,可以簡單地通過增加一個 serverless 插件:serverless-domain-manager 來幫助我們自動關(guān)聯(lián)這個自定義域名。利用這個插件,可以自動生成 AWS Route 53,以及關(guān)聯(lián)相應(yīng)的 Gateway。
然而,真要這樣做,需要去 AWS 上購買域名,或者將自己的域名過戶到 AWS 的控制臺。這……
總之看起來要使用自定義域名,不那么友好,可能還需要產(chǎn)生額外的費用,那這個就沒意思了。
挑戰(zhàn)五:轉(zhuǎn)發(fā) GraphQL 請求
出于節(jié)省成本的考慮,以及盡可能最大化復(fù)用已有服務(wù),決定使用轉(zhuǎn)發(fā) GraqphQL 請求的方式。這里介紹下前情提要,我早些年備案了一個域名:pa-ca.me,并且在這上面部署了一個服務(wù):https://uniheart.pa-ca.me ,該項目源代碼在這里:https://github.com/Jeff-Tian/alpha。
聽說有的大神,可以盲寫代碼直接上線一次過,沒有 BUG。這真令人羨慕,不過我今天也感受了一次一把過,即給已有項目 https://github.com/Jeff-Tian/alpha 添加了一個轉(zhuǎn)發(fā) GraqphQL 請求的新功能,一次提交,自動發(fā)布后就可以使用了,效率實在令自己滿意:https://github.com/Jeff-Tian/alpha/commit/e58dcf0e7f80643b192561e795bbb3cf050993fd。至今沒有發(fā)現(xiàn) BUG,是真的很神嗎?其實不是,只是有一個好習(xí)慣而已:
測試先行
這個已有項目是基于 eggjs 的,eggjs 其實還是有些坑的,即在發(fā)送請求時,有一個 contentType 選項,對于發(fā)送 POST 請求(GraphQL 查詢本質(zhì)上是一個 HTTP Post 請求),我相信多數(shù)開發(fā)都會自然設(shè)置 contentType = "application/json",但是在 eggjs 生態(tài)里,這樣設(shè)置是沒有效果的,會導(dǎo)致 GraphQL 服務(wù)收不到請求 body。這一次新功能的添加,之所以一次部署就能過,其實是因為在改代碼前,先寫好了自動化測試。即先寫好了一個期待的轉(zhuǎn)發(fā)功能的正確表現(xiàn),然后運行,讓測試失敗。然后寫實現(xiàn)代碼,再次運行測試,直到測試通過。這其中并沒有想象中的那么順利,需要嘗試各種請求選項的設(shè)置,知道找到一個(或者幾個)能夠工作的設(shè)置組合。自動化測試的好處是讓我的嘗試可以很快得到驗證。
測試用例
const graphql = async () => {const res = await app.httpRequest().post(`/proxy?url=${encodeURIComponent('https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql')}`).type('application/json').send({ query: '{ \n yuque(id: "53296538") {\n id\n title\n description\n \n }\n \n allYuque {\n nodes {\n id\n title\n }\n }\n}', variables: null }).expect(200);assert.strictEqual(res.body.errors, undefined);assert(res.body.data.yuque.title === '快速下載 GitHub 上項目下的子目錄');};it('should proxy graphql', graphql);
最終實現(xiàn)
subRouter.post('/', controller.proxy.proxy.post);public async post() {const { ctx } = this;const { data } = (await ctx.curl(ctx.query.url, {streaming: false,retry: 3,timeout: [ 3000, 30000 ],method: 'POST',type: 'POST',contentType: 'json',data: ctx.request.body,dataType: 'json',}));ctx.body = data;}
可見這個 contentType 必須設(shè)置成 json,才能觸發(fā) ctx.curl 以及其底層的 urlib 自動將 header 中的 content-type 設(shè)置成 application/json !
至此,就解釋清楚了挑戰(zhàn)一中,為什么生成 apollo 客戶端實例時,會有一個 proxy 的 url 出現(xiàn)了。這一切彎彎繞繞都是因為小程序的限制,如果你足夠有錢,可以接受在 AWS Route 53 里再申請一個域名,那么這一切可以得到一些簡化。
總結(jié)
本文給萬能 BFF 最終在前端的使用舉了一個例子,詳解了如何在小程序中接入 GraphQL。因為利用了 TaroJs,所以同步部署了一個 Web 端:https://taro.pa-ca.me。Web 端的集成只需要參考 Apollo 官方文檔,因此沒有贅述其實現(xiàn),而是找了一個相對更難的實現(xiàn)方案:微信小程序。這不僅彌補了官方示例的空白,而且體現(xiàn)了中國特色。
