knockout + easyui = koeasyui

     在作後臺管理系統的同窗們,是否有用easyui的經歷。雖然如今都是vue、ng、react的時代。但easyui(也就是jquery爲基礎)仍是佔有一席之地的。由於他對後端開發者太友好了,太熟悉不過了。要讓一個後端開發者來理解vue或者是react的VNode、狀態器、組件等,都是有那麼一點點的爲難(反正我轉型時,對這些都是頗有困惑的)。今天我想試着解決這樣一個問題,如:將knockout 與 你們熟悉的easyui結合在一塊兒。讓easyui具備MVVM的能力,也有不使用easyui的特性,看你們是否喜歡這一口。前端

1、項目介紹說明

項目語言:typescriptvue

項目地址:https://gitee.com/ko-plugins/koeasyuinode

初級效果:react

望你們給予評論和支持。jquery

2、如何將easyui轉換爲ko的組件

     再前幾年用ko的時候,因爲他沒有組件的支持(由於當時沒有組件的概念)。至到react、vue提出和引用了組件的概念,以及將此概念深刻到每一個前端開發者的心裏後。ko也提供了組件的支持。2017年看這個新特性的時候,就讓我有改造easyui的衝突。當時苦於對ko和easyui的理解不深刻,硬是沒有找到突破口。今天終於讓我找到。git

2.1 easyui組件如何註冊到爲ko組件

     ko提供了components.register方法,用於註冊一個組件。此方法接受一個字符串的名稱,以及一個對象(至少包含一個template或viewModel屬性),其中viewModel能夠是一個對象,也就是一個function。本人就利用了能夠爲function這一點。根據easyui的組件名動態建立一個function,而後賦值給viewModel,代碼片斷以下:ajax

let plugins = this.easyui.plugins;
        //動態生成一個function的類
        plugins.forEach(pluginName => {
            let defaults = this.jquery.fn[pluginName].defaults;
            let methods = this.jquery.fn[pluginName].methods;
            if(defaults){
                //options必需要是獨立的,事件(放原型上),方法能夠原型鏈上的
                let props = Object.getOwnPropertyNames(defaults);
                //方法
                let methodKeys = Object.getOwnPropertyNames(methods);
                this.option.ko.components.register(`ko-${pluginName}`,{
                    template: '<div></div>',
                    viewModel: EasyuiHelper.createEasyui(props, methodKeys)
                });
            }
        });

 

2.2 easyui組件的配置和方法怎麼變成ko組件的參數和方法

上一步驟中的EasyuiHelper.createEasyui方法,就是實現對easyui組件的建立,以及參數的響應和方法的綁定,算是本插件的核心。typescript

export  class EasyuiHelper{
    static createEasyui(props:Array<string>, methods):any{
        let tmpClass = class { 
            public $dom:JQuery;
            public name:string;
            constructor(params, componentConfig){ 
                this.name = componentConfig.element.nodeName.toLowerCase().replace('ko-', '');
                this.$dom = $(componentConfig.element).find('div');
                //綁定方法,方法還須要繼承組件支持的方法的綁定
                Object.getOwnPropertyNames(methods).forEach(index=>{
                    if(!$.isNumeric(index)) return true;
                    let methodName = methods[index]; 
                    this[methodName] = ()=>{
                        let args = Array.prototype.slice.call(arguments);
                        args.unshift(methodName);
                        return this.$dom[this.name].apply( this.$dom, args);
                    }; 
                });
            }            
            /**
             * 根據參數建立組件的配置對象
             * @param options 配置參數 
             */
            private createOptions(options){
                let opt = null;
                if(options){
                    opt = Object.create({});
                    Object.getOwnPropertyNames(options).forEach(optKey=>{
                        let tmpOpt = options[optKey];
                        if(props.indexOf(optKey) > 0 && ko.isObservable(tmpOpt) ){
                            opt[optKey] = ko.unwrap(tmpOpt);
                        }else{
                            opt[optKey] = tmpOpt;
                        }
                    });
                }
                return opt;
            }
            /**
             * 繪製組件
             * @param options 
             */
            public paint(options:any){
                let opt = this.createOptions(options);
                this.$dom[this.name](opt);
            }
            /**
            * 重組件
            * @param options 配置項
            * @param $dom dom元素對象
            */
            public repaint(options:any){
                let $parent = this.$dom.parent();
                this.$dom[this.name]('destroy');
                let $dom = $('<div></div>');
                let opts = this.createOptions(options);

                $parent.append($dom);
                $dom[this.name](opts);
                this.$dom = $dom;
            }
        };
        return tmpClass;
    }
    /**
     * 根據dom獲取上下文
     * @param dom dom節點 
     */
    static getContextFor(dom:HTMLElement){
        return ko.contextFor(dom);
    }
}

    代碼量很少,其主要思路就是,動態建立一個類(其實js中的類就是function)。構造函數中獲取到dom,以及組件名稱。而後將easyui的方法綁定到類實例上。而後對外提供paint和repaint兩個方法進行組件的繪製和重繪。但這個時候又出現了另外一個問題,何時進行繪製重繪呢?後端

2.3 配置參數改變後,如何即便反饋給easyui

這一步就是解決繪製和重繪的問題。這裏咱們要了解一個ko的loader的概念,他至關因而組件渲染器向外提供的勾子,能夠自定義一些內容。ko的loader提供了以下四個勾子:app

getConfig:獲取組件配置信息

loadComponent:加載組件時的勾子,這裏咱們可使用利用require的異步組件加載什麼

loadTemplate:加載模板,固然你的經過ajax向後端接口獲取模板信息

loadViewModel:加載組件視圖對象(這是咱們要重寫的方法),經過此處的重寫,讓組件渲染器建立咱們指定的類。並執行執行的繪製或者是重繪方法。

export class EasyuiLoader{
    public factory:IGenerate;
    constructor(factory: IGenerate){ 
        this.factory = factory;
    }
    getConfig(name:any, callback:any){
        callback(null);
    }
    loadComponent(name:any, componentConfig:any, callback:any){
        callback(null);
    }
    loadTemplate(name:any, templateConfig:any, callback:any){
        //這裏作一些視圖不顯示的控制,在渲染數據後,進行視頻的展現
        callback(null);
    }
    loadViewModel(name:any, viewModelConfig:any, callback:any){
        //到這裏,視圖都是已經呈現好的
        //這裏要產生兩個生命週期:渲染數據前、渲染數據後,以及一個視圖重繪的事件
        var nViewModelConfig = (params, componentConfig) => {
            let vm = new viewModelConfig(params, componentConfig);
            let name;
            vm = this.factory.generate(name, params, vm);
            return vm;
        }
        callback(nViewModelConfig);
    }
}

如下是factory.generate的源碼:

generate(componentName: string, params: any, viewModel: any):any {
        let first = true;
        viewModel.paint(params.options || {});
        //監聽params的變化變化
        ko.computed(function(){
            let opts = params.options; 
            let changeOpts = new Array<any>();
            let reflows = new Array<any>(); //能夠經過方法來進行配置改變的參數
            Object.getOwnPropertyNames(opts).forEach(key => {
                let param = opts[key];
                let tmp = ko.unwrap(param);
                //探測監控對象有變化的屬性,區分那些能夠用方法進行改變,那些須要重繪
                if(ko.isObservable(param) && param.hasChanged()){
                    changeOpts.push(param);
                    if(relation[viewModel.name] && relation[viewModel.name][key]){
                        reflows.push({
                            val: tmp,
                            methodName: relation[viewModel.name][key]
                        });
                    }
                }
            });

            if(first){ //若是是初始化執行,後面的業務不用重複執行了
                first = false;
                return;
            }
            if(changeOpts.length>0){
                if(changeOpts.length == reflows.length){//說明配置的改變,可能經過方法操做完成
                    Object.getOwnPropertyNames(reflows).forEach(key=>{
                        let item = reflows[key];
                        viewModel.$dom[viewModel.name](item.methodName, item.val);
                    });
                }else{
                    //引發了組件重繪
                    viewModel.repaint(opts);
                }
            }
        });

        return viewModel;
    }

1. 進入此方法,首先咱們進行組件的繪製(也就是建立)

2. 而後經過ko.computed方法監聽params中的options(配置參數)的改變,而後進行組件重繪或者是部分改變(這裏我叫他迴流reflow)。

3. 因爲ko.computed在初始化的時候會執行,因此經過first變量進行問題的迴避。

3、還須要完善的點

1. 如今動態生成的koeasyui組件提供的方法只是easyui組件自己的,而沒有對其繼承的方法進行合併

2. repaint和reflow須要更細緻的區分,讓組件性能達到最優。

相關文章
相關標籤/搜索