【UI插件】简单的日历插件(下)—— 学习MVC思想

2020-11-25 04:54

阅读:927

标签:des   style   blog   class   code   java   

前言

我们上次写了一个简单的日历插件,但是只是一个半成品,而且做完后发现一些问题,于是我们今天尝试来解决这些问题

PS:距离上次貌似很久了

上次,我们大概遇到哪些问题呢:

① 既然想做一套UI库,那么就应该考虑其它UI库的接入问题

这个意思就是,我们的系统中所有UI插件应该有一些统一行为,我们如果希望统一为所有的插件加一点什么东西,需要有位置可加

这个意味着,可能我们所有的插件需要继承至一个抽象的UI类,并且该类提供了通用的几个事件点

② 上次做的日历插件虽然说是简单,其耦合还是比较严重的(其实也说不上,但是人总有想装B的时候)

这个怎么说呢,就日历而言,我们可以将之分成三个部分

1 日历核心部分,用于生产静态html

2 日历数据部分,用于显示各个特殊信息,比如节日什么的

3 日历事件部分,现在的想法便是可以将事件相关给抽象出来

目的便是html/data/events 分开一点点,这个该怎么做呢?这是我们今天该思考的问题

事情多了就什么都不能解决,所以我们今天暂时便处理以上两个问题即可

MVC的学习

由于我们会依赖于underscore,所以,我们这里有一个underscore的扩展,加一些我们自己需要的东西

soscw.com,搜素材soscw.com,搜素材
 1 (function () {
 2 
 3   // @description 全局可能用到的变量
 4   var arr = [];
 5   var slice = arr.slice;
 6 
 7   var method = method || {};
 8 
 9   /**
10   * @description inherit方法,js的继承,默认为两个参数
11   * @param {function} supClass 可选,要继承的类
12   * @param {object} subProperty 被创建类的成员
13   * @return {function} 被创建的类
14   */
15   method.inherit = function () {
16 
17     // @description 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
18     if (arguments.length === 0 || arguments.length > 2) throw ‘参数错误‘;
19 
20     var parent = null;
21 
22     // @description 将参数转换为数组
23     var properties = slice.call(arguments);
24 
25     // @description 如果第一个参数为类(function),那么就将之取出
26     if (typeof properties[0] === ‘function‘)
27       parent = properties.shift();
28     properties = properties[0];
29 
30     // @description 创建新类用于返回
31     function klass() {
32       if (_.isFunction(this.initialize))
33         this.initialize.apply(this, arguments);
34     }
35 
36     klass.superclass = parent;
37     // parent.subclasses = [];
38 
39     if (parent) {
40       // @description 中间过渡类,防止parent的构造函数被执行
41       var subclass = function () { };
42       subclass.prototype = parent.prototype;
43       klass.prototype = new subclass();
44       // parent.subclasses.push(klass);
45     }
46 
47     var ancestor = klass.superclass && klass.superclass.prototype;
48     for (var k in properties) {
49       var value = properties[k];
50 
51       //满足条件就重写
52       if (ancestor && typeof value == ‘function‘) {
53         var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, ‘‘).split(‘,‘);
54         //只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
55         if (argslist[0] === ‘$super‘ && ancestor[k]) {
56           value = (function (methodName, fn) {
57             return function () {
58               var scope = this;
59               var args = [function () {
60                 return ancestor[methodName].apply(scope, arguments);
61               } ];
62               return fn.apply(this, args.concat(slice.call(arguments)));
63             };
64           })(k, value);
65         }
66       }
67 
68       //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
69       if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != ‘function‘ && typeof value != ‘fuction‘)) {
70         //原型链是共享的,这里不好办
71         var temp = {};
72         _.extend(temp, klass.prototype[k]);
73         _.extend(temp, value);
74         klass.prototype[k] = temp;
75       } else {
76         klass.prototype[k] = value;
77       }
78 
79     }
80 
81     if (!klass.prototype.initialize)
82       klass.prototype.initialize = function () { };
83 
84     klass.prototype.constructor = klass;
85 
86     return klass;
87   };
88 
89   _.extend(_, method);
90 
91 })(window);
View Code

对的,以上是我们前面实现的继承,我们将之扩展至underscore上,以后以此实现继承

其次,我们便需要思考如何分离我们的数据/模板/事件了

View/Adapter/ViewController

俗话说,大树底下好乘凉,事实上我一些想法来自于我的老大,我老大又借鉴了原来的ios开发,所以这里形成了一些东西,不知道是否合理,我们拿出来看看

View

首先,无论如何我们的应用都会有一个view的存在,我们认为view只做简单的页面渲染就好,与之有关的数据/事件什么的,我们不予关注

soscw.com,搜素材soscw.com,搜素材
 1 // @description 正式的声明Dalmatian框架的命名空间
 2 var Dalmatian = Dalmatian || {};
 3 
 4 // @description 定义默认的template方法来自于underscore
 5 Dalmatian.template = _.template;
 6 Dalmatian.View = _.inherit({
 7   // @description 构造函数入口
 8   initialize: function(options) {
 9     this._initialize();
10     this.handleOptions(options);
11 
12   },
13 
14   // @description 设置默认属性
15   _initialize: function() {
16 
17     var DEFAULT_CONTAINER_TEMPLATE = ‘
; 18 19 // @description view状态机 20 // this.statusSet = {}; 21 22 this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE; 23 24 // @override 25 // @description template集合,根据status做template的map 26 // @example 27 // { 0: ‘
‘ }
28 // this.templateSet = {}; 29 30 this.viewid = _.uniqueId(‘dalmatian-view-‘); 31 32 }, 33 34 // @description 操作构造函数传入操作 35 handleOptions: function(options) { 36 // @description 从形参中获取key和value绑定在this上 37 if (_.isObject(options)) _.extend(this, options); 38 39 }, 40 41 // @description 通过模板和数据渲染具体的View 42 // @param status {enum} View的状态参数 43 // @param data {object} 匹配View的数据格式的具体数据 44 // @param callback {functiion} 执行完成之后的回调 45 render: function(status, data, callback) { 46 47 var templateSelected = this.templateSet[status]; 48 if (templateSelected) { 49 50 try { 51 // @description 渲染view 52 var templateFn = Dalmatian.template(templateSelected); 53 this.html = templateFn(data); 54 55 // @description 在view外层加入外壳 56 templateFn = Dalmatian.template(this.defaultContainerTemplate); 57 this.html = templateFn({ 58 viewid: this.viewid, 59 html: this.html 60 }); 61 62 this.currentStatus = status; 63 64 _.callmethod(callback, this); 65 66 return true; 67 68 } catch (e) { 69 70 throw e; 71 72 } finally { 73 74 return false; 75 } 76 } 77 }, 78 79 // @override 80 // @description 可以被复写,当status和data分别发生变化时候 81 // @param status {enum} view的状态值 82 // @param data {object} viewmodel的数据 83 update: function(status, data) { 84 85 if (!this.currentStatus || this.currentStatus !== status) { 86 return this.render(status, data); 87 } 88 89 // @override 90 // @description 可复写部分,当数据发生变化但是状态没有发生变化时,页面仅仅变化的可以是局部显示 91 // 可以通过获取this.html进行修改 92 _.callmethod(this.onUpdate, this); 93 } 94 });
View Code

从代码上看,我们需要注意几个事情:

① View会生成静态HTML

② View会根据当前状态、当前数据生成静态HTML

所以,我们的View事实上只会生成静态HTML,不同的是他会根据不同的状态生成不同的HTML,比如初始状态和加载结束状态

有了View便缺不了数据,也就是所谓的Model,我们这里给他取一个名字,Adapter

Adapter

soscw.com,搜素材soscw.com,搜素材
 1 Dalmatian.Adapter = _.inherit({
 2 
 3   // @description 构造函数入口
 4   initialize: function(options) {
 5     this._initialize();
 6     this.handleOptions(options);
 7 
 8   },
 9 
10   // @description 设置默认属性
11   _initialize: function() {
12     this.observers = [];
13     this.viewmodel = {};
14     this.datamodel = {};
15   },
16 
17   // @description 操作构造函数传入操作
18   handleOptions: function(options) {
19     // @description 从形参中获取key和value绑定在this上
20     if (_.isObject(options)) _.extend(this, options);
21   },
22 
23   // @description 设置
24   format: function(origindata){
25     this.datamodel = origindata;
26     this.viewmodel = this.parse(origindata);
27     return this.viewmodel;
28   },
29 
30   // @override
31   // @description parse方法用来将datamodel转化为viewmodel,必须被重写
32   parse: function(origindata) {
33     throw Error(‘方法必须被重写‘);
34   },
35 
36   registerObserver: function(viewcontroller) {
37     // @description 检查队列中如果没有viewcontroller,从队列尾部推入
38     if (!_.contains(this.observers, viewcontroller)) {
39       this.observers.push(viewcontroller);
40     }
41   },
42 
43   unregisterObserver: function(viewcontroller) {
44     // @description 从observers的队列中剔除viewcontroller
45     this.observers = _.without(this.observers, viewcontroller);
46   },
47 
48   notifyDataChanged: function() {
49     // @description 通知所有注册的观察者被观察者的数据发生变化
50     var data = this.format(this.datamodel);
51     _.each(this.observers, function(viewcontroller) {
52       if (_.isObject(viewcontroller))
53         _.callmethod(viewcontroller.update, viewcontroller, [data]);
54     });
55   }
56 });
View Code

Adapter由以下几个关键组成:

① View观察者

② 数据模型,便是原始的数据

③ viewModel,便是view实际需要的数据

并且每一次数据的改变会通知观察的view,触发其update,所以Adapter的组成也比较干脆,并不复杂

但是,我们的view仍然没有事件,而且Adapter也没有与view联系起来,这个时候我们缺少一个要件,他的名字是Controller

ViewController

控制器是链接模型与视图的桥梁,我们这里也不例外,核心动作皆会在控制器处完成

soscw.com,搜素材soscw.com,搜素材
  1 Dalmatian.ViewController = _.inherit({
  2 
  3   // @description 构造函数入口
  4   initialize: function (options) {
  5     this.handleOptions(options);
  6     this.create();
  7   },
  8 
  9   // @description 操作构造函数传入操作
 10   handleOptions: function (options) {
 11     this._verify(options);
 12 
 13     // @description 从形参中获取key和value绑定在this上
 14     if (_.isObject(options)) _.extend(this, options);
 15   },
 16 
 17   // @description 验证参数
 18   _verify: function (options) {
 19     if (!_.property(‘view‘)(options)) throw Error(‘view必须在实例化的时候传入ViewController‘);
 20   },
 21 
 22   // @description 当数据发生变化时调用onViewUpdate,如果onViewUpdate方法不存在的话,直接调用render方法重绘
 23   update: function (data) {
 24 
 25     _.callmethod(this.hide, this);
 26 
 27     if (!_.callmethod(this.onViewUpdate, this, [data])) {
 28       this.render();
 29     }
 30 
 31     _.callmethod(this.show, this);
 32   },
 33 
 34   /**
 35   * @description 传入事件对象,解析之,解析event,返回对象{events: [{target: ‘#btn‘, event:‘click‘, callback: handler}]}
 36   * @param events {obj} 事件对象,默认传入唯一id
 37   * @param namespace 事件命名空间
 38   * @return {obj}
 39   */
 40   parseEvents: function (events) {
 41 
 42     //用于返回的事件对象
 43     var eventArr = [];
 44     //注意,此处做简单的字符串数据解析即可,不做实际业务
 45     for (var key in events) {
 46       var method = events[key];
 47       if (!_.isFunction(method)) method = this[events[key]];
 48       if (!method) continue;
 49 
 50       var match = key.match(delegateEventSplitter);
 51       var eventName = match[1],
 52         selector = match[2];
 53       method = _.bind(method, this);
 54       eventName += ‘.delegateEvents‘ + this.view.viewid;
 55       eventArr.push({
 56         target: selector,
 57         event: eventName,
 58         callback: method
 59       });
 60     }
 61 
 62     return eventArr;
 63   },
 64 
 65   /**
 66    * @override
 67    *
 68    */
 69   render: function() {
 70     // @notation  这个方法需要被复写
 71     // var data = this.adapter.format(this.origindata);
 72     // this.view.render(this.viewstatus, data);
 73   },
 74 
 75   _create: function () {
 76     this.render();
 77   },
 78 
 79   create: function () {
 80 
 81     var $element = selectDom(this.view.viewid);
 82     if (domImplement($element, ‘get‘, false, [0])) {
 83       return _.callmethod(this.recreate, this);
 84     }
 85 
 86     // @notation 在create方法调用前后设置onViewBeforeCreate和onViewAfterCreate两个回调
 87     _.wrapmethod(this._create, ‘onViewBeforeCreate‘, ‘onViewAfterCreate‘, this);
 88 
 89   },
 90 
 91   /**
 92   * @description 如果进入create判断是否需要update一下页面,sync view和viewcontroller的数据
 93   */
 94   _recreate: function () {
 95     this.update();
 96   },
 97 
 98   recreate: function () {
 99     _.wrapmethod(this._recreate, ‘onViewBeforeRecreate‘, ‘onViewAfterRecreate‘, this);
100   },
101 
102   _bind: function () {
103     this.viewcontent = createDom(this.view.html);
104 
105     var eventsList = this.parseEvents(this.events);
106 
107     var scope = this;
108     _.each(eventsList, function (item) {
109 
110       if (item.target === ‘‘) {
111         eventmethod(scope.viewcontent, ‘on‘, item.event, item.callback, scope);
112       } else {
113         eventmethod(scope.viewcontent, ‘on‘, item.event, item.callback, scope, item.target);
114       }
115 
116     });
117   },
118 
119   bind: function () {
120     _.wrapmethod(this._bind, ‘onViewBeforeBind‘, ‘onViewAfterBind‘, this);
121   },
122 
123   _show: function () {
124     var $element = selectDom(‘#‘ + this.view.viewid);
125 
126     // @notation 需要剔除码?
127     // if ((!$element || $element.length === 0) && this.viewcontent) {
128       var $container = selectDom(this.container);
129       domImplement($container, ‘html‘, false, [this.viewcontent]);
130     // }
131 
132     domImplement($element, ‘show‘);
133   },
134 
135   show: function () {
136     this.bind();
137 
138     _.wrapmethod(this._show, ‘onViewBeforeShow‘, ‘onViewAfterShow‘, this);
139   },
140 
141   _hide: function () {
142     var $element = selectDom(‘#‘ + this.view.viewid);
143     domImplement($element, ‘hide‘);
144   },
145 
146   hide: function () {
147     _.wrapmethod(this._hide, ‘onViewBeforeHide‘, ‘onViewAfterHide‘, this);
148 
149     this.forze();
150   },
151 
152   _forze: function () {
153     var $element = selectDom(‘#‘ + this.view.viewid);
154     domImplement($element, ‘off‘);
155   },
156 
157   forze: function () {
158     _.wrapmethod(this._forze, ‘onViewBeforeForzen‘, ‘onViewAfterForzen‘, this);
159   },
160 
161   _destory: function () {
162     var $element = selectDom(‘#‘ + this.view.viewid).remove();
163     domImplement($element, ‘remove‘);
164   },
165 
166   destory: function () {
167     _.wrapmethod(this._destory, ‘onViewBeforeDestory‘, ‘onViewAfterDestory‘, this);
168   }
169 });
View Code

control这里便有所不同,会稍微复杂一点点

① 首先,他会验证自己是否含有view参数,我们这里要求一个控制器必须对应一个view,如果没有指定的话便认为错误

if (!_.property(‘view‘)(options)) throw Error(‘view必须在实例化的时候传入ViewController‘);

② 然后主要有几个关键事件点,第一个是create

PS:这里会区分是否二次创建该View,这个判断事实上不应该通过dom是否存在来判断,这里后期优化

create调用便会调用view的render方法,然后便会构建相关的dom结构,并且append到container中

③ 第二个关键事件点为show,调用时,dom会真正的显示,并且绑定事件

PS:事件这块借鉴的Backbone的机制,全部绑定值根元素,具体优化后面再说吧

④ 除此之外还有hide、forze(解除事件句柄,释放资源)、destroy等不详说了

soscw.com,搜素材

说了这么多都是扯淡,我们下面以两个简单的例子做一次说明

实例说明

MVC学习完整代码

有不对的地方请提出

soscw.com,搜素材soscw.com,搜素材
  1 "use strict";
  2 
  3 // @notation 本框架默认是以来于zepto。这里构建了基础的方法层,当用户使用其他框架时,可能需要复写这几个基础方法
  4 
  5 // @description 解析event参数的正则
  6 var delegateEventSplitter = /^(\S+)\s*(.*)$/;
  7 // Regular expression used to split event strings.
  8 var eventSplitter = /\s+/;
  9 
 10 // ----------------------------------------------------
 11 // @notation 从backbone中借鉴而来,用来多事件绑定的events
 12 
 13 // Implement fancy features of the Events API such as multiple event
 14 // names `"change blur"` and jQuery-style event maps `{change: action}`
 15 // in terms of the existing API.
 16 var eventoperator = function(obj, action, name, rest) {
 17   if (!name) return true;
 18 
 19   // Handle event maps.
 20   if (typeof name === ‘object‘) {
 21     for (var key in name) {
 22       obj[action].apply(obj, [key, name[key]].concat(rest));
 23     }
 24     return false;
 25   }
 26 
 27   // Handle space separated event names.
 28   if (eventSplitter.test(name)) {
 29     var names = name.split(eventSplitter);
 30     for (var i = 0, length = names.length; i ) {
 31       obj[action].apply(obj, [names[i]].concat(rest));
 32     }
 33     return false;
 34   }
 35 
 36   return true;
 37 };
 38 // ----------------------------------------------------
 39 
 40 // @notation 默认使用zepto的事件委托机制
 41 function eventmethod(obj, action, name, callback, context, subobj) {
 42   // _.bind(callback, context || this);
 43 
 44   var delegate = function(target, eventName, eventCallback, subtarget) {
 45     if (subtarget) {
 46       target.on(eventName, subtarget, eventCallback);
 47     }else{
 48       target.on(eventName, eventCallback);
 49     }
 50   };
 51 
 52   var undelegate = function(target, eventName, eventCallback, subtarget) {
 53     if (subtarget) {
 54       target.off(eventName, subtarget, eventCallback);
 55     }else{
 56       target.off(eventName, eventCallback);
 57     }
 58   };
 59 
 60   var trigger = function(target, eventName, subtarget) {
 61     if (subtarget) {
 62       target.find(subtarget).trigger(eventName);
 63     }else{
 64       target.trigger(eventName);
 65     }
 66   };
 67 
 68   var map = {
 69     ‘on‘: delegate,
 70     ‘bind‘: delegate,
 71     ‘off‘: undelegate,
 72     ‘unbind‘: undelegate,
 73     ‘trigger‘: trigger
 74   };
 75 
 76   if (_.isFunction(map[action])) {
 77     map[action](obj, name, callback, subobj);
 78   }
 79 
 80 }
 81 
 82 // @description 选择器
 83 function selectDom(selector) {
 84   return $(selector);
 85 }
 86 
 87 function domImplement($element, action, context, param) {
 88   if (_.isFunction($element[action]))
 89     $element[action].apply(context || $element, param);
 90 }
 91 
 92 function createDom (html) {
 93   return $(html);
 94 }
 95 
 96 // --------------------------------------------------- //
 97 // ------------------华丽的分割线--------------------- //
 98 
 99 // @description 正式的声明Dalmatian框架的命名空间
100 var Dalmatian = Dalmatian || {};
101 
102 // @description 定义默认的template方法来自于underscore
103 Dalmatian.template = _.template;
104 Dalmatian.View = _.inherit({
105   // @description 构造函数入口
106   initialize: function(options) {
107     this._initialize();
108     this.handleOptions(options);
109 
110   },
111 
112   // @description 设置默认属性
113   _initialize: function() {
114 
115     var DEFAULT_CONTAINER_TEMPLATE = ‘
; 116 117 // @description view状态机 118 // this.statusSet = {}; 119 120 this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE; 121 122 // @override 123 // @description template集合,根据status做template的map 124 // @example 125 // { 0: ‘
‘ }
126 // this.templateSet = {}; 127 128 this.viewid = _.uniqueId(‘dalmatian-view-‘); 129 130 }, 131 132 // @description 操作构造函数传入操作 133 handleOptions: function(options) { 134 // @description 从形参中获取key和value绑定在this上 135 if (_.isObject(options)) _.extend(this, options); 136 137 }, 138 139 // @description 通过模板和数据渲染具体的View 140 // @param status {enum} View的状态参数 141 // @param data {object} 匹配View的数据格式的具体数据 142 // @param callback {functiion} 执行完成之后的回调 143 render: function(status, data, callback) { 144 145 var templateSelected = this.templateSet[status]; 146 if (templateSelected) { 147 148 try { 149 // @description 渲染view 150 var templateFn = Dalmatian.template(templateSelected); 151 this.html = templateFn(data); 152 153 // @description 在view外层加入外壳 154 templateFn = Dalmatian.template(this.defaultContainerTemplate); 155 this.html = templateFn({ 156 viewid: this.viewid, 157 html: this.html 158 }); 159 160 this.currentStatus = status; 161 162 _.callmethod(callback, this); 163 164 return true; 165 166 } catch (e) { 167 168 throw e; 169 170 } finally { 171 172 return false; 173 } 174 } 175 }, 176 177 // @override 178 // @description 可以被复写,当status和data分别发生变化时候 179 // @param status {enum} view的状态值 180 // @param data {object} viewmodel的数据 181 update: function(status, data) { 182 183 if (!this.currentStatus || this.currentStatus !== status) { 184 return this.render(status, data); 185 } 186 187 // @override 188 // @description 可复写部分,当数据发生变化但是状态没有发生变化时,页面仅仅变化的可以是局部显示 189 // 可以通过获取this.html进行修改 190 _.callmethod(this.onUpdate, this); 191 } 192 }); 193 194 Dalmatian.Adapter = _.inherit({ 195 196 // @description 构造函数入口 197 initialize: function(options) { 198 this._initialize(); 199 this.handleOptions(options); 200 201 }, 202 203 // @description 设置默认属性 204 _initialize: function() { 205 this.observers = []; 206 this.viewmodel = {}; 207 this.datamodel = {}; 208 },


评论


亲,登录后才可以留言!