【UI插件】简单的日历插件(下)—— 学习MVC思想
2020-11-25 04:54
标签:des style blog class code java 我们上次写了一个简单的日历插件,但是只是一个半成品,而且做完后发现一些问题,于是我们今天尝试来解决这些问题 PS:距离上次貌似很久了 上次,我们大概遇到哪些问题呢: ① 既然想做一套UI库,那么就应该考虑其它UI库的接入问题 这个意思就是,我们的系统中所有UI插件应该有一些统一行为,我们如果希望统一为所有的插件加一点什么东西,需要有位置可加 这个意味着,可能我们所有的插件需要继承至一个抽象的UI类,并且该类提供了通用的几个事件点 ② 上次做的日历插件虽然说是简单,其耦合还是比较严重的(其实也说不上,但是人总有想装B的时候) 这个怎么说呢,就日历而言,我们可以将之分成三个部分 1 日历核心部分,用于生产静态html 2 日历数据部分,用于显示各个特殊信息,比如节日什么的 3 日历事件部分,现在的想法便是可以将事件相关给抽象出来 目的便是html/data/events 分开一点点,这个该怎么做呢?这是我们今天该思考的问题 事情多了就什么都不能解决,所以我们今天暂时便处理以上两个问题即可 由于我们会依赖于underscore,所以,我们这里有一个underscore的扩展,加一些我们自己需要的东西 对的,以上是我们前面实现的继承,我们将之扩展至underscore上,以后以此实现继承 其次,我们便需要思考如何分离我们的数据/模板/事件了 俗话说,大树底下好乘凉,事实上我一些想法来自于我的老大,我老大又借鉴了原来的ios开发,所以这里形成了一些东西,不知道是否合理,我们拿出来看看 首先,无论如何我们的应用都会有一个view的存在,我们认为view只做简单的页面渲染就好,与之有关的数据/事件什么的,我们不予关注 从代码上看,我们需要注意几个事情: ① View会生成静态HTML ② View会根据当前状态、当前数据生成静态HTML 所以,我们的View事实上只会生成静态HTML,不同的是他会根据不同的状态生成不同的HTML,比如初始状态和加载结束状态 有了View便缺不了数据,也就是所谓的Model,我们这里给他取一个名字,Adapter Adapter由以下几个关键组成: ① View观察者 ② 数据模型,便是原始的数据 ③ viewModel,便是view实际需要的数据 并且每一次数据的改变会通知观察的view,触发其update,所以Adapter的组成也比较干脆,并不复杂 但是,我们的view仍然没有事件,而且Adapter也没有与view联系起来,这个时候我们缺少一个要件,他的名字是Controller 控制器是链接模型与视图的桥梁,我们这里也不例外,核心动作皆会在控制器处完成 control这里便有所不同,会稍微复杂一点点 ① 首先,他会验证自己是否含有view参数,我们这里要求一个控制器必须对应一个view,如果没有指定的话便认为错误 ② 然后主要有几个关键事件点,第一个是create PS:这里会区分是否二次创建该View,这个判断事实上不应该通过dom是否存在来判断,这里后期优化 create调用便会调用view的render方法,然后便会构建相关的dom结构,并且append到container中 ③ 第二个关键事件点为show,调用时,dom会真正的显示,并且绑定事件 PS:事件这块借鉴的Backbone的机制,全部绑定值根元素,具体优化后面再说吧 ④ 除此之外还有hide、forze(解除事件句柄,释放资源)、destroy等不详说了 说了这么多都是扯淡,我们下面以两个简单的例子做一次说明 有不对的地方请提出前言
MVC的学习
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/Adapter/ViewController
View
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 });
Adapter
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 });
ViewController
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 });
if (!_.property(‘view‘)(options)) throw Error(‘view必须在实例化的时候传入ViewController‘);
实例说明
MVC学习完整代码
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 },