阅读更多
上半年成功在公司推动了由Richfaces迁移到Primefaces,后续的目标是推动引入一个客户端MVVM框架,本来相中了国货Avalon.js,可惜没有官方的英文网站和文档,最终被某JCEA(嗯,Java架构师……)否决了,好在其对引入MVVM本身倒不太反对,于是改成了Knockout.js。目前在几个前端能力较强的同事中成功局部运用起来了。但要在Java Web Team里推广,就得考虑与Primefaces整合的问题。在此想把探索整合方案的过程记录一下。最近老二刚满月,空余时间较少,希望能坚持下去不要太监吧。
先谈谈为什么要考虑JSF与客户端MVVM整合。整合的原始需求,就由此而来。
初看上去,客户端MVVM与JSF的功能是重叠的,客户端MVVM的目的是把展现逻辑移到前端来,而JSF的给人的感觉恰恰是要通过服务器端组件隐藏客户端代码,岂不是打起架来了。然而在实践中我发现,要想用JSF完全屏蔽掉JS,是不可能的。业务需求千变万化,一套现成的组件库不可能满足所有的场景。虽然自JSF2以来,开发组件的难度越来越小,但毕竟还没到信手沾来的地步。写组件为了能复用,总要额外开个组件库项目吧。如果整个团队事无大小都把涉及JS的代码封成组件往里丢,一来导致一些简单的耦合代码被强行拆分在两个包中;二来必然导致组件库过度膨胀,过度膨胀的后果就是使用者发现与其去查找是否有某个适用的组件还不如自己现写一个丢进去,结果进一步加剧组件库膨胀,恶性循环。
既然无法避免JS,让Java程序员去写少量(相对于Java和Xhtml的代码量来说)JS最容易遇到的问题是:代码零零碎碎的嵌入页面,完全没有组织,这就是我们公司的现状。原因也很直接很简单,当前主流JSF框架的Ajax实现都是基于局部渲染,替换DOM树的。为了保证在DOM树被局部替换后,手工写的JS监听器能重新绑上去,这部分绑定代码(通常也就十几行以下)就必须放到局部渲染区域内部,与被绑定的元素靠在一起。因此把所有JS代码都集中到整个页面之前或之后是不可行的。这是问题的根源,由此会引出多种影响开发与维护效率的问题,比如命名空间混乱,难以从出问题的元素反查到绑定代码,缺乏成体系的客户端API,等等。
特别是“难以从元素反查代码“这个问题,给我们维护代码带来非常多的麻烦。前面说到,为了迁就局部渲染,手写的JS代码倾向于靠近其需要操作的HTML元素。但现实中经常有这样的场景,A,B两个区域,在代码中相隔很远,由于facelet模板允许自由嵌套,往往甚至在不同目录的不同文件中,但它们逻辑上是相关的,总是被同时渲染,而且JS代码所需要的上下文状态需要在B渲染后才能决定。这种情况下我们就很容易会决定把操作A的JS代码与操作B的代码放在一起。有时它们本来就属于同一个逻辑,拆开反而更难处理。然后有一天,A出了问题,你用firebug定位到了A元素的某个属性被错误改变了,然后你就发现,根本没有任何有效线索让你快速找到B处的代码(因为JSF生成的clientId过于繁琐,程序员们往往通过css class或父子兄弟关系来做选择器,你无法确定该如何进行全局查找)。
当然还有其他一些潜在问题,比如说JSF的局部渲染往往产生大量html代码,在流量敏感的移动应用中就不容忽视了。虽然我们公司产品尚未承诺全面支持移动平台,但可以预见这个趋势是无法避免的。
那么从另一个角度看,为何不干脆全面改成RESTful加客户端MVVM。这主要在于JSF在安全性和权限处理上有绝大便利和优势。只要一个组件的rendered属性为false,它内部的command组件就绝对不会被执行。只要一个input组件的readonly为true,它的值就绝对不会被更新到模型中去。只要f:selectItems中不存在某个选项,它依附的select组件就不能对这个选项的值进行decode,自然也无法更新到模型中。换而言之,非常自然地避免了恶意请求的侵袭。不使用JSF的话,我们就要在安全性和权限检查上耗费不少精力。更何况,在大部分场景下,使用现成的组件库是非常方便的。
由此可以总结出这次整合的需求:
1. 解决JS代码碎片化,命名空间混乱的问题
2. 解决难以从元素反查JS代码的问题
3. 提供相对组织良好的客户端API
4. 减少局部渲染响应流量
5. 保留JSF的提交方式,确保服务器的权限检查能正确执行
6. 确保原有的JSF组件能无缝正常使用
然后谈谈客户端MVVM(或MVC)框架的选择。
首先需要大量JS代码来建立模型的Backbone.js可以出局了,公司里的Java程序员不可能愿意手写大量额外JS代码,毕竟我们已经有了完善的服务器端数据模型,客户端模型属于锦上添花,不能喧宾夺主了。
其次使用静态模板的Ember.js和React.js也可以离场了,静态模板难以与JSF组件整合。况且先写模板再引用的方式也不符合原本的开发习惯。
本来Angular.js给我眼前一亮的感觉,但使用下来(用其做了几个内部项目和一个facebook应用)发现,体系太封闭了。一旦让Angular.js接管了,想绕过它进行局部渲染,甚至仅仅是使用JSF的正常方式提交(而不使用它自带的提交功能)后要更新模型值都极其繁琐。两个ViewModel之间通信只能走Observer模式,对Java程序员们来说代码量太大。据说性能一般(建议同页面不超过2000个绑定),而且自带了我感觉根本无法推广的包依赖功能。只好忍痛割爱了。
然后顺着Angular.js,又找到了”到哪儿网“的Avalon.js。这个框架老实说做的不错,借鉴了Angular.js与Knockout.js的不少优秀特性。我用它来做了一些内部项目,甚至局部用到了生产环境的一些次要功能上,都很顺手而且基本没有出什么问题。但最大问题是,这东西没有英文网站和文档。要在团队里推,跟项目经理一谈就被否决了:”你先教会我普通话再说……“。
另外,这个项目的版本概念也比较模糊,没有开发版与稳定版之分,主干上一边改Bug,一边加新特性,等你报的bug改好了,一但更新版本,没准新增的特性又出bug了,没个尽头。但总的来说,这个项目还是不错,一些个人项目或内部项目我还是倾向于使用。
最后,Avalon不能用,就只能退而求其次,改用Knockout.js了。Knockout.js的原生版本基本上实现了Avalon 80%的特性 (或者说,Avalon借鉴了Knockout后又添加了20%的新特性)。但其扩展性感觉要比Avalon好一点,自定义binding非常方便,所以我又只好通过自定binding把这20%的特性加回去。但Knockout.js对ViewModel嵌套的支持很弱,这点暂时无解,好在在JSF整合中,JS模型本就力求简单,不能嵌套也问题不大。最烦人的问题是,Knockout.js在语法层面上区分模型中的普通变量和监控变量,普通变量直接访问,监控变量需要作为getter/setter函数访问,你使用时必须记住模型中的某个属性到底是什么类型,颇令人不爽(相对来说,Avalon想尽办法把所有属性都统一成直接访问了,在旧版IE浏览器甚至动用了VBScript来帮忙,使用上方便不少)。
不管如何,经过一番权衡,最后选定了Knockout.js。下一节开始整合了。
-- 本节完 --