在小程序里接入 GraphQL
共 4833字,需浏览 10分钟
·
2021-10-03 22:34
背景
在前几篇文章里,一直在讲 GraphQL。分别是:
《一顿操作猛如虎,部署一个万能 BFF》,利用 Gatsjs 开发用的 GraphQL 服务器,结合其丰富的数据源插件,部署了一个几乎万能的 BFF 层。
《使用万能 BFF,将语雀文章 GraphQL 服务化》,万能 BFF 层的实例,利用现有插件,直接把语雀的服务 GraphQL 化了。
BFF 是 Backend For Frontend 的简称,是为前端服务的后端。要发挥真正的用处,还得通过前端体现。现在就给个实例,讲解如何在小程序里集成万能 BFF。
前端主要有 Web、小程序以及 Native App 等。要在前端集成 GraphQL,一般采用 Apollo Client。为什么本文选择小程序作为例子呢?因为小程序是中国特色,国外没有。对于如何在 Web 端或者 Native App 端集成 GraphQL,只需要按照 https://www.apollographql.com/docs/ 官方文档的指导去做即可。
采用小程序作为例子,不仅弥补了官方文档的空白,而且由于小程序的一些限制,在集成 GraphQL 的过程中,还面临一些额外的挑战。因为更困难,所以更加符合本公众号(哈德韦,即 Hard Way 的音译)的初衷。
最终成果演示
Web 版:https://taro.pa-ca.me/
微信小程序(体验版):
可以微信扫码打开小程序体验版(由于还没有发布,因此只能以体验的形式),申请体验。我看到申请请求后会第一时间通过,有 100 名的限制哦,如果因为人数超限不能体验,那么请等待我的下一篇文章,如果哈德韦微信小程序正式发布上线,我会再发文通知大家。
项目源代码
https://github.com/Jeff-Tian/weapp
Taro Js
尽管这里只做了微信小程序,但是采用了多端统一开发框架 Taro Js,从而可以打包到不同的平台。
挑战一:在小程序里生成 Apollo Client 实例
基本可以参考官方文档的 React 示例,但是对于小程序,却不能照搬。如果只做 Web 端,可以完全参考官网文档的 React 示例,只需要传入一个 GraphQL 服务的 url 即可。但是对于小程序,只穿 url 会报错,原因是,对于小程序,缺少默认的全局 fetch 函数,因此在生成 GraphQL 客户端实例时,要额外传递自定义的 fetch。当然,对于使用了 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()
})
挑战二:允许小程序访问 GraphQL 服务
从上面的代码中可以看到这个 URL:https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql,这就是上一篇文章《使用万能 BFF,将语雀文章 GraphQL 服务化
》的最终成果,它将语雀博客作为数据源,通过 AWS lambda 暴露成为一个 GraphQL 服务。
而这个长长的 URL
https://jqp5j170i6.execute-api.us-east-1.amazonaws.com/dev/gatsby/graphql 就是利用 serverless 自动创建的 AWS API Gateway 的默认 URL。
直接使用 Taro.request 访问它,在打包成为小程序后,执行到这里就会报错,原因是没有把这个域名配置在白名单里。
这可以尝试通过小程序后台配置 request 合法域名解决:
挑战三:域名备案
一个偷懒的做法,就是将 AWS API Gateway 的默认域名填进去,结果发现通不过域名备案检查!
挑战四:Serverless 自定义域名
当然没有办法给 AWS 生成的域名去备案,但是可以不要用 AWS API Gateway 自动生成的域名,而是指定一个自定义域名,将这个自定义域名备案。
由于我们的 lambda 使用了 serverless 框架自动化,要使用自定义域名,可以简单地通过增加一个 serverless 插件:serverless-domain-manager 来帮助我们自动关联这个自定义域名。利用这个插件,可以自动生成 AWS Route 53,以及关联相应的 Gateway。
然而,真要这样做,需要去 AWS 上购买域名,或者将自己的域名过户到 AWS 的控制台。这……
总之看起来要使用自定义域名,不那么友好,可能还需要产生额外的费用,那这个就没意思了。
挑战五:转发 GraphQL 请求
出于节省成本的考虑,以及尽可能最大化复用已有服务,决定使用转发 GraqphQL 请求的方式。这里介绍下前情提要,我早些年备案了一个域名:pa-ca.me,并且在这上面部署了一个服务:https://uniheart.pa-ca.me ,该项目源代码在这里:https://github.com/Jeff-Tian/alpha。
听说有的大神,可以盲写代码直接上线一次过,没有 BUG。这真令人羡慕,不过我今天也感受了一次一把过,即给已有项目 https://github.com/Jeff-Tian/alpha 添加了一个转发 GraqphQL 请求的新功能,一次提交,自动发布后就可以使用了,效率实在令自己满意:https://github.com/Jeff-Tian/alpha/commit/e58dcf0e7f80643b192561e795bbb3cf050993fd。至今没有发现 BUG,是真的很神吗?其实不是,只是有一个好习惯而已:
测试先行
这个已有项目是基于 eggjs 的,eggjs 其实还是有些坑的,即在发送请求时,有一个 contentType 选项,对于发送 POST 请求(GraphQL 查询本质上是一个 HTTP Post 请求),我相信多数开发都会自然设置 contentType = "application/json",但是在 eggjs 生态里,这样设置是没有效果的,会导致 GraphQL 服务收不到请求 body。这一次新功能的添加,之所以一次部署就能过,其实是因为在改代码前,先写好了自动化测试。即先写好了一个期待的转发功能的正确表现,然后运行,让测试失败。然后写实现代码,再次运行测试,直到测试通过。这其中并没有想象中的那么顺利,需要尝试各种请求选项的设置,知道找到一个(或者几个)能够工作的设置组合。自动化测试的好处是让我的尝试可以很快得到验证。
测试用例
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);
最终实现
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 必须设置成 json,才能触发 ctx.curl 以及其底层的 urlib 自动将 header 中的 content-type 设置成 application/json !
至此,就解释清楚了挑战一中,为什么生成 apollo 客户端实例时,会有一个 proxy 的 url 出现了。这一切弯弯绕绕都是因为小程序的限制,如果你足够有钱,可以接受在 AWS Route 53 里再申请一个域名,那么这一切可以得到一些简化。
总结
本文给万能 BFF 最终在前端的使用举了一个例子,详解了如何在小程序中接入 GraphQL。因为利用了 TaroJs,所以同步部署了一个 Web 端:https://taro.pa-ca.me。Web 端的集成只需要参考 Apollo 官方文档,因此没有赘述其实现,而是找了一个相对更难的实现方案:微信小程序。这不仅弥补了官方示例的空白,而且体现了中国特色。