文 / 尤雨溪
近年来随着Web应用交互复杂度的提升,前端开发也迎来了一个高速发展的时期。除了一些老牌框架纷纷推出改动较大的升级之外,还涌现出一 批新生代的开源库和框架,推动着Web应用开发理念向越来越强调前端架构的方向发展。当下的前端技术可以说是处在一个新旧交替的过程之中,同时存在着许多 不同的观念和实践。
本文试图对目前数量繁多的前端框架进行一些较笼统的分析和比较,抛砖引玉,希望能为大家在选择前端的技术架构时提供一些有益的参考。需要明确的是,本文探讨的前端架构是以JavaScript为主。有一些主要关注CSS层面的前端框架,如Bootstrap,不在本文的讨论范围之内。
今天的JavaScript框架和库繁多复杂,很大程度上源于Web前端开发本身的特殊性。从当初的可有可无到今天各种功能完备的HTML5标 准,JavaScript在Web应用中的职责和定位经历了巨大的变化。加上长期以来各种浏览器对ECMA标准支持参差不齐的复杂环境,这导致大家对于 JavaScript能做什么、该做什么、应该怎么做一直无法形成共识。一个Web应用可以把所有业务逻辑全部放在服务器端,几乎不依赖 JavaScript;也可以完全用JavaScript构建客户端,服务器只负责数据接口;更有可能选择介于两者之间的折中方案。整体架构选择的多样性 使得不同的应用对于前端架构有着截然不同的需求。这意味着很难有一个前端库或框架可以满足所有人,也使得开发者在找不到完美方案的情况下选择重复造轮子。 同时,由于JavaScript是一门相当灵活的语言,不同背景的开发者借鉴了许多不同的软件设计思想来构建他们理想中的JavaScript框架,这也 导致不同的框架/库在解决同一个问题时经常有不同的方案,例如单页应用的设计模式问题。
框架vs.库
众 所周知,在前端开发中对于库(Library)和框架(Framework)的区分向来是有些模糊的。像jQuery、YUI这些项目的官方描述都是 “库”,却经常在各种地方被人们称作“框架”。近两年出现的一些MVC项目号称框架,实际上却更像库。此外,在同样号称框架的各个项目之间所覆盖的功能也 都有所不同。传统软件工程对于库和框架的区分主要着眼于对应用运行流程的控制权。框架提供架构,控制运行流程,让开发者在合适的地方书写针对具体问题的代 码;而库则附属于架构,不控制运行流程,只提供可调用的函数。但由于上述Web前端开发的特殊性,这样的定义显得有些过于严格:真正称得上框架的项目很 少,却又经常需要和作为库的项目进行比较。因此,在比较JavaScript开源项目时,是框架还是库并不特别重要,首先应该分析该项目覆盖了前端开发中 的哪些问题(下文为了论述方便,一律用框架指代各类JavaScript开源项目)。
前端开发可能面对的需求
前端开发中最常见的问题大致可以分为:封装原生API和常用任务、基础架构、富应用架构、视觉交互,以及工具链。下面我们逐个分析。
封装原生API和常用任务
JavaScript的原生API存在以下问题。
- 对象、方法名烦琐。
- 缺少一些常用任务的语法糖。
- 旧浏览器兼容性不佳。
因此早期的一些框架最主要的目标就是把烦琐的原生API和常用的任务封装成更简洁直观的API,同时,在封装过程中也处理了兼容性问题。jQuery就是解 决这一部分问题的典型方案。HTML5和ECMAScript 5标准的出台使得这些问题有所好转。随着新标准的普及,将来对于这一部分功能的需求会逐渐减弱。通常来说,封装的对象包含以下类别。
- DOM的选择和操作:经典的例子如jQuery的链式API。
- DOM事件处理:一个重点是简化事件的delegation,即利用事件冒泡机制在父元素上用一个侦听函数侦听触发在多个子元素上的事件。
- Ajax:简化烦琐的XMLHttpRequest API,并且加强其语义性。
- 语言增强:主要提供一些对数组和对象进行操作的便利函数。jQuery包含一些,但更典型的有Underscore和Lodash。
基础架构
这一部分通常是各类框架中比较底层的功能,决定了采用此框架的代码是如何被组织到一起的。目标是提高代码的可维护性、可协作性和可测试性。
模块管理:对大型的JavaScript项目来说,模块化开发是必需的。有些框架只提供基本的模块注册机制以防止全局变量污染和冲突,而另一些则提供包括 模块依赖解析、文件加载、压缩打包的功能。目前JavaScript模块管理有两个互相竞争的标准,一个是AMD(Asynchronous Module Definition),采用的框架有Dojo,单独的模块管理库有RequireJS;另一个是CommonJS的模块标准,采用者有模块管理库 SeaJS,以及基于SeaJS的开放型框架Arale。面向对象:JavaScript有原型继承,并且可以非常灵活地进行动态混入,但很多大型项目还是需要一个统一的面向对象的继承/扩展系统。对 此,John Resig、Douglas Crockford、Nicholas Zakas等各路JavaScript大神都曾经进行研究并给出过各自的解决方案,许多框架中也包含类似的解决方案。对此感兴趣的同学推荐阅读Arale 文档中的这篇文章:http://aralejs.org/class/docs/competitors.html 。
自定义事件系统:为了提高模块的可复用性、整个系统的容错性和灵活性,各个模块之间需要尽量解耦,使得相互之间尽可能减少依赖。要实现这样的解耦,一个自定义的事件机制 (通常借鉴Pub-Sub、Observer、Mediator等设计模式) 是很好的手段。
组件系统:定义如何使用和书写组件、组件之间如何相互调用和通信等。
富应用架构
这一部分的主要目的是利用设计模式进一步提高代码复用,使得开发者的精力可以主要集中在实现应用本身的功能上。
代码逻辑分层:把对视觉界面、交互逻辑和数据的处理清晰地分开。就这一点而言,大部分框架借鉴了经典的MVC模式,但传统的MVC在前端并不适合直接套 用,因此各个框架对此的处理都略有不同,有些采用了MVP(Model-View-Presenter)或是MVVM(Model-View- ViewModel)模式。著名前端布道师Addy Osmani有详细的分析,本文受篇幅所限不再赘述。
数据绑定:把界面和数据模型进行绑定,使得一方变化的时候另一方也会自动变化,可以省去手动更新DOM的操作。
数据与服务器端的同步:服务器端提供符合REST规范的API,前端的数据模型可以封装同步操作,可以省去手动发送Ajax请求的过程。
模板渲染:在前端存储和渲染可复用的HTML模版,这样更新界面时不需要再向服务器发送额外的请求。
URL路径、应用状态和历史管理:无论是从搜索引擎还是用户体验的角度来看,大型单页应用都应该提供和应用的状态相对应的URL,同时不破坏后退键的功能。
视觉交互
传统型大框架会包含这部分内容,通常是基于自身架构上的扩展,对框架自身有依赖性。但一些新框架则只专注于架构,对扩展部分彻底持开放态度,提倡让开发者自己选择最合适的工具。
效果和动画:用原生JavaScript实现动画是一个比较烦琐的过程,尤其是当需要精确的时间和缓动(easing)处理时。因此一些框架如jQuery提供一个API简洁的动画引擎,并封装了常见的动画效果。
UI组件库:这是传统型的大框架的主要卖点之一,大量现成的UI组件以及与框架本身的亲和性,可以大幅提高开发效率。YUI、Dojo是典型代表。在选择时,需要注意的一点是可定制性。
数据绘图:这是一类比较特定的需求,但其实现的复杂程度也非常高。ExtJS封装了强大的数据图表功能,除此之外也有专门针对数据可视化的库如D3.js。
工具链
随着前端项目越来越大,维护和上线的流程也越来越复杂。利用好各类工具实现自动化,可以大幅提高效率。随着Node.js社区的迅猛发展,各类基于JavaScript的命令行工具大量出现,这其中就有许多针对前端开发的优秀项目。
编译工具:这里的编译严格来说是指js的组合和压缩。通常大型项目都会有多达几百KB的js文件,采用模块化开发的话,文件数量也会非常庞大,进行压缩打 包是必不可少的过程。大型框架如Closure Library、Dojo、YUI都自带编译工具。如果使用了模块管理库,例如RequireJS和SeaJS,也可以使用它们自带的打包工具。其他情况 下则可以自己写build script,也可以借助任务化的编译工具如Grunt.js或者Jake。
包管理工具:很久以来,前端开发者都需要自己下载、管理各类第三方库。前端的包管理机制的好处是可以更方便地管理第三库的版本和相互之间的依赖。目前国外 比较流行的单纯的前端包管理工具有Twitter的Bower,也有混合了包管理和编译于一体的Ender.js和Volo.js,更有集组件框架、包管理和编译于一体的Component。国内方面,SeaJS提供包管理+编译工具spm。
单元测试工具:前端也需要单元测试。同样的,传统的大型框架也大多自带单元测试工具。单独的测试框架中比较流行而且简单易用的有QUnit、 Jasmine、Mocha等。此外,还有一些更复杂的前端测试框架,包含了在各类浏览器里的自动化测试,这又可以单独开出一个话题,限于篇幅不再深入, 感兴趣的朋友可以参考:http://stackoverflow.com/questions/300855/looking-for-a- better-javascript-unit-test-tool。
框架的分类
以下根据风格对一些主流框架进行了粗略的分类,但分类并不是绝对的,只是为了简化比较的过程。
封装型
典型如jQuery、MooTools,国内则有百度的Tangram。这一类框架通常只针对上述需求列表中的“封装原生API”这一块。虽然有插件机制,但通常不提供任何架构方面的帮助,因此现在更多的是和架构类的轻量框架搭配使用。
传统型
典型如Dojo、YUI、Closure Library、ExtJS等,国内则有阿里的KISSY、网易的NEJ、腾讯的JX等。支付宝玉伯将这一类比喻为“大教堂风格”,一般有这些特点:稳 定,经受过实战的考验;覆盖的问题全面,试图在自身范围内解决尽可能多的问题;代码风格和质量一致,也经常会要求使用者遵循一定的风格规范;文档丰富详 细;更新稳重、缓慢;排他性,一旦选择很难替换;通常带有UI组件库。
开放型
就国内外的典型例子来说,国外有由Node.js著名活跃开发者TJ Holowaychuck所牵头的Component(https://github.com/component/component),国内则有阿里的Arale和豆瓣的Oz。
开放型的框架专注于提供开放的基础架构,即代码组织方式和工具链。它们也提供一部分现成的模块,但使用者可以灵活地书写模块,或是博采众家之长,将第三方的库整合为模块来使用,又被称之为“集市风格”。
- 灵活,可以适应不同类型的架构需求。
- 用不同库解决不同问题,解耦性好。
- 社区活跃,更新迅速。
- 代码风格质量不一。
- 选择和整合第三方库时需要很多精力。
单页应用型
代表性的如Backbone.js、Ember.js、AngularJS、Knockout.js等。单页应用的优势是服务器请求数少、UI反应快速、用户体验流畅。对于交互复杂的大型应用,尤其需要有一个为单页应用量身打造的前端架构来支撑。
这一类框架在近两年大量涌现,如上文所说,存在着许多不同的观点。
- 首先是架构采用什么模式:是MVC(Ember.js、AngularJS)、MVP(Backbone.js)还是MVVM(Knockout.js)?
- 其次是侵入性:侵入性较强的提供更多的架构支持,但学习曲线也更陡峭一些(Ember.js、AngularJS);侵入性较弱的则上手更快,和其他框架/库更容易兼容(Backbone.js、Knockout.js)。
- 最后是View的处理:是由JavaScript来把基于String的模板渲染成DOM(Ember.js、Backbone.js),还是由JavaScript为已有的DOM添加行为(AngularJS、Knockout.js)?
这些问题目前还没法得出决定性的结论,更多的是看开发者的个人偏好和习惯更适应哪一种。好在这类框架目前社区都相当活跃,因此可以找到不少参考。
值得一提的是,一些老牌的传统型框架如Dojo、YUI、ExtJS都开始引入了MVC单页应用架构。但Dojo和YUI的这部分功能仍处在比较粗糙的阶段,只有ExtJS进行了整体重大升级,相对成熟一些。
特例
Twitter在2月初刚开源了它的前端框架Flight。这是一个介于单页应用型和开放型之间的框架。其核心是事件驱动的、基于DOM的组件机制。它强调组件之间相对独立松散的架构,但对组件的写法定义十分严格,具有一定的侵入性和排他性。
选择框架时应该考虑什么
项目规模:小项目需要快速迭代,需要灵活性较高、兼容性比较好的架构。而大项目则需要关注成熟度、风格规范、可协作性、可维护性和可测试性。
团队的现有资源:团队是否对后台技术选择和架构有一定的偏好?是否已经对某些框架/工具有实战的经验?如果选择一个开放式的框架,是否有足够的精力来整合各类第三方工具?最后,选择一个团队不熟悉的框架,需要衡量带来的好处是否能抵消掉学习成本。
产品对用户体验的需求:产品本身更适合做成单页应用还是传统Web应用?产品是需要大量现成的UI组件,还是需要注重每一个细节?
建议在选择框架时,首先对自己的项目和团队进行定位,然后总结出具体的技术需求列表,最后参照上文列举的各项细节来寻找适合的框架和周边工具。当然,本文无法穷举所有的细节,只能提供大致的选择方向。确定几个潜在选择之后,还需要开发者自己进行深入的研究和试用。
总结
前端技术正处在一个新老并存、百家争鸣的时代。一方面我们需要等待HTML5以及其他W3C标准的普及,另一方面更新的标准,如Web Components和ECMAScript 6又已在起草之中。这两个标准普及时,前端架构恐怕又会迎来一次洗牌。但不管怎样,做好足够的功课,然后根据自己的实际需求出发来进行选择总是没错的。
作者尤雨溪,就职于Google Creative Lab,参与各类实验性项目的设计与原型开发。GitHub ID:@yyx990803
本文选自《程序员》杂志2013年3期,未经允许不得转载。如需转载请联系 market@csdn.net