• knockout + easyui = koeasyui


         在做后台管理系统的同学们,是否有用easyui的经历。虽然现在都是vue、ng、react的时代。但easyui(也就是jquery为基础)还是占有一席之地的。因为他对后端开发者太友好了,太熟悉不过了。要让一个后端开发者来理解vue或者是react的VNode、状态器、组件等,都是有那么一点点的为难(反正我转型时,对这些都是很有困惑的)。今天我想试着解决这样一个问题,如:将knockout 与 大家熟悉的easyui结合在一起。让easyui具有MVVM的能力,也有不使用easyui的特性,看大家是否喜欢这一口。

    一、项目介绍说明

    项目语言:typescript

    项目地址:https://gitee.com/ko-plugins/koeasyui

    初级效果:

    望大家给予评论和支持。

    二、如何将easyui转换为ko的组件

         再前几年用ko的时候,由于他没有组件的支持(因为当时没有组件的概念)。至到react、vue提出和引用了组件的概念,以及将此概念深入到每个前端开发者的内心后。ko也提供了组件的支持。2017年看这个新特性的时候,就让我有改造easyui的冲突。当时苦于对ko和easyui的理解不深入,硬是没有找到突破口。今天终于让我找到。

    2.1 easyui组件如何注册到为ko组件

         ko提供了components.register方法,用于注册一个组件。此方法接受一个字符串的名称,以及一个对象(至少包含一个template或viewModel属性),其中viewModel可以是一个对象,也就是一个function。本人就利用了可以为function这一点。根据easyui的组件名动态创建一个function,然后赋值给viewModel,代码片段如下:

    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组件的创建,以及参数的响应和方法的绑定,算是本插件的核心。

    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提供了如下四个勾子:

    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变量进行问题的回避。

    三、还需要完善的点

    1. 现在动态生成的koeasyui组件提供的方法只是easyui组件本身的,而没有对其继承的方法进行合并

    2. repaint和reflow需要更细致的区分,让组件性能达到最优。

  • 相关阅读:
    C#中的const和readonly之间的不同(转)
    文字在状态栏上从右往左显示,而且是循环的
    文字在状态栏上从左往右一个一个地显示
    猛然发现,已经第100篇随笔了
    怎样使按钮响应回车键
    编程之我见(二 类库)初露牛角
    编程之我见(一 语言)小试牛刀
    开始→运行→输入的命令集锦(转)收藏
    显示走动的数字时间和显示星期,年,月,日
    在两个页面之间互相写其控件内的值
  • 原文地址:https://www.cnblogs.com/cqhaibin/p/9064803.html
Copyright © 2020-2023  润新知