松志

WEB未来(译)

时至今日,web技术似乎对实时、可交互的应用提供了良好的支持。是否果真如此?本文试图就此问题展开讨论:什么是当前web技术所缺失的,有什么现成的工具能使web更强大!

服务端并非必需

一般而言,传统web服务由数据库(存储数据)、服务器(提供计算能力)和浏览器(下文与「客户端」一词混用)组成,三者通过RPC或REST接口紧密耦合。

图片:传统web服务架构图

如今,浏览器端的JavaScript(以下简称"JS")已经能胜任绝大多数本来由服务器端运行的业务逻辑了。JS用来处理界面逻辑(比服务器端处理更适合),与数据库(以下简称"DB")连接并执行查询和解析查询结果都不在话下。

"嗨,程序员,当你写的后端代码只是用来代理浏览器端查询到DB,再为其返回查询结果时,有没有觉着这样挺愚蠢的?"作者是觉得这层代理有点多余。 ~isomorphic code(同构代码)~、~compile-to-js(编译到JS)~、~node.js~,所有这些技术都出于同一种理念:在不同的地方(服务器端和浏览器端)运行相同的代码。这样的目标其实是错的,真的有必要把同样的代码运行在不同的地方么?如果你确实需要这样做,那也是出于传统的、陈旧的架构所限制而不得已。

其实过去,图形应用都是直接连接DB的,后来才引入了瘦客户端的概念。为了减轻浏览器端业务逻辑,web技术发展出了代理(例如CGI)或服务端技术。如今,浏览器端技术已经足够强大,可以轻松承担更多业务逻辑。只是因为技术沿袭,服务器端端才能依旧如此举足轻重!

最终,浏览器会直接连接数据库,所谓的"软件"将不复存在,这只是时间长久的问题!不像现在,服务器端是任何应用的必要组件,到那时,客户端运行所有业务逻辑。JS发号施令,服务器/DB 按需响应。

能否避免延迟?

虽然客户端可以直接连接数据库,然后交由浏览器完成所有事情,但是我们还是很容易陷入到一种临时、不一致和处于延迟的web应用中。

看看Facebook的主页:

图片:脸书页面状态分布

如果打开这个页面几分钟,你会发现这个页面不同部分的数据具有不同的"鲜度"。有些部分是静态不更新的(除非你刷新浏览器),有些数据几乎是实时更新的,还有一部分是定期更新的。甚至同一数据(比如用户名)因位于页面的位置不同,具有不同的"鲜度"。Facebook作为这个时代最具前沿性的web应用之一,实时性也只能说差强人意。Facebook,还有任何其它web应用都会在最初加载的几秒后变成陈旧和充满延迟的本地应用。看来,真正的实时web应用还未诞生。

或许有人会反驳,web应用是否有必要那么实时。定期刷新网页并无不妥,没人会那么频繁的更改用户名,和对Facebook的喜爱相比,这些小问题根本不算什么吧!

作者完全同意,大众对web实时性的缺失都不痛不痒无所谓。安于现状并不妨碍我们探索和改善,探索那些疯狂和难以企及的角落也许能给我们不同的洞见。

web技术栈是在假定人们只对静态和鲜有更新的数据感兴趣这样的前提下建立起来的。数据的更新伴随着用户的操作才能发生(译者注:想象你提交了订单,然后被带到另外一个网页中)。由此,相伴HTTP,JS,SQL,REST,自然而然诞生了AJAX/WebSockets这样的定期数据更新技术。抛开这些可观的成果,或许我们还可以探索一些似乎对我们而言并不那么自然的技术!

我们真正想要的不单单是一种"请求/响应协议(request-response protocol)",从更高层次上看,我们是想让客户(端)和数据尽可能紧密联系在一起,下面这些要点是我们真正关注的:

  • 对数据变动自始至终连贯的感知。 用户一次性这个得到整个页面,我们却将不同的静态数据拼装在一起,然后在不同地方以不同频率刷新滞后数据,这绝不是我们想要的。只有真正的一致性才能避免用户界面当中矛盾的数据呈现!
  • 持续更新数据。 所有呈现给客户端的数据都应该是当前最新的,哪怕是微乎其微的细节,理想情况下包括任何资源,甚至是应用自身的代码。如果用户上传了一张头像,他期望的是所有引用其头像界面都应该即刻更新,哪怕那是个只有1秒时长的弹出栏。
  • 即时响应。 只要服务端确认了用户的行为,UI应该即刻响应这个本次确认。
  • 能处理网络错误。 物理网络本身并不保证完全可靠的通讯,但是可信赖的协议依然可以建立其上。网络错误、丢包,连接丢失和重复连接不应该破坏系统的一致性。
  • 离线可用。 显然,应用不可能永久在线,但至少应该允许用户离线时能进行本地修改,当网络可用时再合并修改到服务端。
  • 无需管理底层连接。 应用开发人员不应陷入手动处理网络重试和重复连接这样的低级别任务中。这些事情应交由底层库去实现。

在接下来的十年里,我们有可能看到上面提到的某些特性被研究,甚至成为事实标准。

后文是作者针对此目标的一个初步探索,另外,作者并没有一个现成的方法使Facebook成为真正的实时应用,但是我们可以从其它地方开始.

追寻web圣杯

看下面这个问题领域抽象:

图片:数据过滤

最上面是整个应用所有的数据,在这些数据到达用户端前,要通过至少两层过滤器。第一层是安全过滤器,其过滤掉所有未授权给用户的数据,只通过共有和用户私有数据;第二层过滤器筛选出用户感兴趣的数据,比如需要呈现到当前界面上数据。

任何通过这两层过滤的数据都应该即刻推送给客户端,这些数据足以保证客户端界面正常渲染。

这里有两个棘手问题需要指明:

一是效率,web应用页面通常相当复杂,其中可以包含数以千计的对象和复杂查询请求,还有大量聚合操作,更甚,同一时刻还会有不计其数的客户端连接到服务端(DB)。

数据查询可能是未来web领域最值得探索的部分。查询像是从数据库获取数据的菜谱,为了能实时查询到数据,我们需要查询以不同于当前手段的方式实现。客户端依然以"查询"的方式请求数据,就像初始化应用时从服务器获取数据那样,然而,这个初始请求其后可以复用,由过滤器决定整个数据库变动后,哪些数据稍后可以推送给客户端。

图片:数据订阅

看来,我们需要一种新的数据库查询语言。假定它叫"ReversibleQL(可逆SQL)",其能在两个方向上进行查询,因为这个特性,ReversibleQL应该比现在的SQL更简单,也更有局限性。或者,我们需要两种不同的查询语言,谁知道呢!Meteor.js 通过限制'订阅到文档和集合'的方法解决查询难题。RethinkDB试图发明~transparently reversible queries~,但是并没有公开其内部细节。

第二个棘手问题是,过滤器和订阅在一定时间后可能需要被回收,不是出于性能考虑(通常性能不是问题),而是应用管理的问题。客户端方面,我们可以受益于"React"的一个未得到充分赞誉的特性: component lifecycles(组件生命周期)。通过监听~didMount~和~willUnmount~,可靠地管理组件(和它们的订阅者)的生成和销毁才有可能。至于服务端,通过重置定时器、计数器、定时清理和妥善的编程也可解决这个棘手问题。

当数据从DB流动到客户端时,让我们谈论一下可靠性。对于一个基于数据流的应用,如果在传递数据变动时有所疏漏,是万万不能接受的。在重新连接DB时,一个"DELETE"命令的遗失足以造成应用界面的混乱。说到这里,数据库是分布式的,浏览器端只是扮演了一个数据节点的角色。~DB-客户端~同步协议应受益于数据库本身提供的强有力的事件日志,来保证数据同步的可靠性。或者最终,我们会选择分布式CRDTs和Anti-Entropy(反熵)理论。近来,分布式计算实在是太火(有些吵嚷)了,就假定我们已经十分了解它的运作了。浏览器通常不被看成是数据库的一个节点(CouchDB正在朝这个方向努力)。

我们已经讨论了监听 数据/事件 变更,但是还没有谈论如何引发这些变更。显然,每一个本地(在客户端)执行的行为都应该被捕获,再转换成某种形式的变更序列,最后提交到DB,这些对于现在的web技术也不是难事。

难点在于延迟和离线工作。有时网络太慢,为了不影响用户体验我们必须在数据正式提交给服务端前就对用户操作进行反馈!

至于离线,可以看做是某种无限的延迟。离线模式下的应用除了缺乏实时性外,应与其在线状态别无二致,此时,变更队列会无限增长(因为无法向DB提交),但是不影响本地其它操作。

中央管理和应用内状态管理至关重要。如果你把应用状态看作是某种数据存储(例如,Relay存储、嵌套不可变目录或DataScript DB),你就能在应用层面上实现(实时)同步特性。当然,你需要显式地在网络间管理变更:传送、追踪,以及在本地消耗和实施变更。

差强人意的现实

这篇文章的目的更多是启发读者从正确的角度思考web技术的发展。目前作者还未发现任何应用建立在这样的架构上,甚至连一个概念证明也没有。放在两年前,若想自行实现这样的系统想都别想,因为没有任何工具包含上文讨论的特性。直到目前,似乎有那么些还不够严谨的概念已经初见端倪了:

  • RethinkDB 力图实现这样的系统,但是缺乏客户端存储和可信的推送。
  • Relay 支持本地存储和延迟补偿,但是服务端缺乏实时推送。
  • Metor.js 走得更远,提供了延迟管理,本地存储,服务端推送,订阅,数据过滤。但是,作者没有发现更多关于DDP protocol的一致性和可靠性保证方面的信息。

最近,作者看到Clojure社区有巨大的机会可以实现实时应用,那就是Datomic和DataScript:

图片:Datomic 架构图

  • Datomic是一种通用的数据库,但是提供了一份所有数据交易的日志。原子化的交易号保证了同步的可靠性(通过日志复制来同步)。
  • Datomic的公共API支持响应式的交易队列,意味着不用轮询或日志解析即可实现服务端推送。
  • Datomic的数据模型类(datom = 实体,属性,值)似于RDF,具有很好的颗粒性,对于安全和订阅过滤十分有益。Datom是可用来同步信息的最小单位。
  • DataScript在浏览器端模仿Datomic,保证客户端拥有和DB一样的数据模型,使得本地和服务端很好地配合。
  • Datomic和DataScrip共享可序列化的transaction format,因此不必创造临时的队列delta(差量?)。
  • DataScript具有不可变性,意味着客户端可以持久化本地存储(DB)以便应用或撤销,重组、弃用和建立临时存储也一样轻松自如。
  • 从DataScript到React(译者注:Clojure社区的Hoplon框架其实比React更适合搭配DataScript,参见译者的GitHub项目!)的数据流使得响应式UI变得如此便易,当UI需要密集依赖DB中数据的变化时,这种组合尤其简单易写。
  • Datomic架构中唯一的遗憾是缺席一种订阅式语言。Datomic和DataScript使用强大的Datalog查询语言,但是这个语言的弊端是逆向查询并不高效。当有海量的数据交易进出DB时,针对每个原子交易你需要自行判断其变更要传送(授权)给哪个客户端。为单个客户端执行Datalog查询并不是最佳选择。

结论

Web应用的质量几乎取决于其所使用的工具,正确的工具源于正确的目标。依作者拙见,web应用最终要完全遵守响应式原则,本文旨在抛砖引玉,指出尚未解决的问题和讨论可能的实现方法!