使用avalon ms-ui绑定实现基于组件的开发
让日子过得轻松,必须让代码不断往上抽象。avalon的一切就是为这个崇高的目的而迸进——操作数据即操作DOM,远离DOM进行前端开发。javascript之所以在生命的前十年碌碌无为,都是因为开发者被这些兼容性问题搞怕了。在前十年,人们都是用着那些很底层的原生DOM方法,JS方法进行编程,效率异常低下。随着Prototype.js, jQuery等库的出现, 把几乎所有能封装都给封装了, 我们的生活才变得美好起来。但开发企业内部管理系统,却鲜有听说用jQuery来搞,大家都爱用开箱即用的EXT UI框架。UI组件无疑是比jQuery这些DOM操作对象强上一截,是更高程度的抽象与封装。但组件的制定性一般很差,比如说zTree,它就使用插件化来进行扩展。对于目前业界给出的方案,avalon是怎么做的呢?
首先,像ms-class, ms-hover, ms-on其实是在jQuery的事情,不同的是,它没有依赖Sizzle这些动则上千行的选择器引擎,而是通过扫描与转换绑定属性实现。绑定属性会转换一个求值函数,求值函数内部与ViewModel挂钩,因此能即时同步,外部套着一个DOM操作函数,比如说ms-class,就是根据求值函数的返回值的真假来调用toggleClass, ms-hover则是通过绑定mouseenter, mouseleave来toggleClass, ms-checked是处理表单元素的checked属性, ms-visible负责元素的显示隐藏, ms-if负责将元素移出或插入DOM……短短一个绑定属性就包含非常高密度的DOM操作,比jQuery更加write less, do more, do simple!这个园子已经有人在用我的avalon,反映都是很不错的,当然也提了不少改进见意。
其次是组件层面,avalon与angular一样,提供面向组件的开发,但比angular用法简单多了。avalon参考了bootstrap这个twitter开发的UI库,利用data-*属性进行功能定制(当然,它还有其他形式的配置方式)。并且像bootstrap那样,只要引入JS,在相关元素上添加ms-ui="组件名",元素节点就会转换为UI组件,一行代码也不用你写。
ms-ui绑定能给我们带来以下好处:将元素上的绑定属性减到最少,只需添加ms-ui="组件名",或最多添加data-id="xxx", 保证对HTML最少限度的干扰。也就是说,美工与前端有了非常明确的分工。而data-id中的值则是对应ViewModel的ID,我们可以通过此ID在avalon.models[ID]中取得ViewModel,从而实现操作数据即操作DOM。
基于组件,使得我们的表现层变成由一个个“积木”堆积而成。Page规则转向如何协调这些组件的运作,而不是盯着一个个元素节点。不过avalon已经帮你搞定了大部分了。比如说,基本URL远程请求不同的资源,然后转换为ViewModel,然后生成某个页面的区域。还有消息中心(也叫事件中心),由于ViewModel本来就是用观察者模式实现的,一个活生生的消息中心,你不需再造轮子。随着我们项目的深入,我们积累的各种积木也就越来越多,也就是,我们的工作就越轻松,工作效率也就越来越高。
与其他UI库不一样的,avalon编写UI太简单,太方便了,这得益于它强大的双向绑定机制。
拿avalon.tabs组件的源码举例吧:
( function (avalon) { var defaults = { active: 0, event: "click" , //可以使用click, mouseover collapsible: false , bottom: false , removable: false }; avalon.ui.tabs = function (element, id, opts) { var el, tabsParent, tabs = [], tabpanels = []; var $element = avalon(element); //1,设置参数对象options = defaults + opts + $element.data() var options = avalon.mix({}, defaults); if ( typeof opts === "object" ) { avalon.mix(options, opts.$json || opts); } avalon.mix(options, $element.data()); $element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); //2, 清空它内部所有节点,并收集其内容,构建成tabs与tabpanels两个数组 while (el = element.firstChild) { if (!tablist && (el.tagName === "UL" || el.tagName === "OL" )) { tabsParent = el; } if (el.tagName === "DIV" ) { tabpanels.push(el.innerHTML); } element.removeChild(el); } for ( var i = 0; el = tabsParent.children[i++]; ) { tabs.push(el.innerHTML); } //3 设置动态模板 var tablist = '<ul class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header"' + ' ms-class-ui-corner-all="!bottom" ms-class-ui-corner-bottom="bottom" ms-each-tab="tabs">' + '<li class="ui-state-default" ' + ' ms-class-ui-corner-top="!bottom"' + ' ms-class-ui-corner-bottom="bottom"' + ' ms-class-ui-tabs-active="active == $index"' + ' ms-class-ui-state-active="active == $index"' + ' ms-' + options.event + '="activate"' + ' ms-hover="ui-state-hover"' + // float: left; margin: 0.4em 0.2em 0 0; cursor: pointer;这样jquery ui没有封装进去 ' >{{tab|html}}<span class="ui-icon ui-icon-close" style="float: left; margin: 0.4em 0.2em 0 0; cursor: pointer;" ms-if="true" ms-click="remove"></span></li></ul>' ; var panels = '<div ms-each-panel="tabpanels" ><div class="ui-tabs-panel ui-widget-content"' + ' ms-class-ui-corner-bottom="!bottom"' + ' ms-visible="active == $index" >{{panel|html}}</div></div>' ; //4 构建组建的ViewModel var model = avalon.define(id, function (vm) { vm.active = options.active; vm.collapsible = options.collapsible; vm.tabs = tabs; vm.tabpanels = tabpanels; vm.removable = options.removable; vm.activate = function (e) { e.preventDefault(); vm.active = this .$scope.$index; }; vm.remove = function (e) { e.preventDefault(); var index = this .$scope.$index; vm.tabs.removeAt(index); vm.tabpanels.removeAt(index); avalon.nextTick( function () { vm.active = 0; }); }; vm.bottom = options.bottom; }); avalon.nextTick( function () { //5 当这一波扫描过来,再将组建的DOM结构插入DOM树,并绑定ms-*属性,然后开始扫描 element.innerHTML = options.bottom ? panels + tablist : tablist + panels; element.setAttribute( "ms-class-ui-tabs-collapsible" , "collapsible" ); element.setAttribute( "ms-class-tabs-bottom" , "bottom" ); avalon.scan(element, model); }); return model; }; })(window.avalon); |
它基本分为五个步骤,首先怎么也有5个参数给你传过来,element为绑定了ms-ui的元素节点,id为data-id的值,没有框架为你随机生成一个,opts,这个是ms-ui-optsName="uiName"的一个参数,opts就是框架在众多ViewModel通过hasOwnerProperty操作分别出来的参数对象。然后第一步,就是设置配置对象,它由defaults + opts + $element.data()组成,制定性应该非常强与灵活。
第二步,将element元素的内部节点进行处理,有时它们也有绑定属性,但我们不需要立即处理它们——扫描器是从上到下扫描,扫描过后来移除它们,因此阻止它们此时被扫描到——最好方法是将它们移出DOM树。这个过程可能还要做些操作,随你喜欢。
第三步,编写UI的HTML结构。
第四步,定义ViewModel,这时它的ID为我们上面的传参。
第五步,重新开始扫描,但这时,我们不需要整个页面都扫描了,只从这个元素开始就行。
有关这个tabs的示例可到这里查看。
这里有两个重点,一个是扫描。扫描总是从上到下,从左到右,想让它不被扫描,有三个办法,ms-skip,这几乎是永久性的,ms-important,只有ViewModel匹配才进行此区域,最后一个是移出DOM树。扫描不但是为了转换绑定,还起到存储这些关键节点的作用(竟然这些节点做了绑定,说明要通过JS处理,而在以前这些都是通过选择器引擎来做,但一般的jQueryer很少自觉将它们缓存起来,每次用到时都重新选择,不断地遍历DOM树)。第二个是UI组件的ViewModel的构建,它是我们远离DOM编程的关键。它拥有所有关键参数与方法,相当于后端的XML配置文件。当然你也可以什么也不做,defaults其实已经调试好一切了。