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

时间:2021-12-12 03:34:19

前言

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

PS:距离上次貌似很久了

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

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

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

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

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

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

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

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

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

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

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

MVC的学习

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

 (function () {

   // @description 全局可能用到的变量
var arr = [];
var slice = arr.slice; var method = method || {}; /**
* @description inherit方法,js的继承,默认为两个参数
* @param {function} supClass 可选,要继承的类
* @param {object} subProperty 被创建类的成员
* @return {function} 被创建的类
*/
method.inherit = function () { // @description 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
if (arguments.length === 0 || arguments.length > 2) throw '参数错误'; var parent = null; // @description 将参数转换为数组
var properties = slice.call(arguments); // @description 如果第一个参数为类(function),那么就将之取出
if (typeof properties[0] === 'function')
parent = properties.shift();
properties = properties[0]; // @description 创建新类用于返回
function klass() {
if (_.isFunction(this.initialize))
this.initialize.apply(this, arguments);
} klass.superclass = parent;
// parent.subclasses = []; if (parent) {
// @description 中间过渡类,防止parent的构造函数被执行
var subclass = function () { };
subclass.prototype = parent.prototype;
klass.prototype = new subclass();
// parent.subclasses.push(klass);
} var ancestor = klass.superclass && klass.superclass.prototype;
for (var k in properties) {
var value = properties[k]; //满足条件就重写
if (ancestor && typeof value == 'function') {
var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
//只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
if (argslist[0] === '$super' && ancestor[k]) {
value = (function (methodName, fn) {
return function () {
var scope = this;
var args = [function () {
return ancestor[methodName].apply(scope, arguments);
} ];
return fn.apply(this, args.concat(slice.call(arguments)));
};
})(k, value);
}
} //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
//原型链是共享的,这里不好办
var temp = {};
_.extend(temp, klass.prototype[k]);
_.extend(temp, value);
klass.prototype[k] = temp;
} else {
klass.prototype[k] = value;
} } if (!klass.prototype.initialize)
klass.prototype.initialize = function () { }; klass.prototype.constructor = klass; return klass;
}; _.extend(_, method); })(window);

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

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

View/Adapter/ViewController

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

View

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

 // @description 正式的声明Dalmatian框架的命名空间
var Dalmatian = Dalmatian || {}; // @description 定义默认的template方法来自于underscore
Dalmatian.template = _.template;
Dalmatian.View = _.inherit({
// @description 构造函数入口
initialize: function(options) {
this._initialize();
this.handleOptions(options); }, // @description 设置默认属性
_initialize: function() { var DEFAULT_CONTAINER_TEMPLATE = '<section class="view" id="<%=viewid%>"><%=html%></section>'; // @description view状态机
// this.statusSet = {}; this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE; // @override
// @description template集合,根据status做template的map
// @example
// { 0: '<ul><%_.each(list, function(item){%><li><%=item.name%></li><%});%></ul>' }
// this.templateSet = {}; this.viewid = _.uniqueId('dalmatian-view-'); }, // @description 操作构造函数传入操作
handleOptions: function(options) {
// @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options); }, // @description 通过模板和数据渲染具体的View
// @param status {enum} View的状态参数
// @param data {object} 匹配View的数据格式的具体数据
// @param callback {functiion} 执行完成之后的回调
render: function(status, data, callback) { var templateSelected = this.templateSet[status];
if (templateSelected) { try {
// @description 渲染view
var templateFn = Dalmatian.template(templateSelected);
this.html = templateFn(data); // @description 在view外层加入外壳
templateFn = Dalmatian.template(this.defaultContainerTemplate);
this.html = templateFn({
viewid: this.viewid,
html: this.html
}); this.currentStatus = status; _.callmethod(callback, this); return true; } catch (e) { throw e; } finally { return false;
}
}
}, // @override
// @description 可以被复写,当status和data分别发生变化时候
// @param status {enum} view的状态值
// @param data {object} viewmodel的数据
update: function(status, data) { if (!this.currentStatus || this.currentStatus !== status) {
return this.render(status, data);
} // @override
// @description 可复写部分,当数据发生变化但是状态没有发生变化时,页面仅仅变化的可以是局部显示
// 可以通过获取this.html进行修改
_.callmethod(this.onUpdate, this);
}
});

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

① View会生成静态HTML

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

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

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

Adapter

 Dalmatian.Adapter = _.inherit({

   // @description 构造函数入口
initialize: function(options) {
this._initialize();
this.handleOptions(options); }, // @description 设置默认属性
_initialize: function() {
this.observers = [];
this.viewmodel = {};
this.datamodel = {};
}, // @description 操作构造函数传入操作
handleOptions: function(options) {
// @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options);
}, // @description 设置
format: function(origindata){
this.datamodel = origindata;
this.viewmodel = this.parse(origindata);
return this.viewmodel;
}, // @override
// @description parse方法用来将datamodel转化为viewmodel,必须被重写
parse: function(origindata) {
throw Error('方法必须被重写');
}, registerObserver: function(viewcontroller) {
// @description 检查队列中如果没有viewcontroller,从队列尾部推入
if (!_.contains(this.observers, viewcontroller)) {
this.observers.push(viewcontroller);
}
}, unregisterObserver: function(viewcontroller) {
// @description 从observers的队列中剔除viewcontroller
this.observers = _.without(this.observers, viewcontroller);
}, notifyDataChanged: function() {
// @description 通知所有注册的观察者被观察者的数据发生变化
var data = this.format(this.datamodel);
_.each(this.observers, function(viewcontroller) {
if (_.isObject(viewcontroller))
_.callmethod(viewcontroller.update, viewcontroller, [data]);
});
}
});

Adapter由以下几个关键组成:

① View观察者

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

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

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

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

ViewController

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

 Dalmatian.ViewController = _.inherit({

   // @description 构造函数入口
initialize: function (options) {
this.handleOptions(options);
this.create();
}, // @description 操作构造函数传入操作
handleOptions: function (options) {
this._verify(options); // @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options);
}, // @description 验证参数
_verify: function (options) {
if (!_.property('view')(options)) throw Error('view必须在实例化的时候传入ViewController');
}, // @description 当数据发生变化时调用onViewUpdate,如果onViewUpdate方法不存在的话,直接调用render方法重绘
update: function (data) { _.callmethod(this.hide, this); if (!_.callmethod(this.onViewUpdate, this, [data])) {
this.render();
} _.callmethod(this.show, this);
}, /**
* @description 传入事件对象,解析之,解析event,返回对象{events: [{target: '#btn', event:'click', callback: handler}]}
* @param events {obj} 事件对象,默认传入唯一id
* @param namespace 事件命名空间
* @return {obj}
*/
parseEvents: function (events) { //用于返回的事件对象
var eventArr = [];
//注意,此处做简单的字符串数据解析即可,不做实际业务
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue; var match = key.match(delegateEventSplitter);
var eventName = match[1],
selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.view.viewid;
eventArr.push({
target: selector,
event: eventName,
callback: method
});
} return eventArr;
}, /**
* @override
*
*/
render: function() {
// @notation 这个方法需要被复写
// var data = this.adapter.format(this.origindata);
// this.view.render(this.viewstatus, data);
}, _create: function () {
this.render();
}, create: function () { var $element = selectDom(this.view.viewid);
if (domImplement($element, 'get', false, [0])) {
return _.callmethod(this.recreate, this);
} // @notation 在create方法调用前后设置onViewBeforeCreate和onViewAfterCreate两个回调
_.wrapmethod(this._create, 'onViewBeforeCreate', 'onViewAfterCreate', this); }, /**
* @description 如果进入create判断是否需要update一下页面,sync view和viewcontroller的数据
*/
_recreate: function () {
this.update();
}, recreate: function () {
_.wrapmethod(this._recreate, 'onViewBeforeRecreate', 'onViewAfterRecreate', this);
}, _bind: function () {
this.viewcontent = createDom(this.view.html); var eventsList = this.parseEvents(this.events); var scope = this;
_.each(eventsList, function (item) { if (item.target === '') {
eventmethod(scope.viewcontent, 'on', item.event, item.callback, scope);
} else {
eventmethod(scope.viewcontent, 'on', item.event, item.callback, scope, item.target);
} });
}, bind: function () {
_.wrapmethod(this._bind, 'onViewBeforeBind', 'onViewAfterBind', this);
}, _show: function () {
var $element = selectDom('#' + this.view.viewid); // @notation 需要剔除码?
// if ((!$element || $element.length === 0) && this.viewcontent) {
var $container = selectDom(this.container);
domImplement($container, 'html', false, [this.viewcontent]);
// } domImplement($element, 'show');
}, show: function () {
this.bind(); _.wrapmethod(this._show, 'onViewBeforeShow', 'onViewAfterShow', this);
}, _hide: function () {
var $element = selectDom('#' + this.view.viewid);
domImplement($element, 'hide');
}, hide: function () {
_.wrapmethod(this._hide, 'onViewBeforeHide', 'onViewAfterHide', this); this.forze();
}, _forze: function () {
var $element = selectDom('#' + this.view.viewid);
domImplement($element, 'off');
}, forze: function () {
_.wrapmethod(this._forze, 'onViewBeforeForzen', 'onViewAfterForzen', this);
}, _destory: function () {
var $element = selectDom('#' + this.view.viewid).remove();
domImplement($element, 'remove');
}, destory: function () {
_.wrapmethod(this._destory, 'onViewBeforeDestory', 'onViewAfterDestory', this);
}
});

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等不详说了

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

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

实例说明

MVC学习完整代码

有不对的地方请提出

 "use strict";

 // @notation 本框架默认是以来于zepto。这里构建了基础的方法层,当用户使用其他框架时,可能需要复写这几个基础方法

 // @description 解析event参数的正则
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// Regular expression used to split event strings.
var eventSplitter = /\s+/; // ----------------------------------------------------
// @notation 从backbone中借鉴而来,用来多事件绑定的events // Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventoperator = function(obj, action, name, rest) {
if (!name) return true; // Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
} // Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
} return true;
};
// ---------------------------------------------------- // @notation 默认使用zepto的事件委托机制
function eventmethod(obj, action, name, callback, context, subobj) {
// _.bind(callback, context || this); var delegate = function(target, eventName, eventCallback, subtarget) {
if (subtarget) {
target.on(eventName, subtarget, eventCallback);
}else{
target.on(eventName, eventCallback);
}
}; var undelegate = function(target, eventName, eventCallback, subtarget) {
if (subtarget) {
target.off(eventName, subtarget, eventCallback);
}else{
target.off(eventName, eventCallback);
}
}; var trigger = function(target, eventName, subtarget) {
if (subtarget) {
target.find(subtarget).trigger(eventName);
}else{
target.trigger(eventName);
}
}; var map = {
'on': delegate,
'bind': delegate,
'off': undelegate,
'unbind': undelegate,
'trigger': trigger
}; if (_.isFunction(map[action])) {
map[action](obj, name, callback, subobj);
} } // @description 选择器
function selectDom(selector) {
return $(selector);
} function domImplement($element, action, context, param) {
if (_.isFunction($element[action]))
$element[action].apply(context || $element, param);
} function createDom (html) {
return $(html);
} // --------------------------------------------------- //
// ------------------华丽的分割线--------------------- // // @description 正式的声明Dalmatian框架的命名空间
var Dalmatian = Dalmatian || {}; // @description 定义默认的template方法来自于underscore
Dalmatian.template = _.template;
Dalmatian.View = _.inherit({
// @description 构造函数入口
initialize: function(options) {
this._initialize();
this.handleOptions(options); }, // @description 设置默认属性
_initialize: function() { var DEFAULT_CONTAINER_TEMPLATE = '<section class="view" id="<%=viewid%>"><%=html%></section>'; // @description view状态机
// this.statusSet = {}; this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE; // @override
// @description template集合,根据status做template的map
// @example
// { 0: '<ul><%_.each(list, function(item){%><li><%=item.name%></li><%});%></ul>' }
// this.templateSet = {}; this.viewid = _.uniqueId('dalmatian-view-'); }, // @description 操作构造函数传入操作
handleOptions: function(options) {
// @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options); }, // @description 通过模板和数据渲染具体的View
// @param status {enum} View的状态参数
// @param data {object} 匹配View的数据格式的具体数据
// @param callback {functiion} 执行完成之后的回调
render: function(status, data, callback) { var templateSelected = this.templateSet[status];
if (templateSelected) { try {
// @description 渲染view
var templateFn = Dalmatian.template(templateSelected);
this.html = templateFn(data); // @description 在view外层加入外壳
templateFn = Dalmatian.template(this.defaultContainerTemplate);
this.html = templateFn({
viewid: this.viewid,
html: this.html
}); this.currentStatus = status; _.callmethod(callback, this); return true; } catch (e) { throw e; } finally { return false;
}
}
}, // @override
// @description 可以被复写,当status和data分别发生变化时候
// @param status {enum} view的状态值
// @param data {object} viewmodel的数据
update: function(status, data) { if (!this.currentStatus || this.currentStatus !== status) {
return this.render(status, data);
} // @override
// @description 可复写部分,当数据发生变化但是状态没有发生变化时,页面仅仅变化的可以是局部显示
// 可以通过获取this.html进行修改
_.callmethod(this.onUpdate, this);
}
}); Dalmatian.Adapter = _.inherit({ // @description 构造函数入口
initialize: function(options) {
this._initialize();
this.handleOptions(options); }, // @description 设置默认属性
_initialize: function() {
this.observers = [];
this.viewmodel = {};
this.datamodel = {};
}, // @description 操作构造函数传入操作
handleOptions: function(options) {
// @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options);
}, // @description 设置
format: function(origindata){
this.datamodel = origindata;
this.viewmodel = this.parse(origindata);
return this.viewmodel;
}, // @override
// @description parse方法用来将datamodel转化为viewmodel,必须被重写
parse: function(origindata) {
throw Error('方法必须被重写');
}, registerObserver: function(viewcontroller) {
// @description 检查队列中如果没有viewcontroller,从队列尾部推入
if (!_.contains(this.observers, viewcontroller)) {
this.observers.push(viewcontroller);
}
}, unregisterObserver: function(viewcontroller) {
// @description 从observers的队列中剔除viewcontroller
this.observers = _.without(this.observers, viewcontroller);
}, notifyDataChanged: function() {
// @description 通知所有注册的观察者被观察者的数据发生变化
var data = this.format(this.datamodel);
_.each(this.observers, function(viewcontroller) {
if (_.isObject(viewcontroller))
_.callmethod(viewcontroller.update, viewcontroller, [data]);
});
}
}); Dalmatian.ViewController = _.inherit({ // @description 构造函数入口
initialize: function (options) {
this.handleOptions(options);
this.create();
}, // @description 操作构造函数传入操作
handleOptions: function (options) {
this._verify(options); // @description 从形参中获取key和value绑定在this上
if (_.isObject(options)) _.extend(this, options);
}, // @description 验证参数
_verify: function (options) {
if (!_.property('view')(options)) throw Error('view必须在实例化的时候传入ViewController');
}, // @description 当数据发生变化时调用onViewUpdate,如果onViewUpdate方法不存在的话,直接调用render方法重绘
update: function (data) { _.callmethod(this.hide, this); if (!_.callmethod(this.onViewUpdate, this, [data])) {
this.render();
} _.callmethod(this.show, this);
}, /**
* @description 传入事件对象,解析之,解析event,返回对象{events: [{target: '#btn', event:'click', callback: handler}]}
* @param events {obj} 事件对象,默认传入唯一id
* @param namespace 事件命名空间
* @return {obj}
*/
parseEvents: function (events) { //用于返回的事件对象
var eventArr = [];
//注意,此处做简单的字符串数据解析即可,不做实际业务
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue; var match = key.match(delegateEventSplitter);
var eventName = match[1],
selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.view.viewid;
eventArr.push({
target: selector,
event: eventName,
callback: method
});
} return eventArr;
}, /**
* @override
*
*/
render: function() {
// @notation 这个方法需要被复写
// var data = this.adapter.format(this.origindata);
// this.view.render(this.viewstatus, data);
}, _create: function () {
this.render();
}, create: function () { var $element = selectDom(this.view.viewid);
if (domImplement($element, 'get', false, [0])) {
return _.callmethod(this.recreate, this);
} // @notation 在create方法调用前后设置onViewBeforeCreate和onViewAfterCreate两个回调
_.wrapmethod(this._create, 'onViewBeforeCreate', 'onViewAfterCreate', this); }, /**
* @description 如果进入create判断是否需要update一下页面,sync view和viewcontroller的数据
*/
_recreate: function () {
this.update();
}, recreate: function () {
_.wrapmethod(this._recreate, 'onViewBeforeRecreate', 'onViewAfterRecreate', this);
}, _bind: function () {
this.viewcontent = createDom(this.view.html); var eventsList = this.parseEvents(this.events); var scope = this;
_.each(eventsList, function (item) { if (item.target === '') {
eventmethod(scope.viewcontent, 'on', item.event, item.callback, scope);
} else {
eventmethod(scope.viewcontent, 'on', item.event, item.callback, scope, item.target);
} });
}, bind: function () {
_.wrapmethod(this._bind, 'onViewBeforeBind', 'onViewAfterBind', this);
}, _show: function () {
var $element = selectDom('#' + this.view.viewid); // @notation 需要剔除码?
// if ((!$element || $element.length === 0) && this.viewcontent) {
var $container = selectDom(this.container);
domImplement($container, 'html', false, [this.viewcontent]);
// } domImplement($element, 'show');
}, show: function () {
this.bind(); _.wrapmethod(this._show, 'onViewBeforeShow', 'onViewAfterShow', this);
}, _hide: function () {
var $element = selectDom('#' + this.view.viewid);
domImplement($element, 'hide');
}, hide: function () {
_.wrapmethod(this._hide, 'onViewBeforeHide', 'onViewAfterHide', this); this.forze();
}, _forze: function () {
var $element = selectDom('#' + this.view.viewid);
domImplement($element, 'off');
}, forze: function () {
_.wrapmethod(this._forze, 'onViewBeforeForzen', 'onViewAfterForzen', this);
}, _destory: function () {
var $element = selectDom('#' + this.view.viewid).remove();
domImplement($element, 'remove');
}, destory: function () {
_.wrapmethod(this._destory, 'onViewBeforeDestory', 'onViewAfterDestory', this);
}
});

underscore扩展

 (function () {

   // @description 全局可能用到的变量
var arr = [];
var slice = arr.slice; var method = method || {}; /**
* @description inherit方法,js的继承,默认为两个参数
* @param {function} supClass 可选,要继承的类
* @param {object} subProperty 被创建类的成员
* @return {function} 被创建的类
*/
method.inherit = function () { // @description 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
if (arguments.length === 0 || arguments.length > 2) throw '参数错误'; var parent = null; // @description 将参数转换为数组
var properties = slice.call(arguments); // @description 如果第一个参数为类(function),那么就将之取出
if (typeof properties[0] === 'function')
parent = properties.shift();
properties = properties[0]; // @description 创建新类用于返回
function klass() {
if (_.isFunction(this.initialize))
this.initialize.apply(this, arguments);
} klass.superclass = parent;
// parent.subclasses = []; if (parent) {
// @description 中间过渡类,防止parent的构造函数被执行
var subclass = function () { };
subclass.prototype = parent.prototype;
klass.prototype = new subclass();
// parent.subclasses.push(klass);
} var ancestor = klass.superclass && klass.superclass.prototype;
for (var k in properties) {
var value = properties[k]; //满足条件就重写
if (ancestor && typeof value == 'function') {
var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
//只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
if (argslist[0] === '$super' && ancestor[k]) {
value = (function (methodName, fn) {
return function () {
var scope = this;
var args = [function () {
return ancestor[methodName].apply(scope, arguments);
} ];
return fn.apply(this, args.concat(slice.call(arguments)));
};
})(k, value);
}
} //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
//原型链是共享的,这里不好办
var temp = {};
_.extend(temp, klass.prototype[k]);
_.extend(temp, value);
klass.prototype[k] = temp;
} else {
klass.prototype[k] = value;
} } if (!klass.prototype.initialize)
klass.prototype.initialize = function () { }; klass.prototype.constructor = klass; return klass;
}; // @description 返回需要的函数
method.getNeedFn = function (key, scope) {
scope = scope || window;
if (_.isFunction(key)) return key;
if (_.isFunction(scope[key])) return scope[key];
return function () { };
}; method.callmethod = function (method, scope, params) {
scope = scope || this;
if (_.isFunction(method)) {
method.apply(scope, params);
return true;
} return false;
}; /**
* @description 在fn方法的前后通过键值设置两个传入的回调
* @param fn {function} 调用的方法
* @param beforeFnKey {string} 从context对象中获得的函数指针的键值,该函数在fn前执行
* @param afterFnKey {string} 从context对象中获得的函数指针的键值,该函数在fn后执行
* @param context {object} 执行环节的上下文
* @return {function}
*/
method.wrapmethod = method.insert = function (fn, beforeFnKey, afterFnKey, context) { var scope = context || this;
var action = _.wrap(fn, function (func) { _.callmethod(_.getNeedFn(beforeFnKey, scope), scope); func.call(scope); _.callmethod(_.getNeedFn(afterFnKey, scope), scope);
}); return _.callmethod(action, scope);
} _.extend(_, method); })(window);

简单alert框

首先我们来做一个简单的alert框,这个框在我们点击界面时候弹出一个提示,提示文字由文本框给出

第一步便是简单的HTML了

 <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">
<link href="../style/main.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.cui-alert { width: auto; position: static; }
.txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; }
</style>
</head>
<body>
<article class="container">
</article>
<input type="text" id="addmsg" class="txt">
<button id="addbtn" class="btn">
show message</button>
<script type="text/underscore-template" id="template-alert">
<div class=" cui-alert" >
<div class="cui-pop-box">
<div class="cui-bd">
<p class="cui-error-tips"><%=content%></p>
<div class="cui-roller-btns">
<div class="cui-flexbd cui-btns-cancel"><%=cancel%></div>
<div class="cui-flexbd cui-btns-sure"><%=confirm%></div>
</div>
</div>
</div>
</div>
</script>
<script type="text/javascript" src="../../vendor/underscore-min.js"></script>
<script type="text/javascript" src="../../vendor/zepto.min.js"></script>
<script src="../../src/underscore.extend.js" type="text/javascript"></script>
<script src="../../src/mvc.js" type="text/javascript"></script>
<script type="text/javascript" src="ui.alert.js"></script>
</body>
</html>

因为该插件本身比较简单,不存在状态值便会,所以view定义如此即可

 var htmltemplate = $('#template-alert').html();

 var AlertView = _.inherit(Dalmatian.View, {
templateSet: {
0: htmltemplate
}, statusSet: {
STATUS_INIT: 0
}
});

Adapter也比较简单

var Adapter = _.inherit(Dalmatian.Adapter, {
parse: function (data) {
return data;
}
});

现在重点便是controller了

 var Controller = _.inherit(Dalmatian.ViewController, {
render: function () {
var data = this.adapter.viewmodel;
this.view.render(this.viewstatus, data);
}, set: function (options) {
this.adapter.datamodel.content = options.content;
this.adapter.notifyDataChanged();
}, events: {
"click .cui-btns-cancel": "cancelaction"
}, cancelaction: function () {
this.onCancelBtnClick();
}, attr: function (key, value) {
this[key] = value;
}
});

这里有个不一样的地方便是,这里有一个Adapter的set方法,set之后会改变其状态,这里会发生一次通知view更新的动作

最后我们将之串联起来

var view = new AlertView()
var adapter = new Adapter();
var controller = new Controller({
view: view,
adapter: adapter,
container: '.container',
onViewBeforeCreate: function () { var origindata = {
content: 'fuck',
confirm: 'confirmbtn',
cancel: 'cancelbtn'
} this.adapter.format(origindata); this.adapter.registerObserver(this);
this.viewstatus = this.view.statusSet.STATUS_INIT;
},
onCancelBtnClick: function () {
alert('cancel 2')
}
});

然后我们写一段业务代码

 $('#addbtn').on('click', function (e) {
var content = $('#addmsg').val();
// adapter.datamodel.content = content;
// adapter.notifyDataChanged();
controller.set({ content: content });
controller.show();
});

基本完成我们的操作了

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

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

事实上,我对这段代码并不是十分满意,于是,我们这里做一次简单重构:

 var htmltemplate = $('#template-alert').html();

 var AlertView = _.inherit(Dalmatian.View, {
templateSet: {
0: htmltemplate
}, statusSet: {
STATUS_INIT: 0
}
}); var Adapter = _.inherit(Dalmatian.Adapter, {
parse: function (data) {
return data;
}
}); var Controller = _.inherit(Dalmatian.ViewController, {
//设置默认信息
_initialize: function () {
this.origindata = {
content: '',
confirm: '确定',
cancel: '取消'
}
}, initialize: function ($super, opts) {
this._initialize();
$super(opts);
this._init();
}, //基础数据处理
_init: function () {
this.adapter.format(this.origindata);
this.adapter.registerObserver(this);
this.viewstatus = this.view.statusSet.STATUS_INIT;
}, render: function () {
var data = this.adapter.viewmodel;
this.view.render(this.viewstatus, data);
}, set: function (options) {
_.extend(this.adapter.datamodel, options);
// this.adapter.datamodel.content = options.content;
this.adapter.notifyDataChanged();
}, events: {
"click .cui-btns-cancel": "cancelaction"
}, cancelaction: function () {
this.onCancelBtnClick();
}
}); var view = new AlertView()
var adapter = new Adapter(); var controller = new Controller({
view: view,
adapter: adapter,
container: '.container',
onCancelBtnClick: function () {
alert('cancel 2')
}
}); $('#addbtn').on('click', function (e) {
var content = $('#addmsg').val();
// adapter.datamodel.content = content;
// adapter.notifyDataChanged();
controller.set({ content: content, confirm: '确定1' });
controller.show();
});

这个例子结束后,我们来写另一个例子

todolist

Backbone有一个todoList,我们这里也来写一个阉割版的,因为若是今天全部时间来写这个,后面就没法继续了

这个例子事实上也比较简单了,首先看我们的HTML结构

 <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">
</head>
<body>
<article class="container">
</article>
<script type="text/underscore-template" id="template-todolist">
<section class="row">
<div class="col-xs-9">
<form action="">
<legend>To Do List -- Input</legend>
<input type="text" placeholer="ToDoList" id="todoinput">
<button class="btn btn-primary" data-action="add">添加</button>
</form>
<ul id="todolist">
<%_.each(list, function(item){%>
<li><%=item.content %></li>
<%})%>
</ul>
</div>
</section>
</script>
<script type="text/javascript" src="../../vendor/underscore-min.js"></script>
<script type="text/javascript" src="../../vendor/zepto.min.js"></script>
<script src="../../src/underscore.extend.js" type="text/javascript"></script>
<script src="../../src/mvc.js" type="text/javascript"></script>
<script type="text/javascript" src="demo.js"></script>
</body>
</html>

其次是我们的js

 var htmltemplate = $('#template-todolist').html();

 var view = new Dalmatian.View({
templateSet: {
0:htmltemplate
},
statusSet: {
STATUS_INIT: 0
}
}); var Adapter = _.inherit(Dalmatian.Adapter, {
parse: function (origindata) {
return origindata;
}
}); var Controller = _.inherit(Dalmatian.ViewController, {
render: function() {
console.log('controller-render')
var data = this.adapter.viewmodel;
this.view.render(this.viewstatus, data);
}, events: {
'click button': 'action'
}, action: function(e) {
e.preventDefault(); var target = $(e.currentTarget).attr('data-action');
var strategy = {
'add': function(e) {
var value = $('#todoinput').val();
this.adapter.datamodel.list.push({ content: value });
// this.adapter.parse(this.adapter.datamodel);
this.adapter.notifyDataChanged();
}
} strategy[target].apply(this, [e]);
}
}) var controller = new Controller({
view: view,
adapter: new Adapter(),
container: '.container',
onViewBeforeCreate: function () {
this.adapter.format({
list: []
});
this.adapter.registerObserver(this);
this.viewstatus = this.view.statusSet.STATUS_INIT
}
}); controller.show();

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

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

阶段总结

MVC的学习暂时到这里,我们下面继续日历的的东西,虽然我与老大商量后形成了一些自己觉得不错的东西,但是真正使用过程中还是发现一些问题

① 第一个我认为比较大的问题是viewController中的代码,比如

var controller = new Controller({
view: view,
adapter: new Adapter(),
container: '.container',
onViewBeforeCreate: function () {
this.adapter.format({
list: []
});
this.adapter.registerObserver(this);
this.viewstatus = this.view.statusSet.STATUS_INIT
}
});

以及

var controller = new Controller({
view: view,
adapter: adapter,
container: '.container',
onCancelBtnClick: function () {
alert('cancel 2')
}
});

事实上这些代码不应该存在于此,真实情况下我所构想的viewController不会在实例化时候还有如此多的业务相关信息,viewController在实例化时候只应该包含系统级的东西

比如Controller释放出来的接口,比如全局消息监听什么的,显然我们上面代码中的做法是有问题的,这些东西事实上应该在定义ViewController类时,在继承处得到处理

不应该在实例化时候处理,我们viewController实例化时候应该有更重要的使命,这些留待下面解决

上面要表达的意思是,事实上我们ViewController是最后继承下来是需要干业务的事情,所以他应该在几个事件点将要干的事情做完,比如TodoList应该是这样的

 var htmltemplate = $('#template-todolist').html();

 var Adapter = _.inherit(Dalmatian.Adapter, {
parse: function (origindata) {
return origindata;
}
}); var Controller = _.inherit(Dalmatian.ViewController, { //设置默认信息
_initialize: function () {
this.view = new Dalmatian.View({
templateSet: {
0: htmltemplate
},
statusSet: {
STATUS_INIT: 0
}
});
this.adapter = new Adapter(); }, initialize: function ($super, opts) {
this._initialize();
$super(opts);
}, render: function () {
console.log('controller-render')
var data = this.adapter.viewmodel;
this.view.render(this.viewstatus, data);
}, container: '.container',
onViewBeforeCreate: function () {
this.adapter.format({
list: []
});
this.adapter.registerObserver(this);
this.viewstatus = this.view.statusSet.STATUS_INIT
}, events: {
'click button': 'action'
}, action: function (e) {
e.preventDefault(); var target = $(e.currentTarget).attr('data-action');
var strategy = {
'add': function (e) {
var value = $('#todoinput').val();
this.adapter.datamodel.list.push({ content: value });
// this.adapter.parse(this.adapter.datamodel);
this.adapter.notifyDataChanged();
}
} strategy[target].apply(this, [e]);
}
}) var controller = new Controller(); controller.show();

这样的话,业务应该的代码事实上写到了类的几个事件点中了,这些会在实例化时不同的状态被触发,所以根本不必在实例化时做任何操作

实例化时候应该有他的作用,因为继承到这一层的时候,该业务类便专注于处理这个业务了

简单日历

上次,我们的日历基本都成型了,今天我们便根据前面的想法为他做一次封装......

PS:想想有点很傻很天真的感觉......现在的问题是要将原来一个基本算总体的东西,分成三个部分,说实话这样封装的结构首先是让人阅读上稍微困难了

首先仍然是定义view的事情,首先一来就遇到个比较烦的地方,因为之前我们将模板分的很细:

① 星期显示模板

② 月模板

③ 日模板

所以,我们这里便不太好区分,而且还有一定嵌套关系,这里小钗费了一点功夫......

由这块的操作,我们甚至可以调整原来view的逻辑,优化由此一点一点慢慢就开始了......

 <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">
<link href="../style/main.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.cui-alert { width: auto; position: static; }
.txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; }
ul, li { padding: 0; margin: 0; }
.cui_calendar, .cui_week { list-style: none; }
.cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; }
</style>
</head>
<body>
<article class="container">
</article>
<script type="text/template" id="template-calendar">
<ul class="cui_week">
<% var i = 0, day = 0; %>
<%for(day = 0; day < 7; day++) { %>
<li>
<%=weekDayItemTmpt[day] %></li>
<%} %>
</ul> <ul class="cui_calendar">
<% for(i = 0; i < beginWeek; i++) { %>
<li class="cui_invalid"></li>
<% } %>
<% for(i = 0; i < days; i++) { %>
<% day = i + 1; %>
<li class="cui_calendar_item" data-date="<%=year%>-<%=month + 1%>-<%=day%>"><%=day %></li>
<% } %>
</ul>
</script>
<script type="text/javascript" src="../../vendor/underscore-min.js"></script>
<script type="text/javascript" src="../../vendor/zepto.min.js"></script>
<script src="../../src/underscore.extend.js" type="text/javascript"></script>
<script src="../../src/util.js" type="text/javascript"></script>
<script src="../../src/mvc.js" type="text/javascript"></script>
<script type="text/javascript">
var tmpt = $('#template-calendar').html(); var CalendarView = _.inherit(Dalmatian.View, {
templateSet: {
0: tmpt
}, statusSet: {
STATUS_INIT: 0
}
}); var CalendarAdapter = _.inherit(Dalmatian.Adapter, {
_initialize: function ($super) {
$super(); //默认显示方案,可以根据参数修改
//任意一个model发生改变皆会引起update
this.weekDayItemTmpt = ['日', '一', '二', '三', '四', '五', '六'];
}, //该次重新,viewmodel的数据完全来源与parse中多定义
parse: function (data) {
return _.extend({
weekDayItemTmpt: this.weekDayItemTmpt
}, data);
}
}); var CalendarController = _.inherit(Dalmatian.ViewController, { _initialize: function () {
this.view = new CalendarView();
this.adapter = new CalendarAdapter(); //默认业务数据
this.dateObj = new Date();
this.container = '.container'; var s = '';
}, initialize: function ($super, opts) {
this._initialize();
$super(opts);
}, onViewBeforeCreate: function () { //使用adpter之前必须注册监听以及格式化viewModel,此操作应该封装起来
this.adapter.registerObserver(this);
this.adapter.format(this._getMonthData(this.dateObj.getFullYear(), this.dateObj.getMonth())); //view显示之前必定会给予状态,此应该封装
this.viewstatus = this.view.statusSet.STATUS_INIT; var s = '';
}, render: function () {
//该操作可封装
var data = this.adapter.viewmodel;
this.view.render(this.viewstatus, data);
}, //根据传入年月,返回该月相关数据
_getMonthData: function (year, month) {
this.date = new Date(year, month);
var d = new Date(year, month);
//description 获取天数
var days = dateUtil.getDaysOfMonth(d);
//description 获取那个月第一天时星期几
var _beginWeek = dateUtil.getBeginDayOfMouth(d);
return {
year: d.getFullYear(),
month: d.getMonth(),
beginWeek: _beginWeek,
days: days
};
}
}); var calendar = new CalendarController();
calendar.show(); </script>
</body>
</html>

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

首次调整后,大概的东西出来了,这样一次操作后就会发现之前定义的MVC一些不合理的地方

① 操作Adapter有parse与format两个地方,我们用着用着就会分不清,应该只对外暴露一个借口

② Controller处操作Adapter以及view也会有多个地方事实上有一些必定会发生的流程我们应该封装起来,类似:

//使用adpter之前必须注册监听以及格式化viewModel,此操作应该封装起来
this.adapter.registerObserver(this);
this.adapter.format(this._getMonthData(this.dateObj.getFullYear(), this.dateObj.getMonth())); //view显示之前必定会给予状态,此应该封装
this.viewstatus = this.view.statusSet.STATUS_INIT;

③ 整个MVC的逻辑还是有一些不太清晰的地方,这个留待后续调整

这个时候我们将之前的一些借口加入进来,比如我们的handleDay

handleDay: function (dateStr, fn) {
if (dateUtil.isDate(dateStr)) dateStr = dateUtil.format(dateStr, 'Y-m-d');
var el = this.viewcontent.find('[data-date="' + dateStr + '"]'); if (typeof fn == 'function') fn(el, dateUtil.parse(dateStr, 'y-m-d'), this); }
var calendar = new CalendarController();
calendar.show(); calendar.handleDay(new Date(), function (el, date, calendar) {
el.html('今天');
});

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

现在如果有事件绑定的话,便注册至viewController即可,我这里便暂时结束了

结语

通过今天的学习,我将与我老大研究出来的MVC的东东搞了出来,事实证明还是需要有一些优化的......

今天状态不是太好,今天暂时到此,剩下的我们后面点来,这块还有很多东西要清理呢。。。。。。