为什么要使用GraphQL和Falcor?

banq 16-04-04
                   

REST的JSON格式是现在流行的通讯数据格式,但是在Reactive运动如React.js等前端新技术的推动下,以及面向函数编程概念的普及,人们发现JSON已经不再满足需要了。

GraphQL和Falcor是对REST的JSON的进化发展,GraphQL是一种嵌套的有层次的JSON,如果说JSON代表一个对象,那么GraphQL代表的是父对象和子对象的这样有主次层次关系的聚合对象,如果你知道DDD,那么,后端的一个聚合根可以使用一个GraphQL来传递,一次性将一个聚合根打包后在前后端一次全部传输完毕,这样比分解成一个个JSON显然节省网络开销。

以下是GraphQL的格式,父领域模型下可以有子领域模型:

{
domain: [],
subdomain: [
[‘members’, {}, []],
[‘messageCount’, {}, []]
]
}


更重要的是,对GraphQL的一次性的调用方式与JSON多次调用相比,显然GraphQL一次调用是属于一种声明式调用,而JSON的多次调用属于命令式调用,声明式调用优于命令式调用的原因也在于此,GraphQL一次性调用属于函数的Monad单子,也就是通过一种声明类似SQL语句的调用方式可以一次性实现对数据集合的处理。

What’s the Point of GraphQL and Falcor? — Medium一文对此进行了详细阐述,下面大意翻译一下:

React.js越来越流行的一个主要原因是它的自然的声明式风格,所有你需要做的就是指定你要的DOM结构,React将会以最高效率方式只计算更新DOM的更新部分,而不是更新整个DOM(为什么更新整个DOM会很慢?因为DOM是一个庞大的树形结构,一旦任何一个节点更新,会触发父子关系节点相关的无数个监听器,如同一颗地雷引发整个地雷阵大爆发)。

React这种方式不只是考虑性能问题,还是因为其遵循函数编程概念,使得你编程变得更有逻辑,带来更多的好处。

同时大家也意识到无态React组件的好处,当整个用户界面来自一个不可变状态的计算时,整个应用的逻辑就更容易遵循,从而变得有序,组件变成可重用,另外一个抽象是time-traveling 时间旅行调试器undo/redo都变得容易实现。

我们的应用程序从而变成无态,可变的状态被限定在一个单个的序列化对象中,不会扩散到整个应用代码的各个本地变量中,也就是说,本地变量不再零碎地想使用哪个变化数据就使用了,没有必须使用可变变量,而都是使用不变量或常量。

虽然,可变状态被限定在一个可序列化对象中,但是这个对象还在我们的应用中,如同一个定时炸弹,一泡鸡屎坏一缸酱,整个应用其实还是有状态的,因为必须进行从后端抓取这个可序列化的对象。

可能难以想象这种抓取不是声明式的,你会经常在React看到下面发出HTTP请求代码:

fetchData: function(query) {
this.setState({loading: true})
fetch(url, {method: ‘get’})
.then((response) => response.json())
.then((data) => this.setState({data, loading: false}))
}

这是从后端抓取JSON数据包,然后转为可序列化的可变状态对象放入到React的可变状态中。

这种方式非常琐碎,涉及太多处理与后端通讯的细节,如果我们换一个思路,通过WebSocket对后端资源进行订阅或取消订阅如何?无论是否使用高阶函数概念:


componentWillMount: function() {
this.sub = Meteor.subscribe(‘chatroom’, this.props.roomId)
},
componentWillUnmount: function() {
this.sub.stop()
}

这是直接向后端订阅一个指定ID值的聊天室房间资源,取消订阅是释放这个聊天室资源。

这种方式其实很自然,如果你熟悉React编程,会发现在React中操作DOM也是以类似方式进行:

function newTodo(text) {
var todo = document.createElement(“span”)
todo.textContent = todo
document.getElementById(‘todos’).appendChild(todo)
}

这种方式其实是一种声明式编程风格,通过声明你想要什么即可,而不会涉及到想要东西的内部细节,类似一条SQL语句能够获得所有我们想要的数据,至于如何获取这些数据是数据库职责。

命令方式抓取数据好像很方便,在需要时发出命令,比如在渲染之前以命令方式抓取后端数据,但是这种抓取会和React的渲染功能等无态组件紧耦合在一起,我们应该为每个组件专门声明地它们需要的数据(而不是在组件运行过程中随意抓取),分析组件树形结构来计算我们是否需要抓取一些数据,比如订阅某个聊天室或取消订阅它。

当将抓取数据这种副作用从React组件中迁移出去(通过专门声明方式迁移出去),我们就能减少重复请求的可能,缓存请求,或使用高阶函数订阅,也不会前后发送单独的大量HTTP请求,完全可以将这些多次请求放在一起一次性发出一个请求,一次请求获得所有需要的数据。

这样方式也提供后端数据库查询效率,节省网络开销,比如一个用户资料的组件只需要用户名字段,用户资料的子模型有一个图片字段,那么我们还需要对同样的该用户发出抓取图片字段的请求,我们应该将这两个请求合并在一个数据库查询。

从这个用户资料的思路我们推广到整个视图组件中,如果我们增加或移除组件时,无需考虑跟踪这些组件所需要的后端数据资源,那么类似用户资料情况的这种抓取做法无疑是一大进步。

这里就是GraphQL的动机

假设组件有一个“domain”请求和"subdomain"请求(这两个请求的领域模型其实是主从关联关系,组合或聚合关系),那么一个领域模型的请求将是高层次的请求,比如用户资料请求,不仅仅包含用户资料,还有其子对象图片资料的数据,这些子领域请求被称为“fragments碎片” 或“edges边缘”.

使用这个模式我们能够以声明方式为每个组件指定领域模型和子领域模型的请求,这非常类似我们与虚拟DOM打交道一样,将这些请求组合起来变成高效的后端数据库查询,可以使用Monad单子来代表这种从组件角度的主从领域模型请求方式,一个无数据的请求方式类似:


{ domain: [], subdomain: [] }


一个聊天室组件就可以有顶级的领域请求,返回如下数据格式:

{
domain: [
[‘chatroom’, {roomId: 42}, [
[‘id’, {}, []],
[‘name’, {}, []]
]]
],
subdomain: []
}


那么关于聊天室包含创建者的用户资料等多中高聚合数据获得可以如下:

{
domain: [
[‘chatroom’, {roomId: 42}, [
[‘id’, {}, []],
[‘name’, {}, []],
[‘members’, {}, []],
[‘messageCount’, {}, []],
[‘messages’, {limit: 50, skip: 0}, [
[‘id’, {}, []],
[‘text’, {}, []]
[‘owner’, {}, [
[‘id’, {}, []],
[‘name’, {}, []]
[‘image_url’, {size: ‘sm’}, []]
]]
]],
]]
],
subdomain: []
}


下一步挑战就是,当我们将这种数据格式通过请求发送到后端服务器,如何转换为数据库的查询呢?Datomic可以实现这个目标。

我们也可以考虑一次性在客户端抓取所有的数据,Falcor提供了一种基于JSON图的简单实现。

Falcor类似GraphQL,是将JSON数据作为一种图结构数据,所谓图结构也是类似树形结构的数据,Falcor 有路径数据集用来声明数据抓取,而GraphQL似乎更注重查询的组合,与React结合比较紧密。

更灵活的方案是DataScript,客户端缓存将可能是另外一场挑战。

无论Falcor或GraphQL,在理论上都属于一种DSL(领域规定语言domain specific language),它们的语法并不难理解,如果你理解业务。

更多参考原文:

What’s the Point of GraphQL and Falcor? — Medium

                   

1