基于vue.js的漸進式組件嘗試

robert0512 7年前發布 | 10K 次閱讀 Vue.js CSS Vue.js開發

我們有個內部運營系統,是基于keenthemes的一個主題進行開發的,而這個主題就是基于jQuery+bootstrap+jQueryPlugins 進行的定制主題,用于顯示各種圖表和曲線。所以,這個系統的特點就是,加載了一堆js和css進行堆砌組合,以及內容被一層層的標簽和樣式包圍。長這個樣子:

這種寫多了確實就是體力活,一般的開發過程也就是復制粘貼,而且為了不出意外的問題,有用的沒用的js script和css link都是直接復制的,反正放內部用一般忽略加載的延遲。

所以,有沒有辦法把各種標簽打包成一個新的標簽,css和js的依賴也打包在一塊呢?就像html提供的基礎標簽一樣,放個圖片,那放個img就可以了。

這個肯定是有的,痛的人那么多,所以現在已經web components草案在討論中,chrome等現代瀏覽器也相繼地提供了shadow DOM, custom Elements的特性支持,google還推出了polymer項目。不過說實話,要是一個項目從頭開始折騰,還是可以考慮的,但是一想到又要用npm安裝一堆依賴,也是頭大。

我需要的方案是,在已有的項目上,門檻低點,依賴很少的東西,還能包容已有的開發模式。比如說,我就把一堆標簽用一個新的標簽替代,然后解析頁面的執行js腳本還原回來,這是最基本的一步。

在我有限的認知里,vue.js就是最簡單的滿足需求的選擇。為什么不用react?一出來就令人驚呼的jsx,我還是嫌依賴太多。我就想要一種old school的方式,引用一個js,然后馬上寫,隨便寫。而且,vue.js提供的雙向綁定功能也很適合,不用滿個頁面里寫id然后腳本里再去各種引用。還有一點,運營系統天生以頁面為模塊劃分,引入的js只充當controller的角色就可以了。

以datepicker的jQuery插件為例,下面代碼放components.js里:

Vue.component('datepicker', {
    template: '\
        <div class="input-group input-small date" data-date-format="yyyy-mm-dd" :data-date-end-date="enddate">\
            <input type="text" class="form-control" readonly="">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    '
})

先假設頁面上已經加載了需要的css和js,那么在頁面上就可以直接使用

<datepicker></datepicker>

而我們除了需要加載components.js和vue.js之外,其它照舊。當然就是包含datepicker標簽的元素需要加載到一個Vue實例中。

然后,再加強對這個標簽的控制,比如說傳入值,獲取值以及對于datepicker事件的處理等,使得它功能更加完整。

Vue.component('datepicker', {
    props: ['value'],
    template: '\
        <div ref="picker" class="input-group input-small date" data-date-format="yyyy-mm-dd">\
            <input type="text" class="form-control" readonly="" :value="value">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    ',
    mounted: function() {
            var self = this;
            $(this.$refs.picker).datepicker({orientation: "right top", autoclose: true});
            $(this.$refs.picker).on('changeDate', function(e) {
                self.$emit('input', e.format('yyyy-mm-dd'));
            })
        }
})

以上示例代碼中,模板新加入ref屬性,就可以通用this.$refs引用原始的DOM節點,而props數據value的傳入以及input事件的觸發,則是為了實現神奇的 v-model,看:

<datepicker v-model='selectedDate'></datepicker>

如此一來就對datepicker父組件的 selectedDate 實現了雙向綁定。其實v-model也只是個語法糖,展開來,其實就是:

<datepicker :value='selectedDate' @input='selectedDate=arguments[0]'></datepicker>

另外,示例代碼中是在Vue實例的生命周期的mounted階段(DOM節點掛載完成)進行了事件綁定,這是為了確保編譯后節點的已經正常存在。

然后,到這里,仍然是基于頁面上已經手動加載了依賴的css和js,這個組件其實還不算完整。事實上,我們還希望能夠只要引用這個組件,依賴也要自然地滿足。而這個,無非就是在合適的時候把依賴的css和js動態加載進來。這個“合適的時候”我仍然選擇的是"mounted"階段,為什么?感覺自然而然呀。

可是,動態加載CSS和JS的難點其實是,如何判斷已經資源加載完成?兼容性仍然是個問題。所以,我又假設了,我們就只使用chrome吧~~ 理想的情況是,加載的資源并行請求,然后渲染執行的時候則按先后順序,這明顯沒那么完美的事情。所以,對于CSS文件,我仍然并行加載,那么依賴先后順序的樣式有可能有問題,要保證順序只能串行化,而且由于瀏覽器緩存的存在,在我有限的測試次數中,肉眼上還沒有看出問題。而js的話就不得不優先考慮加載順序的問題了,所以最后選擇串行加載,而且是忽略了失敗的情況。

解決依賴這種事情,是很個組件都需要的功能,所以采用了mixin, 可以大大地減少重復代碼,看起來就像是聲明了一個接口,有依賴的組件只要按需實現即可:

Vue.component('datepicker', {
    mixins: [DepMixins],
    props: ['value'],
    template: '\
        <div ref="picker" class="input-group input-small date" data-date-format="yyyy-mm-dd">\
            <input type="text" class="form-control" readonly="" :value="value">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    ',
    methods: {
        needDeps: function() {
            var deps = ['/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css',
                        '/assets/global/plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js'];
            return deps;
        },
        loadedDeps: function() {
            var self = this;
            $(this.$refs.picker).datepicker({orientation: "right top", autoclose: true});
            $(this.$refs.picker).on('changeDate', function(e) {
                self.$emit('input', e.format('yyyy-mm-dd'));
            })
        }
    }
})

而DepMixins長這樣子:

var DepMixins = {
    mounted: function() {
        if (!this.needDeps) return;
        var deps = this.needDeps();
        if (!deps) return;

        var cb = this.loadedDeps;
        var compName = Vue.util.formatComponentName(this);
        if (typeof DepMixins.comps[compName] === 'undefined')
            DepMixins.comps[compName] = [cb];
        else if (DepMixins.comps[compName] === 'loaded')
            return cb && cb();
        else
            return DepMixins.comps[compName].push(cb);

        // 假設css都在前面,而js是按照依賴順序排列在后面
        for (var i=0; i<deps.length; i++) {
            var dep = deps[i];
            if (dep.endsWith('.css')) {
                visd.loadCSS(dep);
                continue;
            }

            next(i);
            break;
        }

        function next(i) {
            var dep = deps[i];
            if (!dep) return clearQueue();

            visd.cachedScript(dep).then(function() {
                return next(i+1);
            });
        }
        // will only called once
        function clearQueue() {
            var list = DepMixins.comps[compName];
            // if (list === 'loaded') return;
            for (var i=0; i<list.length; i++)
                list[i] && list[i]();
            DepMixins.comps[compName] = 'loaded';
        }
    }
}
DepMixins.comps = {};

可以看到,我又偷了懶,只完成了核心功能,連css和js的樣式都只是自己約定了一下。visd.loadCSS和visd.cachedScript分別只是普通的加載CSS和JS的函數包裝。

visd.loadCSS = function(src) {
    $("<link>").attr({rel: 'stylesheet', type: 'text/css', href: src}).appendTo(document.head);
}
visd.cachedScript= function(url, options) {
  // allow user to set any option except for dataType, cache, and url
  options = $.extend(options || {}, {
    dataType: "script",
    cache: true,
    url: url
  });

  // Use $.ajax() since it is more flexible than $.getScript
  // Return the jqXHR object so we can chain callbacks
  return jQuery.ajax(options);
};

最后,這個datepicker組件就算完成了。只需要新增加一個vue.js的依賴,而且還減少了頁面上其它不明所以的資源文件引用,其它照舊,就算來個后臺同學來看html代碼,相信都能看懂,能手寫。原有的開發環境也不需要任何更新,只用文本編輯器也照樣敲代碼。

再來兩個經典例子:

watch字段的經典在于,模板中并沒有引用到rows這個變量,那么vue實例也就不會把它加入watch列表,當父組件傳入的rows變化的時候,data-table組件什么都不知道也就不會更新了,所以需要手動添加到watch列表中。

kee-modal中使用 了slot 標簽,叫做內容分發,是web components spec的一個proposal(不會翻譯),用于組件中的組合,也就是說我可以這樣子用keen-modal:

<keen-modal>
    <div>想放點什么?</div>
    <datepicker></datepicker>
</keen-modal>

果然很方便!

 

來自:http://imweb.io/topic/5848e62e9be501ba17b10a9c

 

 本文由用戶 robert0512 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!