讓日子過得輕松,必須讓代碼不斷往上抽象。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, vmodels, 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其實已經調試好一切了。
