和一些前端框架打过交道,想起来这也是技术选型中经常面对的内容。我把我的经验、思考、感受,甚至是吐槽,记录在这里,有些零散,并且更多的是个人的感悟。而且由于技术所限,可能部分内容不够深入,或者不甚客观。当然,网上有很多分析对比,视角可能更为全面和系统。如果你在技术选型,或者在考虑要学习使用哪一款MVC/MVP/MVVM框架的时候,此文能够给你有价值的信息,就更棒了。如果你觉得我哪些部分说得不正确,或者需要补充,也烦请告知。
需要预先说明的是,这篇文章不是教程,因此如果你对其中某一框架知之甚少,可能需要先去简单学习了解以后才能和我产生共鸣,或者产生反驳的冲动。
以下是第一部分,先谈谈GWT、AngularJS和Backbone。我会在周末和下几周努力去完成其余的部分。
我在《GWT初体验》里已经举例叙述了我的感受。好坏当然见仁见智,但是我是不喜欢它把JavaScript这样灵活而强大的能力约束起来的,代码可以写得干干净净、规规矩矩,但是也没有什么乐趣可言。但是作为从后端语言渗透到前端的尝试,和Node.js这样从前端渗透到后端的“异类”一样,无疑是具有代表性意义的。
GWT 的贡献远不只是在于语言转化的层面,在架构控制上面,非常有效。比方说“无状态服务端+状态化的客户端”这样的经典组合,包括其中客户端和服务端数据交换这样典型的问题上面,处理得非常成熟,并且不需要程序员过多的介入(比如不用选择协议,不用定义格式,不用处理序列化,不用考虑异常的通用处理……)就能够几乎是“无意识”地规范实现了。正规地写,代码容易受控,抓个包,看到的东西清清爽爽,也不容易出现天马行空的或者不统一的设计来。这点其实很重要,一般的前端框架局限于在客户端上做文章,因而是无法严格把控这一点的。Google的维护是品质的保证。
但是想要使用GWT来降低Java程序员的实际项目的学习曲线,恐怕是一厢情愿。因为许多项目大量的时间都会被花在问题定位和一些困难需求或者奇葩功能的实现上面,很可能不得不使用JSNI去写JavaScript,碰到JSNI和Java互相调用的case,就更讨厌。
再有,一门声明式的语言始终是无法避免的。命令式的语言无法解决不直观的问题,我想没有人会喜欢一大堆丑陋的get/set方法。UI Binder的XML是一个令人熟悉的选项,依然保持规规矩矩地风格,但也无可避免地啰嗦而低效。当然,选择了GWT的人,就意味着选择了好几倍的代码量,自然是不会对代码精简有太高要求的。
最后,从工程上看,我用过Eclipse的GWT插件,可以说非常有效。对于静态代码的管理,有大量的检查工具和更有效的测试框架,这些都是很受项目经理喜欢的优点,并且是其它传统JavaScript框架所望尘莫及的。另外,编译时间是一个在选型时常见的担忧。
这些明显的优缺点如同爱憎分明强烈的个性一般,让我参与的许多次技术选型中,都看到了GWT的名字,但是最后,都被排除掉了……
如果团队中只有很少数有经验的前端程序员,而大家都对Java精通,特别是有Swing经验,并且又准备做一个类似Single Page Application (SPA)的话,那么GWT是一个值得考虑的选项。不过话说回来,如果没有任何一个有经验的前端,还想做出成熟和有一定复杂度的页面的话,还是别想了,用什么都不行的。
我说从2014年初开始接触并在项目中使用 AngularJS 的,这又是Google维护的一个非常有前端进化和发展意义的框架。
在 《借助AngularJS写优雅的代码》 中我叙述了当时的感受,当时最令我印象深刻的就是其中的2-way binding。我原本不知道这个东西,后来被保持JavaScript代码中模型和DOM模型之间的状态同步给整烦了,搜索之后才知道解决这个问题的最常见方案就是AngularJS。
当然,AngularJS的双向绑定是毁誉参半的,推荐Marius Gundersen的这个讲座 《A comparison of the two-way binding in AngularJS, EmberJS and KnockoutJS》 ,AngularJS、EmberJS和KnockoutJS都能实现双向绑定,但是各有优劣,很有意思。而不考虑workaround的情况下,AngularJS的双向绑定,在参与的DOM数量比较大(比如数千个)的时候,性能常常出现明显的问题。这在技术选型的时候是必须考虑的因素。
可是,AngularJS包含的意义远不止这一点,对于web界面描述使用更纯粹的声明式代码亦是其核心的追求。我们都写HTML,都知道这种标记语言很适合用来表现所见所得的结构,比编程式的代码更有表现力。但是,HTML和原生JavaScript的支持度还太弱,在AngularJS之前我不记得我知道哪个框架可以把前端MVC中的View变成核心——核心都是Controller,URL mapping也挂在controller上面,它总是知道请求从哪里来,找哪个Model要数据,最后又把数据送到哪个View上去渲染。但是AngularJS把和Controller之间的绑定用属性的形式固定在DOM上了(属性ng-controller),甚至把Controller上面方法的调用也用属性的形式固定在DOM上了。这最初看起来是“反最佳实践”的——我们都说View这一层要纯粹,要守规矩,JQuery之类类库的做了那么多工作把绑定的行为从DOM中分离出去,怎么历史倒退了,View怎么可以知道那么多的东西?
哪知AngularJS在View中体现出来的野心居然比这还大。在MVVM中,我们知道ViewModel的就是给View专门用的数据模型,但是Angular提供的如同管道一般的过滤器,把或简单或复杂的DataModel转化为ViewModel的过程就这样简洁优雅地解决了,比如当时我举的”phone in phones | filter:query | orderBy:orderProp”这样的例子——在传统的做法里面,不应该是写一坨专门的代码来做这个转换么?
通过Directive,View可以做更多的未知的事,这也是一种一定程度上的DSL。在Amazon的内部,多数前端项目都相较简单,但是工程师希望代码清晰、简洁、可维护,因此AngularJS也是比较流行的。而很多项目里面,都把一些可复用的组件,用Directive实现了。
再提一提其中的依赖注入(DI)和遵循的Convention over Configuration (CoC)规则,在写Controller代码的时候,还是比较舒服的,既有scope内变量访问的控制,也把依赖的组件都列在方法签名处,清晰好维护。
说到不好的方面,最大的挑战来自于思维的转变,或者说整体编程范型的转变。对于习惯了写JavaScript各种绑定和用命令式的语句来更新状态的工程师来说,这是一个陡峭的学习曲线。我看到好多人在用AngularJS了,还在反复用JQuery来绑定变量,这个转变真不是轻而易举能完成的。另外,除了Directive的API臭名昭著地难以理解外, digest/watch/apply这套组合拳 也常常被认为是不易理解,但又必须理解的(包括监控变化的是引用还是值这一点)。
再有一个不好的地方在于调试。错误有时候吞了(当然你也可以说“健壮”),有时候则是不知所云,在实践的时候需要反复“编写-运行”这样的过程,以减少每次代码更新的数量,帮助定位问题。
Backbone.js 可能是我接触最早的前端MVC/MVVM框架(那个时候写过一点点入门的总结)。整体来说就是简单、清晰、轻量级,学习曲线平缓,依赖性少,可定制性强,很适合中小型web项目,和对于前端不太深入的团队。
如果属于写惯了JQuery之类的绑定流,Backbone.js是非常容易上手的。
在View里面(别看其名,其实里面的东西看起来包含了以往MVC的Controller的逻辑,我一直有点奇怪它为什么不单独分离出一个真正的“Controller”来单一化职责呢?但是Backbone.js说了,它的Controller是Router,那好吧……)写着写着,有一种只手遮天的感觉——什么东西它都知道,它都管,包括初始化、模板渲染、DOM操纵、事件响应、绑定等等。对比AngularJS的通过DOM属性的方式来控制范围和绑定行为,Backbone.js看起来更加容易理解,在View里面用el这个属性来建立和限定区域DOM树的联系。总的来说,它的设计上是简单了,但是它把不同逻辑不通职责的代码管理留给框架使用者了,结果也很容易臃肿。
Model层(Model.extend)的设计,代码风格一样,但是要纯粹得多,更符合单一职责的原则,只负责数据模型的初始化、获取、转换、校验等等。
和Model搭配干活的,还有一个Collection,方便熟悉面向对象的程序员对数据进行包装分类。通常从服务端Ajax获取数据也是使用它来完成的。
Router层也是很好的设计,清晰简单,专门负责URL mapping,代码风格依然和上面一样保持一致。
模板默认是Underscore.js,但是这个是可以换的。它欠缺了双向绑定,一个特别有用的特性。无论是Model中的数据通过set方法来主动更新(JavaScript代码更新),需要在Model中bind事件来监听;还是DOM树上的呈现发生被动变化(用户更新),需要在View中的events中还是绑定事件来监听,这些不同组件(层)之间的消息互通,实现都是类似的——而对于程序员来说,这可是一大块工作,不但枯燥和令人沮丧,还容易出错。选择了Backbone.js还迫切需要双向绑定的,可以使用第三方的库,比如 Epoxy.js ,不过这不在今天的讨论范围内。
总体来说,Backbone.js最简单,最容易上手,提供了非常易于操作的前端代码模块化的方案,对HTML的侵入性也最小,和别的库的集成也相对容易。但是需要写比AngularJS多得多的JavaScript,尤其是其中的事件响应代码,还有模板渲染代码,在比较多的时候,写起来并不愉快。自由总有代价,它很多特性都是缺失的,除了上面说的双向绑定,还有缺少良好的模块之间的依赖管理工具,这些东西都需要在必要时候去寻找第三方的类库(比如 RequireJS )来完成,通常这一时间和风险开销在技术选型的时候需要特别考虑。