ReactEurope 2016 小记 - 下

ReactEurope 2016 小记 - 下

诚身诚身

GraphQL at Facebook - Dan Schafer

相信各位熟悉 React 生态的朋友都应该听说过 GraphQL 的大名。Facebook 从 2012 年开始使用 GraphQL,迄今为止 GraphQL 已经成为了 Facebook 整体技术栈中不可或缺的一环,日均服务调用次数超过 3,000 亿次。

来自 GraphQL 团队的 Dan Schafer 向所有听众从以下三个方面介绍了 Facebook 是如何在工程中使用 GraphQL 的。对于 GraphQL 还不是很了解的朋友,推荐先阅读一下本专栏中另一篇介绍 GraphQL 的文章,相信会对你理解本文以下的内容大有帮助。


How do I implement authorisation?(授权)

在回答关于授权的问题之前,Dan 提出了一个非常有趣的问题,那就是我们如何去定义一个数据模型,或者说什么是一个数据模型(举例:在一个简单的 todoMVC 中什么是一个待办事项)?

从目前最流行的 REST 风格的服务架构角度来讲,一个数据模型就意味着一个 URL,如:
http://api.todoapp.com/todo/4

而从传统的 SQL 数据库角度来讲,一个数据模型就意味着一条 SQL 语句,如:

SELECT * FROM todoitems WHERE id = 4

以上的这两种答案都没有错,但他们也都存在着各自的问题。我们一起来看下面的三张图:

如果我们在外部接口的层面上去做授权,那么需要做的工作就太多了,因为每一个接口都需要鉴权这一环节。而如果我们在数据存储这一层上去做授权,我们又暴露了太多数据存储方面的细节,而这些具体的实现逻辑是客户端所毫不关心的。这个问题的解法就存在于后面的两张图中,那就是在业务逻辑层去做授权。笔者见过太多的中小型公司囿于各方面资源的匮乏,很难建立起独立的业务逻辑层,只是简单地将后台工程师们做好的表中的数据通过接口的方式暴露出来。这样的架构当然可以支撑起一个完整的项目,但整个项目的可伸缩性与复用性几乎为零,仅从结果上来讲毫无疑问是一种非常糟糕的架构。从第三张图中,我们看到了 Facebook 极为推崇的以业务逻辑为核心的架构方式,GraphQL 不过是包在核心业务逻辑之上的一层接口而已。而这样的架构方式不仅可以应用于软件工程中,也可以拓展到公司的组织架构层面,当一家公司的业务处于极速膨胀期时,各个业务部门(一个业务部门是许多业务逻辑的总集)之间的协同配合就变得异常重要,如何灵活地复用现有的业务资源,才是满足日渐膨胀的业务需求的唯一解法,遍地竖烟囱的时代显然已经过去了。

在这里我们又要提到三条在 GraphQL 的设计中非常重要的理念:

  • Think Graphs, not Endpoints(专注于数据之间的关系,而不是使用数据的过程及终点)
  • Single Source of Truth(单一数据来源,稳定的数据存储层)
  • Thin API layer(不涉及任何业务逻辑,适用于任何业务系统)

以下的所有内容都是围绕着以上这三点设计理念展开的,理解了这三点理念,即使你不在日常生产中使用 GraphQL,你也已经学到了它的精粹。

回到授权的问题上来,我们来看如下代码:

class TodoItem {
  // Single source of truth for fetching
  static async gen(viewer: Viewer, id: number): Promise<?TodoItem> {
    const data = await Redis.get("ti:" + id);
    if (data === null) return null;
    // Single source of truth for authorisation
    const canSee = checkCanSee(viewer, data);
    return canSee ? new TodoItem(data) : null;
  }
}

function checkCanSee(viewer: Viewer, data: Object) {
  return (data.creatorID === viewer.userID);
}
是的,当我们建立了独立的业务逻辑层后,我们也就有了业务数据的单一数据源,然后我们就可以在 GraphQL 中实现带有授权机制的数据查询了。
todoItem: {
  type: TodoItemType,
  args: { id: { type: GraphQLId } },
  resolve: (obj, {id}, viewer) => TodoItem.gen(viewer, id)
}

// ...
const viewer = Viewer.fromAuthToken(request.auth_token);
graphql(schema, "{todoItem(id: $id)}", viewer);
How do I make GraphQL efficient?(效率)

假设如下的场景,用户需要查询自己前五位好友的最亲密好友。

如果我们不对请求过程做任何优化的话,我们需要先发送 5 个独立的请求去查询自己的前五位好友,然后再分别发送 5 个请求去查询他们各自的最亲密好友。但事实上,以上的这 10 个请求是分两次同时发生的,我们是否能够将他们合并成为 2 个独立的请求呢?

当然可以,但实现这一切的魔法并不来自于 GraphQL,而是 Facebook 的另一个开源项目 DataLoader。DataLoader 接收一个函数,这个函数接收一个用户 id 的列表(在以上的场景中),并返回异步的数据。至此,我们对于请求过程的优化就结束了,DataLoader 会处理接下来的所有事情。它将延迟发送这些独立的请求,合并相同的请求,最后一次性发送给服务端。而且,DataLoader 还会缓存已经发送过的请求,合并请求后并不会向服务端重复发送的已经缓存过的请求。看起来 DataLoader 已经帮我们解决了绝大多数的问题,但我们还有最后一个需求,那就是如果客户端也希望缓存这些数据,应该如何去做呢?

How do I cache my results?(缓存)

在 GraphQL 中,请求结果与 URL 之间并不存在严格的一一对应的关系,所以我们无法使用简单的 HTTP 缓存。为了实现类似的功能,GraphQL 需要做到以下的三件事:

  • Global Unique Cache Key(全局唯一的缓存键)
  • Refetch Identifier(再次请求时的标识符)
  • Opaque to Clients(客户端无感知)

解决以上的三个问题并不算困难,GraphQL 使用添加业务前缀的方式来保证缓存键的全局唯一性,再使用 base64 的方法对这些缓存键进行加密,以达到客户端无感知。综上,当客户端将一个 base64 加密过的无业务意义的键值传回后,GraphQL 就可以根据这个键值再次获取数据,代码如下所示:

const userType = new GraphQLObjectType({
  name: 'User',
  fileds: () => ({
    id: {
      type: GraphQLID,
      resolve: (user) => base64("user:" + user.getID())
    }
  })
});

讲到这里,我们基本已经覆盖到了 Dan 演讲中的绝大多数内容,简单地谈一下笔者的个人感受。

应该说 GraphQL 是 React 生态中离前端最远的一个领域,各位读者从上面的介绍中应该也可以看出 GraphQL 的核心在服务端,GraphQL 对服务端提出了比 REST、RPC 等传统标准更加严格的要求,以使得服务端可以满足客户端按需取数的需求。也许我们在很长的一段时间内都无法将 GraphQL 应用于生产环境中,也无法享受到服务端返回的数据结构与客户端渲染所需要的数据结构相同的便利,但 GraphQL 团队却成为了我最为敬佩的一个团队,因为只有他们实现了从前端痛点反推后端技术,并解决了在 Facebook 内部让整个公司都感到极为痛苦的多维数据(在社交网络的数据世界中,经常会出现十维以上的数据)获取难题。

Software engineering is all about teamwork and teamwork is all about making your teammates better players.

On the Spectrum of Abstraction - Cheng Lou

当我们在讨论软件工程时,我们在讨论些什么?来自 React 团队的华裔工程师,react-motion 的作者楼成对于这个问题的回答是:抽象。

但从另一方面来讲,软件工程师的最终任务却是产出具象的产品,抽象不过是过程中的一个方法,所以抽象到什么级别,如何抽象就变得异常重要。而且我们还要时刻牢记,抽象是要付出代价的,过度的抽象会增加使用者的认知成本(mental overhead),让使用者不得不花费更多的力气将一个抽象的框架/库具象到最终产品的级别,而最终产品的具象程度又与终端用户的体验直接相关,可以说最终产品的具象级别是非常高的,或者说最终产品不应该具有任何程度的抽象,这样才能保证用户来到这个页面就知道如何去完成自己想做的事情。

楼成用了 react 和使用 react 生产的最终产品来举例,react 很强大(powerful)因为它可以覆盖到几乎所有需要跟 DOM 打交道的应用场景,但另一方面,react 却一点用也没有,因为终端用户不可能去使用如此抽象的产品。使用 react 生产的最终产品却十分有用(useful),但它们并不强大,只能覆盖到一些特定的含有业务逻辑的用户场景。

Principle of Least Power: Use least powerful tool to build your concrete products.

正如软件工程师们每天都会遇到的 library 与 framework 之争(举例:jQuery vs. React),其实类似争论的答案都取决于你到底需要将需求抽象到什么级别。如果只是为了满足在某些特定场景下的应用,那么 library 可能是一个更好的选择,因为它可以直接拿过来用,快速地解决你所遇到的问题。假设我只是需要拿到一个独立表单中所有 input 框的数据,那么我大可以直接使用 jQuery 的选择器来取值,而不需要先抽象出许多 input 控件,然后再挨个为它们绑定 onChange 事件(不要忘了,即使你简单地抽象出了 input 控件,为了使用它你还需要继续抽象出表单控件等等)。

Lots of problems arise from a bad understanding of where we should be in the levels of abstraction.

如果我们一开始就在业务的抽象层级上站错了队,那么我们将人为地给自己创造出许多不必要的困难,也就违反了上面提到的 Principle of Least Power。无论何时,软件工程的最终目的都是创造出独一无二的产品,抽象与独一无二无疑是完全背道而驰的两种理念,而这也是软件工程师与产品经理之间的最大矛盾所在。

在我们了解上述关于抽象的理念后,我们来看几个案例。

Grunt vs. Gulp

Grunt 与 Gulp 之间的竞争,本质上来讲是 declarative DSL(声明式领域专用语言) 与 build system(构建系统)之间的竞争,而从软件工程这么多年的发展来看,build system always wins. 如果说 DSL 是 do one thing and do it well,那么在前端工程化需求日益膨胀的今天,就显得越来越不够用了,而在 DSL 的基础上去改造或扩展它,成本又异常高昂,所以 build system(在函数层级抽象) 获得最终的胜利就只是时间问题了。

React vs. Templates

是的,React 将 JavaScript 注入了 HTML 中,但 React 并不是一个模板引擎,因为二者在抽象的层级上是不同的。React 是在函数层级上的抽象,而模板引擎则是在数据层级上的抽象,所以模板引擎使用起来更方便,函数支持的场景更多也更灵活。

Diffing on View vs. Diffing on (Model + Computation)

虽然函数层级的抽象更灵活,但它却几乎没有任何优化的空间,也没有配置的可能,所以 React 将优化的工作都放在了最终的视图层面,也就是我们经常提到的 virtual DOM diff. 而实现 virtual DOM diff 的前提是,在数据层面的抽象,因为数据是可比较的,而函数是不可比较的。

Immutability vs. Mutability

毫无疑问,可变的数据更强大,能做更多的事情,但这并不是前端工程中的重点。在前端工程中,不可变的数据可以帮助我们写更少的代码去实现诸如时间旅行、数据持久化、shouldComponentUpdate 等等这些功能,在这一需求中,我们需要的是更加具象的实现。

JS(Inline) CSS vs. Traditional CSS

传统的 CSS 有很多优点,比如写起来很容易,每个属性也都很具体,可以直接影响到 DOM 元素的表现形式。但正是因为传统的 CSS 如此具象,它也丧失了很多灵活性,以至于为了实现某些复杂需求,我们不得不在 CSS 内部做许多的 HACK。JS CSS 给予我们的,是一个编程语言(Programming Language)所具备的所有特性,让我们可以使用更强大的工具去完成复杂的需求,而不再需要在 CSS 内部去做 HACK。

在演讲的最后,楼成还总结了四条在开发过程中的个人经验:

  • Don’t cover every use-case
  • Repeating your code is fine
  • Don’t be swayed by elegance
  • When in doubts, use examples

在这里限于篇幅,而且以上的这四条经验实在有些太“湿”了,相信每个开发者所遇到的情况都不尽相同,所以我们就不一一展开讨论了。

Code is work, architecture is experience, abstraction is art.

小结

至此,我们的 React Europe 2016 之旅就暂时告一段落了,感谢大家一路的陪伴,也希望大家可以从这些业界大牛的演讲中吸取到一些优秀的解决问题的思路。

最后,我想借用楼成演讲中的一句话来总结我们的这两篇文章:

最强大的工具都是抽象且无用的,正如我的这个演讲,也许它是整个 React Europe 中最无用的一个。

但我想,楼成所提到的关于抽象层级的思考方式却可以渗透到我们日常工作中的方方面面。HACK 永远只是一时之策,真正考验软件工程师(架构师)能力的还是如何在问题发生之前预料并解决掉它。对于优秀的软件工程师而言,写代码是一件非常容易的事情,如何将具象的业务需求抽象为与业务无关的技术方案才是真正的难点。

如果读者在抽象需求方面有非常好的最佳实践,也欢迎在专栏下留言,我们可以一起探讨。

======

感谢您的阅读,React Europe 2017 我们再见!

文章被以下专栏收录
9 条评论