和一些前端框架打过交道,想起来这也是技术选型中经常面对的内容。我把我的经验、思考、感受,甚至是吐槽,记录在这里,有些零散,并且更多的是个人的感悟。而且由于技术所限,可能部分内容不够深入,或者不甚客观。当然,网上有很多分析对比,视角可能更为全面和系统。如果你在技术选型,或者在考虑要学习使用哪一款 MVC/MVP/MVVM 框架的时候,此文能够给你有价值的信息,就更棒了。如果你觉得我哪些部分说得不正确,或者需要补充,也烦请告知。
需要预先说明的是,这篇文章不是教程,因此如果你对其中某一框架知之甚少,可能需要先去简单学习了解以后才能和我产生共鸣,或者产生反驳的冲动。
以下是第一部分,先谈谈 GWT、AngularJS 和 Backbone。我会在周末和下几周努力去完成其余的部分。
GWT
我在 《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 是一个值得考虑的选项。不过话说回来,如果没有任何一个有经验的前端,还想做出成熟和有一定复杂度的页面的话,还是别想了,用什么都不行的。
AngularJS
我说从 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 之前我见过一些实现的骨架代码,核心都是 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
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)来完成,通常这一时间和风险开销在技术选型的时候需要特别考虑。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》