2004年,对于前端社区来说,是里程碑式的一年。Gmail横空出世,它带来基于前端渲染的原生应用级别的体验,相对于之前的服务端渲染网页可谓提升了一个时代,触动了用户的G点。自此,前端渲染的网站成为无数开发者追逐的方向。
为了更好地开发前端渲染的“原生级别的”网站,包括Backbone和Angular在内的一系列前端框架应运而生,并迅速获得了大规模的采用。但是很快地,新的性能和SEO问题也接踵而来。几经尝试后,Twitter甚至从前端渲染重回服务器渲染,而Strikingly也面对过同样棘手的问题。
2014年,React进入我们的视线。让人耳目一新的是,对于其他开源框架遇到的种种问题,React都自信地给出了解答。几乎没有犹豫,我们开始使用React来重构Strikingly。若干年后,当我们回望,也许会发现,2014年也是前端社区里程碑式的一年。
React简介
React究竟是什么?Facebook把它简单低调地定义成一个“用来构建UI的JavaScript库”。这个定义也许会让我们联想到许多JavaScript模板语言(比如Handlebars和Swig),或者早期的控件库(比如YUI和Dojo),但是React所基于的几个核心概念使它与那些模板和控件库迥然不同。事实上这几个核心概念非常超前,已经给整个前端世界带来了冲击性的影响。它们包括:
- 组件和基于组件的设计流程;
- 单向数据流动;
- 虚拟DOM取代物理DOM作为操作对象;
- 用JSX语法取代HTML模板,在JavaScript里声明式地描述UI。
这几条简单的原则放在一起带来了大量的好处:
- 前端和后端都能够从React组件渲染页面,完全解决了SEO长期困扰JavaScript单页应用的问题;
- 我们可以简单直接地写前端测试而完全忘掉DOM依赖;
- 组件的封装方式和单向数据流动能够极大地简化前端架构的理解难度。
我们来看一个例子:
var HelloMessage = React.createClass({ render: function() { return <div>Hello {this.props.name}</div>; } }); React.render(<HelloMessage name="John" />, document.body);
这个React版的Hello World已经展现了React的一些核心特性。首先,HelloMessage是一个React组件;创建React应用的时候,我们总是以组件为出发点。每个组件的核心是一个render方法,在其中我们把这个组件的props和state拼装到一个最终要渲染的模板中,然后返回这个模板(确切地说这里是一个UI描述而不是传统意义上的模板)。这段代码里看起来像HTML一样的部分就是著名的JSX语法,它是在React中描述“模板”的最佳方式。
现在,以var开头的第一段里我们定义了一个叫HelloMessage的组件;下面的React.render这一行所做的,则是把这个组件渲染到document.body里——也就是我们实际的页面上。但在使用〈HelloMessage/〉的时候,我们做了另一件事:name="John"。看起来很像HTML中的元素属性,但是既然JSX不是HTML,这个语法的作用是什么呢?实际上,这就是我们向React组件传入props的方式。回头看第一段,我们可以看到在组件的内部有对this.props.name的引用。这个name就是我们刚刚指定的John!
看到这里,如果你熟悉jQuery的话也许在想,这与$(document.body).html('Hello John') 有什么根本区别呢?
这就是虚拟DOM出场的地方了。我们像写HTML一样写JSX,但是JSX并不会直接变成HTML和DOM。在幕后,React维护着一个虚拟DOM,而实际上被浏览器直接操作的“物理”DOM只是这个虚拟DOM的投影。虚拟DOM不依赖于浏览器环境,它可以运行在任何JavaScript执行环境。这就让下面的代码成为可能:
var html =React.renderToString(<HelloMessage name="John"/>);res.send(html);
如果第二行有点眼熟,你没有猜错——这段代码发生在服务器端!是的,同样的 HelloMessage,我们不仅可以让React在前端渲染到页面,同样可以在后端直接渲染成HTML字符串,然后把它返回给前端。服务端预渲染就这么自然地发生了。
React带来的革命性创新是前端世界过去几年最激动人心的变化。自从接触React以来,我们深信React会彻底改变客户端开发者(包括前端、iOS和Android)的开发体验。在下面的篇幅里,我们想从四个大的方向——目标平台(Targets)、数据处理(Data)、工具(Tools)和新的挑战——分享一下React生态系统和社区的进展和未来趋势。
目标平台
对于虚拟DOM的讨论,很多人会说速度快过于真正的DOM。这样的讨论可以让人快速入门理解React,但是真正写过React应用的人会明白速度并不是虚拟DOM的精髓。我们认为虚拟DOM的存在帮助我们做到了两件事。第一是申明式UI。通过虚拟DOM,UI不再是一个不断被更变的DOM,你只要申明UI是怎么生成的,React会自动帮你把UI的改变渲染到真正的DOM上。这种新的思维方式让你可以不用手动操作真正的DOM。第二是多Target。我们一直在讲Web,但React让我们做到Web以外的Target。虚拟DOM更像是UI虚拟机,自动帮你映射到真正的实现上,可以是浏览器DOM、iOS UI、Android UI。甚至有人做到了React映射到终端文本UI。
多Targets是React社区常常在讨论的主要话题之一。多Targets的根本是提高开发者体验。开发者体验(DX,Developer Experience)是在React社区里屡次被提起的概念。如何在保持一样的用户体验下,提高开发者体验,是包括React在内的前端社区正在思考的问题。事实上任何一家有多客户端的公司都面临着这样同一个问题:在各种客户端语言里重新造轮子。开发者需要学习新的语言、写和维护类似的功能。提升客户端开发者体验就是减少学习成本和维护成本。这就是React提倡的“Learn once,write everywhere”。
最近也有一些鼓舞人心的消息。Facebook内部Ads Manager iOS版本由7位前端工程师用React Native花了5个月完成。而Android版本,是同一班人,3个月内完成。代码重用率达到了87%。
多Targets也可以是在单个平台更深度的结合。来自React核心团队的Sebastian Markbåge在ReactEurope大会上给了一个让人目瞪口呆的演讲《DOM as a Second-class Citizen》。演讲中他畅想React直接输出到浏览器架构的底层(图1浏览器的渲染架构,图2为Sebastian Markbåge认为React可以做的事情)。
图1 浏览器的渲染架构
图2 Sebastian Markbåge认为React还可以做很多事情
姑且不谈该不该这么做,通过虚拟DOM打开了这样的机会就已经让我们兴奋不已了。也说明了Facebook在设计React时已经考虑到超越DOM。想法确实很超前。
【服务端预渲染(Pre-rendering)】
对于其他主流前端框架,页面SEO和首次打开速度的问题都很让人头疼。Twitter当年因为首次打开速度过于慢甚至重回服务器渲染方案。一直以来人们一直在寻找一种只需要编写一次UI组件,前后端同时都能渲染的方案。如果能做到的话,我们就可以在首次打开页面时先用服务端渲染页面HTML,当浏览器收到后已经可以显示页面。这样SEO和首次打开速度都能被解决。这种完美方案社区里称之为Isomorphic/Universal App。
React原生支持了Pre-rendering(服务端渲染)。由于有虚拟DOM,也就意味着我们只需要后端运行JavaScript引擎就能渲染整个DOM。目前主流后端语言都可以运行V8 JavaScript引擎。比如Strikingly的后端使用Ruby on Rails,只需要使用开源的react-rails gem就可以在Rails后端渲染前端React组件。
使用服务端渲染时要注意window和document这些浏览器才有的全局变量是不存在的。React组件提供这两个lifecycle hook:componentDidMount和componentDidUpdate在服务器不会被运行,只有在前端才会运行。使用服务器渲染时如果要使用任何浏览器才有的变量需要把代码放到这两个lifecycle hook定义里。
数据处理
React定义自己为MVC中的View。这让前端开发者从V开始去思考UI设计。但现在针对数据操作和获取方式,社区里还没有一种公认的方法。这也是任何写React应用时最难处理的地方。
【Flux】
对于M和C,Facebook提出了Flux的概念。Flux是一个专门为React设计的应用程序架构:应用程序由Dispatcher、Store和View组成,其中的View就是我们的React组件。Flux的核心是如图3所示的单向数据流动。
图3 单向数据流动为Flux的核心
应用程序中的任何一次数据变化都作为Action发起,经过Dispatcher分发出去,被相关的Store接收到并整合,然后作为props和state提供给View(React组件)。当用户在View上做了任何与数据相关的交互,View会发起新的Action,开启一次新的数据变化周期。这种单向性使Flux在高层次上比传统MVC架构和以Angular和Knockout为代表的双向数据绑定容易理解得多,大大简化了开发者的思考和Debug过程。
在Facebook把Flux作为一种设计模式(而不是已经做好的框架)宣布之后,几乎每个月出现一新的Flux库,他们都有各自的特色,有的对服务器渲染支持比较好,有的运用了更多函数式编程的概念。很多Flux库更像是实验,这有助于React生态的生长,但不可否认的是,未来会有大量Flux库慢慢死去,而只有少数会存留下来或进行合并。
【GraphQL】
在构建大型前端应用时,前端和后端工程师通过API的方式进行合作。API也是双方的协议。现在主流的方式是RESTful API,然而在实践中,我们发现RESTful在一些真实生产环境的需求下不是很适用。往往我们需要构建自定义endpoint,而这违背了RESTful的设计理念。
举个例子,我们想要显示论坛帖子、作者和对应的留言。我们分别要发出三个不同的请求。第二个请求依赖第一个请求结果返回的user_id,前端需要写代码协调请求之间的依赖。分别发出三个不同请求在移动端这种网络不稳定的环境下效果很不理想。
GET /v1/posts/1 { "id": 1, "title":"React.js in Strikingly", "user_id":2 }
GET /v1/users/2 { "id":2, "name":"dfguo" }
GET /v1/posts/1/comments [{ "id":6, "name":"rechtar", "comment":"Thanks for sharing! I would love to see some examples on GraphQL."},{ "id":9, "name":"tengbao", "comment":"I heard that you guys also use immutable.js. How did it help?"},{ "id":12, "name":"syjstc", "comment":"Impressive work! Thanks guys!" },{ "id":18, "name":"abeth86", "comment":"Thanks for the sharing!" }]
为解决这类问题,工程师会自定义一些endpoint。对于这个例子,我们可以建立一个/feeds的endpoint,集合了所有前端需要的结果:
GET /v1/feeds/1 { "id":1, "title":"React.js in Strikingly", "user":{ "id":2, "name":"dfguo" }, "comments":[ { "id":6, "name":"rechtar", "comment":"Thanks for sharing! I would love to see some examples on GraphQL." }... ] }
但是我们在某些场景上可能只需要post和user,不想要comments。这时难道要再定义一个feeds_without_comments的endpoint?随着需求的改变,自定义endpoint的方法往往使得API接口变得累赘,违背了RESTful的设计理念。而任何前端工程师需要的数据一旦要改变都需要后端工程师的配合,这降低了产品的迭代速度。
来自Facebook的GraphQL是我认为目前最接近完美的解决方法。后端工程师只需要定义可以被查询的Type System,前端工程师就可以使用GraphQL自定义查询。GraphQL查询语句只需要形容需要返回的数据形状:
{ post(id:1){ id, title, user{ id, name }, comments{ id, name, comment } } }
GraphQL服务器就会返回正确的JSON格式:
{ "id":1, "title":"React.js in Strikingly", "user":{ "id":2, "name":"dfguo" }, "comments":[ { "id":6, "name":"rechtar", "comment":"Thanks for sharing! I would love to see some examples on GraphQL. }... ] }
GraphQL也原生支持了API版本控制,让你可以同时共存多个版本的客户端(包括Web和Mobile)。这些都会减少客户端工程师和后端工程师的耦合度,提高生产力。
今年7月刚推出了GraphQL的规范并开源了JavaScript GraphQL库。然而要让GraphQL成为主流,Facebook需要打造一个像React这样的生态系统。要想在你自己的应用上用GraphQL还必须要有后端语言提供GraphQL库的支持。比如Strikingly需要GraphQL Ruby库。这不仅仅需要前端工程师。我们认为这将会比React生态系统更难建立(见图4所示)。Facebook需要整个社区的参与才能达到。
图4 GraphQL生态系统
【Relay】
Relay是Facebook提出的在React上应用GraphQL的方案。React的基础单位是组件(Component),构建大型应用就是组合和嵌套组件。以组件为单位的设计模式是目前社区里最认可的,这也是前端世界的趋势之一。每个组件需要的数据也应该在组件内部定义。Relay让组件可以自定义其所需要GraphQL数据格式,在组件实例化的时候再去GraphQL服务器获取数据。Relay也会自动构建嵌套组件的GraphQL查询,这样多个嵌套的组件只需要发一次请求。Relay将会在8月份开源。
【Immutability】
React社区接受了很多函数式编程的想法,其中受Clojure影响很深。对Immutable数据的使用就是来自Clojure社区。当年Om,这个用ClojureScript写的React wrapper在速度上居然完虐原生JavaScript版本的React。这让整个社区都震惊了。其中一个原因就是ClojureScript使用了Immutable数据。React社区里也冒出了Immutable.js,这让JavaScript里也能使用Immutable数据,完美弥补了JavaScript在负责数据对象比较的先天性不足。Immutable.js也成为了构建大型React应用的必备。甚至有在讨论是否把Immutable.js直接纳入JavaScript语言中。我们认为小型应用不会遇到虚拟DOM的性能瓶颈,引入Immutable.js只会让数据操作很累赘。
工具
工欲善其事,必先利其器。React的火爆得力于来自社区的工具,而React也推动了这些工具的进步。这里我们想介绍几个React社区里比较受欢迎的工具。
【Webpack】
在React里,由于需要用到JSX,使用Webpack或Browserify这类工具编译代码已经渐渐成为前端工程师工作流程的一部分。Webpack是一款强大的前端模块管理和打包工具(见图5所示)。这里列出它的一些特性:
- 同时支持CommonJS和AMD模块;
- 灵活和可扩展的Loader(加载器)机制,例如提供对JSX、ES6、Less的支持;
- 支持对CSS,图片等其他资源进行打包;
- 可以基于配置和智能分析打包成多个文件;
- 内置强大的Code Splitting功能可以拆分并动态加载包;
- 开发模式支持Hot Module Replacement模式,提高开发效率。
图5 前端模块管理和打包工具Webpack
【Babel】
ECMAScript 6(ES6)规范在今年四月刚敲定,React社区基本全面拥抱ES6。但目前还有很多浏览器不支持ES6。使用像Webpack这样的工具编译代码使得我们可以在开发时使用ES6(或者更新版本),在上线前编译成ES5。编译工具中最引人注意的是Babel。前身为ES6to5,Babel是目前社区最火的ES6编译到ES5的代码工具,Facebook团队甚至已经决定转用Babel而不再维护之前内部使用的jstranform。通过Loader机制,Webpack可以非常简易地和Babel结合应用。
【React-hot-reload】
在开发任何大型前端应用过程中,我们常常会因为一些小错误就需要重新刷新整个页面。React-hot-reload尝试解决这个问题,提高开发效率。他使用了Webpack的Hot Module Replacement功能,动态替换React组件的lifecycle hook定义,不用刷新页面也可以更新代码变化。
【React Developer Tool】
这款Facebook官方推出的Chrome插件可以让你方便地在浏览器中直接查看React的组件结构。安装后,在Chrome开发者工具中会多出一个React Tab。界面就像DOM Inspector一样,只不过是看React组件结构关系。是开发React应用不可多得的工具之一。
挑战
React正在快速开拓着它的疆界,这意味在获得新的喜悦的同时,我们也面临着许多新的挑战。现在围绕着几个大的议题,React社区仍没有达成定论,每周甚至每天都有新的实验项目在尝试这些问题的解决。
【动画】
一直以来大家都对动画应该在React里怎么表达为状态感到困惑。Cheng Lou的React Tween State是我们认为最符合React思维的做法。把位移存在State里,然后通过JavaScript动态渲染新的位置。不过大家对该做法是否能达到满意的速度一直持有保留态度。在今年ReactEurope的演讲中,他为我们展现出了出色的效果和速度,非常值得一看。
在Strikingly,我们对于动画则采取了比较实用主义的处理方式:我们定义了一些容器组件,比如〈JQFade/〉和〈JQSlide/〉,在其中调用jQuery的动画方法来实现相应的Transition。这种方式在理论上并不完全符合React的精神,不过到现在为止还是能够满足我们需求的。
【Flux库与Relay】
正如上文已经提到过的,目前Flux的各种实现可谓是百花齐放,其中还并没有出现一个具有权威性的事实标准。Relay同样也是刚刚孵化不久的新生概念——所有这些意味着虽然Flux+Relay会带来生产力的飞升,要实际用上它们我们还要待以时日。
【CSS】
CSS是一个有趣的话题:似乎所有人都觉得当前的CSS有深刻的缺陷,但是对于怎么解决这些缺陷大家的意见却分成了两派各不相让:一派认为CSS“可以被修好”,并且致力于修好它,由此诞生了cssnext这样的项目;另一派认为CSS从根本上作为诞生于一个古老时代的东西,已经不能适应大规模、组件化的现代开发流程,这一思想集中反映在Christopher Chedeau的演讲《React: CSS in JS》中;在其中他提出了CSS的七个根本问题,然后指出在JavaScript中直接使用inline CSS可以几乎“免费”地解决所有这些问题。在传统的Web开发最佳实践中inline CSS一直是被压制的反面实践,现在我们却能够以一个全新的视角看待它,这也完美地例证了React真的是在给整个前端世界带来根本性的推动。
总结
在不久前的JSConf 2015上赫门提出了前端的摩尔定理:前端每18月会难一倍。前端之所以变化这么快,是因为我们现在面临着前所未有的工程化挑战。今天的前端复杂度跟几年前完全不是一个等级。这也促使社区要找到在这种复杂度下能保持开发效率和开发体验的工具和设计模式。React社区从其他领域(游戏渲染、ClojureScript、函数式编程)偷师学艺,结合前端面临的独特问题,提出了一系列解决方案。React社区在各方面都推动着前端社区往前进。这对整个社区都是好事。我们也希望前端各个框架可以互相学习,共同推动整个社区的发展。
作者简介
郭达峰,Strikingly.com CTO、联合创始人。2010年开发了三款Facebook平台应用,获取了超过千万的用户。2012年创立了建站平台Strikingly,成为第一家进入YC孵化器的华人团队。
冯哲锐,Strikingly.com前端工程师。学生时代玩Turbo Pascal和Delphi,毕业后写Python和C#;2014年作为前端工程师加入Strikingly,开始专注于技术和用户体验的结合。近期倾心于React和ES6带来的大规模前端技术革新。