分享

avalon:小而美,轻量级前端MVVM框架

 萤火与皓月 2015-11-12

这几年,国内在造轮子上非常狂热,几乎每个大公司都有自己的框架,阿里的Kissy和Arale,腾讯的JX,百度的Tangram,360的QWrap,甚至每个部门每个小组都有自己的框架或库。但纵观它们在GitHub上极少的Star或Pull Request数,avalon虽只有2000多个Star,但也算很成功了。再看一下应用情况,许多框架只在公司内部使用,avalon在百度、阿里、腾讯、盛大、搜狐、银联都有使用,能统计到公司达40多家,偷偷用的公司保守估计有300多家(比如某些公司只将它的名字与核心方法改一下,以避免版权问题),这是体现avalon成功的另一点。大家多比较崇尚国外技术,加之国内框架作者精力有限,而为之贡献代码的人少之又少,所以说avalon能有今天的成绩,实属不易。

基于MVVM数据绑定

avalon的诞生与崛起完全是一个无心插柳的结果。avalon最早发布于2012年9月15日,当时是以mass Framework一个子模块的形式存在,后来作为我开发的第三个框架独立了出来。

Mass Framework的失败让我深深感到,如果没有太多创新点,与外国jQuery、MooTools这些框架进行同质化竞争只有死路一条。国外框架作者时间充裕,精力旺盛,而且贡献者众多,国内许多大公司的框架大多吃亏在这一点上。正如我在《JavaScript框架设计》一书中总结的三大金规之一所说,必须出奇制胜。

在盛大做通行证期间,我被众多视图搞得晕头转向,同一份数据,因为入口不同往往需要呈现不同的外观。人工做数据与视图的分离全依赖于编码者的功力,显然非常不可靠,一般的前端会给你写一堆if、else语句,维护性非常差。于是我不得不向后端取经。

在前端被IE6统治的十年间,后端一直接手前端的活儿,因此一定需要发明某种方案来处理该情况。果不其然,早在2005年,微软就已经发明了MVVM这一神器。其核心是绑定。绑定是视图上一些特殊的标记,它可以实现如jQuery的on、css、prop、html、text、each等功能。如果再往前看,XSLT其实也做了相同的事情。

avalon也是基于这个理论建立起来的,在视图上添加一些标记,然后在JavaScript中对对象的属性进行监听,当这些属性发生变化,以观察者模式,通知视图进行变更;视图上也可以绑定一些事件,然后当这些事件被触发时,反过来修改这个对象的特定属性,这叫双向绑定。如果绑定做得足够强大,写业务的同学无需写任何关于DOM的逻辑,就能实现以前用jQuery才能搞定的操作,这叫做操作数据即操作DOM。所有需要操作的数据被集中一起管理,它们合并成一个叫ViewModel的东西,而原来的HTML页面只做了一些修改,称之为View,而View本身是一个动态模板(Live Template)。模板是一个非常有用的东西,没有它,大家就会在JavaScript代码里拼凑代码,或者项目组每个人都选用自己的模板,造成维护问题。

ViewModel里放着许多属性与方法,有的属性是后端数据库不存在的字符串,有的还可能依赖其他属性才能计算出自己的值,这些属性,我们通常会做成计算属性。这些都是MVVM最重要的概念。

出于Knockout,比Angular更易用

在我写avalon第一行代码时,前端MVVM框架就只有Knockout,彼时Angular尚在Google内部运行,但外界对它一无所知。因此avalon的参照物只有Knockout。

Knockout监听一个对象的属性变动非常怪异,成本也非常高。它使用一个叫ko.observable()函数进行包装,换言之,原来是字符串的属性,在Knockout的ViewModel里就变成了一个函数,不单是这样,所有需要的监控也全部变成了函数。有没有可能让原来的字符串仍继续保持其原来的类型呢?我在IE8发现了Object.defineProperty这个新API,但它只能应用于DOM节点,在其它浏览器下,它能劫持普通JavaScript对象属性的赋值取值行为,请见下面代码1。

  1. var a = {}  
  2. a.b = 1  
  3. console.log(a.b) //1  
  4.   
  5. Object.defineProperty(a, "b", {  
  6.   get: function(){  
  7.     return this.__a + 100  
  8.   },  
  9.   set: function(v){  
  10.     return this.__a = v  
  11.   }  
  12. })  
  13.   
  14. a.b = 10  
  15. console.log(a.b)//返回110,而不是10  

代码1

如何使IE6~8兼容Object.defineProperty,便成了关键。在《JavaScript框架设计》中,我列举了数种方法,并比较了其优缺点,最终建议使用VBScript的Set、Get、Let语句。在IE下,我们可以在JScript混写VBscript,因此我们可以创建一个VBscript对象,把需要监控的属性全部放到这个VBScript对象,就能实现相同的效果。当然,性能会有所损失,但做到对属性劫持这一步,我们就可以通过观察者模式同步视图了。

大家现在唯一想知道是这东西容不容易上手,能不能Hold住非常复杂的需求呢?答案是肯定的。我上家公司使用了Angular,早期代码全由后端人员写,结果全部掉到坑中,为了覆盖Angular所有的功能,avalon被逼添加了大量近似API,但比Angular更易用。总之,从最开始起,avalon就搞定了浏览器兼容性与易用性这两大问题。

此外,还有一个大问题,就是API的向前兼容,俗称稳定性问题。Angular是一个反例,每一个小版本都不向前兼容(它在1.08时兼容IE6~8; 1.2时需要打补丁兼容旧式IE; 1.3摒弃对旧式IE的兼容,直接在源码中删除所有兼容代码,因此所有补丁方案都无力回天,并且不支持全局Ctrl函数,许多模块需要独立引用;1.4不向下支持动画模块……)。原因很简单,Angular估计在谷歌内部甚少使用,更可能只有一条产品线在做试水,因此作者改起来真是得心应手,没什么内部压力。像avalon在公司应用于十多条业务线,碰到一些脾气不好的同学,好不容易记完所有API,升一下就要重头学,人家肯定不会放过你,下次业务线就不会用你的东西。从avalon1.2起,添加的API就寥寥可数,所有准备废弃的API只是警告不会做实际的裁减。

当然,这也是依仗avalon绑定的足够强大,虽然avalon也提供了类似于jQuery的css、attr、data、 on、offset等方法,但大家都不用,现在纯粹变成框架内部消费了。至于你要的其他乱七八糟功能,对不起,这不是avalon核心库要做的事情,avalon要保证小而美,API越少越好。但是,如果业务线的同学的确很需要这些功能呢,我们有OniUI,各种组件应有尽有,光是不同功能的日历就有三个。

Object.defineProperty实时监控

最后是性能问题,avalon使用Object.defineProperty进行实时监控,不像Angular那样使用脏检测,在性能上可以扛住20000个绑定。但由于数据与监听函数至少是一对一的关系,如果页面上有个三重循环,轻松就能造成上万个绑定。因此,未来还需要引入其他方案。

目前,想到两个优化点。(1)在ViewModel上减少监听函数,Object.observe真是生逢其时。(2)在View减少冗余的DOM操作。avalon更新视图的方法很方便,直接vm.a=1,就可触发一次视图改动。因此必须减少一次事务中对同一个元素的重复操作,Angular有$apply手动触发,avalon可以用$unwatch、$watch,但都不太好用。这难题最后被Facebook的新锐视图库React搞定了,它号称是使用了一种叫Virtual DOM的技术搞定。显然,这答案没有暴露其全貌,其他使用Virtual DOM的库,性能也很难追得上React。

React为了提高性能,其最核心的架子是其基于层次结构的UUID技术。有了它,才能实现节点的最小化更新。比如,一个对象里面有30个键值对,后来更新该对象,换成另外11个新的键值对,这样我们只要去掉页面多出的19个DOM节点,再修改已有11个节点内容或属性即可。如果你的框架使用静态模板来实现这一功能,这30个节点需要重新创建与插入,性能肯定也没有这么好。avalon的问题在于,VBscript对象只要多一个新属性或少一个属性,就需换上一个新的VBscript对象,于是对应区域不得不全部换掉新节点。目前avalon只是在数组上进行优化,对对象(hash)还是比较无力。当然解决方法是有的,就是使用Object.getOwnPropertyDescriptor,但这东西是VBscript也搞不定的高级API,因此我只能用在未来的avalon2上了。当然未来,其实还有Proxy这个超酷的API,它比Object.defineProperty、Object.observe更好用。Proxy让JavaScript拥有比Ruby对象更为强大的反射能力,有了它,真是什么黑魔法都能搞出来,元编程就是魔法的缤纷盛放!

更适合强交互页面

下面所示的代码2是一个经典的表单示例,有输入框与输入框之间的联动(firstName与fullName),输入框与文本之间的联动,Checkbox之间的联动(下方的全选与非全选功能)。通观所有代码,没有一行操作DOM的代码,没有DOM逻辑,意味以后我们将它放到NodeJS环境中做各种测试会非常轻松。此外,也没有看到事件绑定,这正是MVVM的神奇之处。没有事件绑定,avalon是怎么捕捉到用户的操作呢?其实,avalon也使用了事件绑定,只不过偷偷在做,因为事件绑定存在各种各样的兼容问题。比如说,观察输入框的内容变化,要求每一次的字符改动都要触发回调,就算在高级浏览器下,光凭oninput事件也不尽完美,需要配合到compositionstart、compositionend、DOMAutoComplete这些连中级JavaScript工程师都知之甚少的小众事件。至于IE6~9,就需要更多神奇的事件了。因此avalon不打算将这些烦人的细节暴露出来,这也方便自己以后不断重构与优化。此外,MVVM里面最倚重的设计模式是观察者模式,因此你可以看到非表单元素的文本部分也能实现联动,这也说明,MVVM是非常适合应用于那些强交互的页面中。

  1. <!DOCTYPE html>  
  2. <html>      
  3.     <head>          
  4.          <title>avalon 101</title>          
  5.          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">            
  6.          <script src="avalon.js" ></script>          
  7.          <script>      
  8.             var first = 0;              
  9.             var model = avalon.define({                  
  10.                 $id: "test",                  
  11.                 firstName: "司徒",                  
  12.                 lastName: "正美",                  
  13.                 fullName: {//计算属性                      
  14.                     set: function(val) {//setter                          
  15.                        var array = (val || "").split(" ");                   
  16.                        this.firstName = array[0] || "";                               
  17.                        this.lastName = array[1] || "";                      
  18.                     },                      
  19.                     get: function() {//getter,这个必须存在                          
  20.                         return this.firstName + " " + this.lastName;     
  21.                     }                  
  22.                 },                  
  23.                 arr: ["aaa", "bbb", "ccc", "ddd"],                  
  24.                 selected: ["bbb", "ccc"],                  
  25.                 checkAllbool: false,                  
  26.                 checkAll: function() {                      
  27.                     if (!first) {                          
  28.                          first++                               
  29.                          return                     
  30.                     }                      
  31.                     if (this.checked) {                          
  32.                         model.selected = model.arr                      
  33.                      } else {                          
  34.                          model.selected.clear()                      
  35.                      }                 
  36.                 },                  
  37.                 checkOne: function() {                      
  38.                     var bool = this.checked                      
  39.                     if (!bool) {                          
  40.                          model.checkAllbool = false                      
  41.                     } else {                          
  42.                         model.checkAllbool = model.selected.size() ===  
  43.                     model.arr.length   
  44.                     }                  
  45.                   }              
  46.              })     
  47.       </script>       
  48.    </head>      
  49. <body>          
  50.    <div ms-controller="test">              
  51.       <p>First name: <input ms-duplex="firstName" /></p>              
  52.       <p>Last name: <input ms-duplex="lastName"  /></p>              
  53.       <p>Hello,    <input ms-duplex="fullName"></p>              
  54.       <div>{{firstName}} | {{lastName}}</div>              
  55.       <ul>                  
  56.          <li><input type="checkbox" ms-duplex-checked="checkAllbool"  data-duplex-changed="checkAll"/>Select All</li>                  
  57.          <li ms-repeat="arr" ><input type="checkbox" ms-attr-value="el" ms-duplex="selected" data-duplex-changed="checkOne"/>{{el}}</li>              
  58.       </ul>          
  59.     </div>      
  60.   </body>  
  61. </html>         

代码2

代码执行效果如图1。


图1 表单示例

在企业内部ERP、SCM、CRM、BRP等各种系统中,存在大量表格与表单操作,像图2这种表格融合表单的GRID也非常常见,因此非常适合avalon。


图2 表格融合表单的Grid

国内唯一兼容IE6的MVVM框架

avalon最早靠口碑相传,因为国内能兼容IE6的MVVM框架也只有avalon一款。能同时给出移动端、后台系统、Chrome插件、微信公共号的例子,也只有avalon一款。avalon也曾搞过国际化,一个澳洲的朋友在用。不过,avalon的重心还是国内,源码里大段的中文注释,改起来相当吃力,便放弃了。现在,网上可以找到大量avalon的视频教程和详细的教学文章。它还有自己的论坛,完全使用avalon搭建。

目前,avalon已经很少添加新特性了。近几月 ,一直在修改由于用户一些意想不到的使用方式而引发的Bug。其次就是性能改进,我有一个叫avalon.test的项目专门做这些,避免乱优化导致改坏的悲剧。许多新点子已经集中到我的另一个项目avalon2上了,它会添加异步批处理开关,支持CSS3或JavaScript动画,基于自定义标签的Web Components,更好地支持移动端……换言之,更加适应以后Web开发的需求。

不管你们用不用avalon,MVVM绝对是你们值得一试的好东西,因此设法在你们的项目中体验一下双向绑定的高效开发方式吧。

作者简介

钟钦成,网名司徒正美,是中国最早研究加载器、选择器与MVVM的人之一,著有《Javacript框架设计》一书。自栩为穿梭于二次元与二进制间的魔法师,致力发掘各种黑魔法,提升一般前端工程师的生产效率。

本文选自程序员电子版2015年8月A刊,该期更多文章请查看这里。2000年创刊至今所有文章目录请查看程序员封面秀。欢迎订阅程序员电子版(含iPad版、Android版、PDF版)。 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多