???? 在做后臺管理系統的同學們,是否有用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需要更細致的區分,讓組件性能達到最優。