背景

在前几篇文章里,一直在讲 GraphQL。分别是:

BFF 是 Backend For Frontend 的简称,是为前端服务的后端。要发挥真正的用处,还得通过前端体现。现在就给个实例,讲解如何在小程序里集成万能 BFF。

前端主要有 Web、小程序以及 Native App 等。要在前端集成 GraphQL,一般采用 Apollo Client。为什么本文选择小程序作为例子呢?因为小程序是中国特色,国外没有。对于如何在 Web 端或者 Native App 端集成 GraphQL,只需要按照 https://www.apollographql.com/docs/ 官方文档的指导去做即可。

image.png

采用小程序作为例子,不仅弥补了官方文档的空白,而且由于小程序的一些限制,在集成 GraphQL 的过程中,还面临一些额外的挑战。因为更困难,所以更加符合本专栏(哈德韦,即 Hard Way 的音译)的初衷。

最终成果演示


  • Web 版: https://taro.pa-ca.me/
  • 微信小程序(体验版):
    basicprofile.jpeg
    可以微信扫码打开小程序体验版(由于还没有发布,因此只能以体验的形式),申请体验。我看到申请请求后会第一时间通过,有 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 客户端实例的生成是这样的:

typescript 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 合法域名解决:

image.png

挑战三: 域名备案

一个偷懒的做法,就是将 AWS API Gateway 的默认域名填进去,结果发现通不过域名备案检查!

image.png

挑战四: 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。只一次新功能的添加,之所以一次部署就能过,其实是因为在改代码前,先写好了自动化测试。即先写好了一个期待的转发功能的正确表现,然后运行,让测试失败。然后写实现代码,再次运行测试,直到测试通过。这其中并没有想象中的那么顺利,需要尝试各种请求选项的设置,知道找到一个(或者几个)能够工作的设置组合。自动化测试的好处是让我的尝试可以很快得到验证。

测试用例


typescript

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 idn titlen descriptionn n }n n allYuque {n nodes {n idn titlen }n }n}, variables: null }) .expect(200);

assert.strictEqual(res.body.errors, undefined);
assert(res.body.data.yuque.title === 快速下载 GitHub 上项目下的子目录);

};

it(should proxy graphql, graphql);

最终实现

typescript 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 官方文档,因此没有赘述其实现,而是找了一个相对更难的实现方案:微信小程序。这不仅弥补了官方示例的空白,而且体现了中国特色。