迷你MVVM框架 avalonjs 組件編寫指南

avalon通過半年的宣傳,已經有很多公司在使用avalon應用於它們內外網應用或移動項目,比較大牌的客戶有百度,搜狐,金山,邊緣,去哪兒……最近成爲去哪兒的前端架構師後,掌握更多資源,能夠隨使抓我的幫忙寫文檔作測試寫UI,以前的種種誥病都會迅速被解決掉的。所以你們不須要擔憂什麼,放心試用avalon吧!javascript

說說去哪兒的狀況吧,如今我所在的酒店部門有一個40多號人的前端組,而且不斷壯大。很早以前,他們就用個人avalon重構他們的組件庫OnionUI 。對於一個公司來講,組件庫是一個重要的財富,能讓咱們更快地進行開發迭代。至於通常的業務開發,藉憑avalon操做數據即操做DOM的機制,也是不費吹灰之力就能搞定。大頭的仍是組件,像阿狸的Kissy,成熟無比,組件應有盡用,這是咱們奮鬥的目標。由於去哪兒前端團隊很早就使用avalon的UI綁定了。html

avalon的UI綁定的語法以下:前端

  ms-widget="uiName, id?, optsName? "
  • uiName,必選,必定要所有字母小寫,表示組件的類型
  • id 可選 這表示新生成的VM的$id,方便咱們從avalon.vmodels[id]中獲取它操做它,若是它等於$,那麼表示它是隨機生成,與不寫這個效果同樣,框架會在uiName加上時間截,生成隨機ID
  • optName 可選, 配置對象的名字。這是指框架會找離它最近那個VM做爲目標,而後在上面找與它同名的一個對象。若是你沒有指定, 那麼這個配置對象的名字與組件的名字同名。
<div ms-controller="xxx">
     <div ms-controller="yyy">
        <div ms-widget="dialog,$,$opt">
        </div>
    </div>
</div>
<script>
   avalon.define("xxx", function(vm){
        vm.uuu = "ssdf"     
   })
   avalon.define("yyy", function(vm){
        vm.dfd = "sdfdf"
        vm.$opt = {//這個對象是做爲dialog的配置對象而存在
            width: 400,
            height: 200,
            toggle: false
        }
   })
</script>

編寫一個組件,咱們很是注重它的可配置性。avalon的UI綁定擁有三處用於定義配置的地方。第一處,就是上面提到的,在一個已存的VM中定義一個對象(最好將它定義不可監聽的,以$開頭或放在$skipArray數組中)。 它至關於一個父類。讓一組UI共享相同的配置。第二處是位於UI綁定的構造器中,咱們能夠經過avalon.ui[widgetName].defaults訪問到。它是讓同一種組件的全部實例都共享相同的配置。第三處是在定義ms-widget所在的元素上,添加一些HTML5 data-*屬性,格式爲data- widgetName - optionName,好比你想爲suggest組件定義一個叫toggle的配置項,那麼就應該寫做data-suggest-toggle,若是是一個叫currentValue,那麼要將它改爲"連字符風格",即將大寫變小寫前面再加一橫槓,data-suggest-current-value。它們是用來制定當前UI實例的。java

 <input ms-controller="bbb" ms-widget="datepicker"   data-datepicker-date-format="yyyy-MM-dd">

好了,咱們正式介紹如何編寫組件自己。咱們要記住一點,avalon全部操做都與掃描機制息息相關,就像jQuery喜歡把它的API選擇器引擎綁架在一塊兒。爲何這樣說呢,由於視圖與代碼分定義在不一樣的地方,只有通過掃描後,視圖中的綁定纔會挾持它們所在的元素節點與VM關聯在一塊兒。框架會默認在domReady以後掃描一次。若是這時咱們用到的組件所對應的JS文件尚未加載好,那麼當加載好後咱們須要本身手動掃描。git

require("avalon.dialog", function() {
    avalon.define("test", function(vm) {
        vm.$skipArray = ["dialog"]
        vm.dialog = {
            buttons: [{text: "ok"}, {text: "cancel"}]
        }
    })
    avalon.scan()
})

組件大抵都是如下樣子,留意一下里面的註釋:github

//avalon 1.2.5 2014.4.2
define(["avalon",
    "text!avalon.tabs.tab.html", //這是組件用到的VM
    "text!avalon.tabs.panel.html",
    "text!avalon.tabs.close.html"],
        function(avalon, tabHTML, panelHTML, closeHTML) {

            var widget = avalon.ui.tabs = function(element, data, vmodels) {
                var options = data.tabsOptions//★★★取得配置項

                var vmodel = avalon.define(data.tabsId, function(vm) {
                    avalon.mix(vm, options)//這視狀況使用淺拷貝或深拷貝avalon.mix(true, vm, options)
                    vm.$init = function() {//初始化組件的界面,最好定義此方法,讓框架對它進行自動化配置
                        avalon(element).addClass("ui-tabs ui-widget ui-widget-content ui-corner-all")
                        // ★★★設置動態模板,注意模塊上全部佔位符都以「MS_OPTION_XXX」形式實現
                        var tablist = tabHTML
                                .replace("MS_OPTION_EVENT", vmodel.event)
                                .replace("MS_OPTION_REMOVABLE", vmodel.removable ? closeHTML : "")
                        //決定是重複利用已有的元素,仍是經過ms-include-src引入新內部
                        var contentType = options.contentType === "content" ? 0 : 1
                        var panels = panelHTML.split("MS_OPTION_CONTENT")[contentType]
                        element.innerHTML = vmodel.bottom ? panels + tablist : tablist + panels
                        element.setAttribute("ms-class-1", "ui-tabs-collapsible:collapsible")
                        element.setAttribute("ms-class-2", "tabs-bottom:bottom")
                        avalon.scan(element, [vmodel].concat(vmodels))

                    }
                    vm.$remove = function() {//清空構成UI的全部節點,最好定義此方法,讓框架對它進行自動化銷燬
                        element.innerHTML =  ""
                    }
                    //其餘屬性與方法
                    vm.tabs = []
                    vm.tabpanels = []
                    vm.disable = function(index, disable) {
                       //具體實現
                    }
                    vm.enable = function(index) {
                      //具體實現
                    }
                    vm.add = function(config) {
                      //具體實現
                    }
                     vm.remove = function(config) {
                      //具體實現
                    }
                    vm.activate = function(event, index) {
                       //具體實現
                   }
                })
                return vmodel//必須返回組件VM
            }
            widget.defaults = {//默認配置項
                collapsed: false,
                active: 0, //默認打開第幾個面板
                event: "click", //切換面板的事件,移過(mouseenter)仍是點擊(click)
                collapsible: false, //當切換面板的事件爲click時,
                bottom: false, //按鈕位於上方仍是上方
                removable: false, //按鈕的左上角是否出現X,用於移除按鈕與對應面板
                activate: avalon.noop, // 切換面板後觸發的回調
                contentType: "content"
            }
            return avalon
        })

這裏須要着重留意的是data裏面有兩個屬性,一個叫"組件名+Options",是一個對象,若是它裏面有widget+"Id"這個屬性,那麼新生成的VM就是用它做爲它的$id。 一個叫"組件名+Id",就是新生成的VM的$id。組件必須註冊到avalon.ui上,它的構造器必須定義一個叫defaults的默認配置項。數組

另外,因爲掃描從外到裏,當它掃描了ms-widget所在的元素,若是此元素裏面還有子元素, 而且它們的綁定屬性須要用到新VM的某一些字段,這時讓它繼續掃下去就有出錯的危險。咱們能夠先它的全部子元素放到一個文檔碎片中。待到新VM中出來後,再插回原地置,而後手動掃描。架構

在avalon1.2.4中,ms-widget所在的元素還添加了一個msData對象,保存着全部ms-*屬性,此外data屬性還添加了ms-widget-id屬性,保存着新生成的組件VM的ID。咱們能夠靠它們作更多的事。app

在avalon1.2.5中,要求組件做者最好爲VM添加$init, $remove方法,方便自動化初始化組件與對它進行銷燬。銷燬的條件是定義ms-widget的那個元素被移出DOM。有時咱們不得不對此元素進行移動(移動時也會發生移出DOM樹的操做),這時就須要注意啦,爲元素添加一個msRetain,值爲true,爲能夠避銷自動化銷燬。當它再插回DOM樹,記得將此屬性去掉。框架

下面是ms-widget的部分源碼

                data[widget + "Id"] = args[1]
                data[widget + "Options"] = avalon.mix({}, constructor.defaults, vmOptions, widgetData)
                element.removeAttribute("ms-widget")
                var vmodel = constructor(element, data, vmodels)//防止組件不返回VM
                data.evaluator = noop
                element.msData["ms-widget-id"] = vmodel.$id
                if (vmodel.hasOwnProperty("$init")) {//初始化組件
                    vmodel.$init()
                }
                if (vmodel.hasOwnProperty("$remove")) {
                    var offTree = function() {//CG回收
                        vmodel.$remove()
                        element.msData = {}
                        delete VMODELS[vmodel.$id]
                    }
                    if (supportMutationEvents) {
                        element.addEventListener("DOMNodeRemoved", function(e) {
                            if (e.target === this && !this.msRetain) {
                                offTree()
                            }
                        })
                    } else {
                        element.offTree = offTree
                        launchImpl(element)//若是不支持DOMNodeRemoved事件,使用一個全局的定時器進行輪詢
                    }
                }

下面是avalon.dialog的部分源碼


           vm.$init = function() {
                //CSS自適應容器的大小
                if (options.height === "auto") {
                    var style = element.style
                    style.width = style.height = "auto"
                    style.minHeight = element.clientHeight + "px"
                }
                element.msRetain = true //▲▲▲▲防止被銷燬
                vmodel.parent = vmodel.parent === "parent" ? element.parentNode : document.body
                element.removeAttribute("title")
                avalon(element).addClass("ui-dialog-content ui-widget-content")
                dialog.appendChild(element)

                avalon.ready(function() {
                    vmodel.parent.appendChild(dialog)
                    element.msRetain = false//▲▲▲▲▲

                    vmodel.fullScreen = /body|html/i.test(dialog.offsetParent.tagName)
                    if (vmodel.fullScreen) {
                        dialog.setAttribute("data-drag-containment", "window")
                    }
                   //。。。。。。。略
                })
            }

爲了方便你們編寫組件,avalon還暴露了 getWidgetData, parseExprProx等接口,讓你們自行解析綁定屬性。

你們能夠參考下面官方組件編寫本身的UI組件。

相關文章
相關標籤/搜索