【单页应用】理解MVC
前言
之前我们为view引入了wrapperSet的概念,想以此解决view局部刷新问题,后来发现这个方案不太合理
view里面插入了业务相关的代码,事实上这个是应该剥离出去,业务的需求千奇百怪,我们不应该去处理
view现在只提供最基础的功能:
① 定义各个状态的模板
② 渲染模板
整个view的逻辑便该结束了,有一个比较特殊的情况是,当状态值不变的情况就应该是更新,这个可能会有不一样的逻辑也应该划出去
Adapter的意义在于存储view渲染过程中需要的data数据,从组成上分为
① datamodel
② viewmodel
datamodel用于具体操作,viewmodel被干掉了,提供一个getViewModel的方法替换,并且对外提供一个format方法用于用户继承
format的参数便是datamodel,这里通过处理返回的数据便是我们所谓的viewModel,他将会用于view生成对应html
然后datamodel的改变会引起对应view的变化,这个变化发起端与控制端皆在viewController,最后viewController会通知到view重新渲染
Controller依旧是交互的核心,他是连接Adapter以及view的桥梁
view与Adapter本身并没有关联,Controller将之联系到了一起:
① 在实例化时候便会关联一个Adapter以及view的实例,这里Adapter不是必须的
② viewController会保留一个view的根节点,view的根节点只会存在一个
③ viewController会在Adapter实例上监听自身,在adpter datamodel发生变化时候通知到自己,便会触发update事件
④ 传入初始状态的status以及Adapter的datamodel,调用view的render方法,会生成当前状态的view的html
⑤ 将之装入view的根节点,并且为viewController的this.$el赋值,create的逻辑结束
⑥ 触发viewController show事件,将events绑定到根节点,将$el append到container容器中并显示,初步逻辑结束
⑦ viewController有几个事件点用于用户注册,本身也具有很多一系列dom事件,可能导致datamodel的变化
⑧ 若是Adapter的datamodel发生变化便会触发dataAdpter改变的notify事件,这个时候viewController便会有所反应
⑨ datamodel的改变会触发viewController的update事件,默认会再次触发render事件重新新渲染结构
由于render会放给view自定义,所以其中需要执行的逻辑便不需要我们的关注了
实例
这个便是一个标准的MVC模型,借IOS MVC的模型来说
核心操控点在于Controller,Controller会组织model以及view
由于Controller上注册的各系列事件,会引起model的变化,
每次model产生变化后会重新通知,Controller 通知view update操作
这个时候view会获取viewController的状态值以及model的数据渲染出新的view
view只负责html渲染
model只负责操作数据,并且通知观察者改变事件
Controller将view以及model联系起来,所有的事件全部注册至Controller
PS:传统的View会包含事件交互,这里放到了Controller上
模型会把datamodel的改变通知到控制器,控制器会更新视图信息,控制器根据view组成dom结构,并且注册各种UI事件,又会触发datamodel的各种改变
这就达到了理想情况的view与model的分离,一个model(adpter可用于多个viewController),一个dataAdpter的改变会影响两个视图的改变
这个MVC可以完全解耦view以及model,view的变化相当频繁,若是model控制view渲染便会降低model的重用
这里首先举一个例子做简单说明:
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>ToDoList</title> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css"> 8 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css"> 9 <link href="../style/main.css" rel="stylesheet" type="text/css" /> 10 <style type="text/css"> 11 .cui-alert { width: auto; position: static; } 12 .txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; } 13 ul, li { padding: 0; margin: 0; } 14 .cui_calendar, .cui_week { list-style: none; } 15 .cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; } 16 </style> 17 </head> 18 <body> 19 <article id="container"> 20 </article> 21 <script type="text/underscore-template" id="template-ajax-init"> 22 <div class="cui-alert" > 23 <div class="cui-pop-box"> 24 <div class="cui-hd"> 25 <%=title%> 26 </div> 27 <div class="cui-bd"> 28 <div class="cui-error-tips"> 29 </div> 30 <div class="cui-roller-btns" style="padding: 4px; "><input type="text" placeholder="设置最低价 {day: '', price: ''}" style="margin: 2px; width: 100%; " id="ajax_data" class="txt" value="{day: , price: }"></div> 31 <div class="cui-roller-btns"> 32 <div class="cui-flexbd cui-btns-sure"><%=confirm%></div> 33 </div> 34 </div> 35 </div> 36 </div> 37 </script> 38 <script type="text/underscore-template" id="template-ajax-suc"> 39 <ul> 40 <li>最低价:本月<%=ajaxData.day %>号,价格:<%=ajaxData.price %> 元</li> 41 </ul> 42 </script> 43 44 <script type="text/underscore-template" id="template-ajax-loading"> 45 <span>loading....</span> 46 </script> 47 48 <script src="../../vendor/underscore-min.js" type="text/javascript"></script> 49 <script src="../../vendor/zepto.min.js" type="text/javascript"></script> 50 <script src="../../src/underscore-extend.js" type="text/javascript"></script> 51 <script src="../../src/util.js" type="text/javascript"></script> 52 <script src="../../src/mvc.js" type="text/javascript"></script> 53 <script type="text/javascript"> 54 55 //模拟Ajax请求 56 function getAjaxData(callback, data) { 57 setTimeout(function () { 58 if (!data) { 59 data = {day: 3, price: 20}; 60 } 61 callback(data); 62 }, 1000); 63 } 64 65 var AjaxView = _.inherit(Dalmatian.View, { 66 _initialize: function ($super) { 67 //设置默认属性 68 $super(); 69 70 this.templateSet = { 71 init: $('#template-ajax-init').html(), 72 loading: $('#template-ajax-loading').html(), 73 ajaxSuc: $('#template-ajax-suc').html() 74 }; 75 76 } 77 }); 78 79 var AjaxAdapter = _.inherit(Dalmatian.Adapter, { 80 _initialize: function ($super) { 81 $super(); 82 this.datamodel = { 83 title: '标题', 84 confirm: '刷新数据' 85 }; 86 this.datamodel.ajaxData = {}; 87 }, 88 89 format: function (datamodel) { 90 //处理datamodel生成viewModel的逻辑 91 return datamodel; 92 }, 93 94 ajaxLoading: function () { 95 this.notifyDataChanged(); 96 }, 97 98 ajaxSuc: function (data) { 99 this.datamodel.ajaxData = data; 100 this.notifyDataChanged(); 101 } 102 }); 103 104 var AjaxViewController = _.inherit(Dalmatian.ViewController, { 105 _initialize: function ($super) { 106 $super(); 107 //设置基本的属性 108 this.view = new AjaxView(); 109 this.adapter = new AjaxAdapter(); 110 this.viewstatus = 'init'; 111 this.container = '#container'; 112 }, 113 114 //处理datamodel变化引起的dom改变 115 render: function (data) { 116 //这里用户明确知道自己有没有viewdata 117 var viewdata = this.adapter.getViewModel(); 118 var wrapperSet = { 119 loading: '.cui-error-tips', 120 ajaxSuc: '.cui-error-tips' 121 }; 122 //view具有唯一包裹器 123 var root = this.view.root; 124 var selector = wrapperSet[this.viewstatus]; 125 126 if (selector) { 127 root = root.find(selector); 128 } 129 130 this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 131 132 root.html(this.view.html); 133 134 }, 135 136 //显示后Ajax请求数据 137 onViewAfterShow: function () { 138 this._handleAjax(); 139 }, 140 141 _handleAjax: function (data) { 142 this.setViewStatus('loading'); 143 this.adapter.ajaxLoading(); 144 getAjaxData($.proxy(function (data) { 145 this.setViewStatus('ajaxSuc'); 146 this.adapter.ajaxSuc(data); 147 }, this), data); 148 }, 149 150 events: { 151 'click .cui-btns-sure': function () { 152 var data = this.$el.find('#ajax_data').val(); 153 data = eval('(' + data + ')'); 154 this._handleAjax(data); 155 } 156 } 157 }); 158 159 var a = new AjaxViewController(); 160 a.show(); 161 162 </script> 163 </body> 164 </html>
1 (function () { 2 var _ = window._; 3 if (typeof require === 'function' && !_) { 4 _ = require('underscore'); 5 }; 6 7 // @description 全局可能用到的变量 8 var arr = []; 9 var slice = arr.slice; 10 11 var method = method || {}; 12 13 14 /** 15 * @description inherit方法,js的继承,默认为两个参数 16 * @param {function} supClass 可选,要继承的类 17 * @param {object} subProperty 被创建类的成员 18 * @return {function} 被创建的类 19 */ 20 method.inherit = function () { 21 22 // @description 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类 23 if (arguments.length === 0 || arguments.length > 2) throw '参数错误'; 24 25 var parent = null; 26 27 // @description 将参数转换为数组 28 var properties = slice.call(arguments); 29 30 // @description 如果第一个参数为类(function),那么就将之取出 31 if (typeof properties[0] === 'function') 32 parent = properties.shift(); 33 properties = properties[0]; 34 35 // @description 创建新类用于返回 36 function klass() { 37 if (_.isFunction(this.initialize)) 38 this.initialize.apply(this, arguments); 39 } 40 41 klass.superclass = parent; 42 // parent.subclasses = []; 43 44 if (parent) { 45 // @description 中间过渡类,防止parent的构造函数被执行 46 var subclass = function () { }; 47 subclass.prototype = parent.prototype; 48 klass.prototype = new subclass(); 49 // parent.subclasses.push(klass); 50 } 51 52 var ancestor = klass.superclass && klass.superclass.prototype; 53 for (var k in properties) { 54 var value = properties[k]; 55 56 //满足条件就重写 57 if (ancestor && typeof value == 'function') { 58 var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(','); 59 //只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定) 60 if (argslist[0] === '$super' && ancestor[k]) { 61 value = (function (methodName, fn) { 62 return function () { 63 var scope = this; 64 var args = [function () { 65 return ancestor[methodName].apply(scope, arguments); 66 } ]; 67 return fn.apply(this, args.concat(slice.call(arguments))); 68 }; 69 })(k, value); 70 } 71 } 72 73 //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展 74 if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) { 75 //原型链是共享的,这里不好办 76 var temp = {}; 77 _.extend(temp, klass.prototype[k]); 78 _.extend(temp, value); 79 klass.prototype[k] = temp; 80 } else { 81 klass.prototype[k] = value; 82 } 83 84 } 85 86 if (!klass.prototype.initialize) 87 klass.prototype.initialize = function () { }; 88 89 klass.prototype.constructor = klass; 90 91 return klass; 92 }; 93 94 // @description 返回需要的函数 95 method.getNeedFn = function (key, scope) { 96 scope = scope || window; 97 if (_.isFunction(key)) return key; 98 if (_.isFunction(scope[key])) return scope[key]; 99 return function () { }; 100 }; 101 102 method.callmethod = function (method, scope, params) { 103 scope = scope || this; 104 if (_.isFunction(method)) { 105 return _.isArray(params) ? method.apply(scope, params) : method.call(scope, params); 106 } 107 108 return false; 109 }; 110 111 //获取url参数 112 method.getUrlParam = function (url, name) { 113 var i, arrQuery, _tmp, query = {}; 114 var index = url.lastIndexOf('//'); 115 var http = url.substr(index, url.length); 116 117 url = url.substr(0, index); 118 arrQuery = url.split('&'); 119 120 for (i = 0, len = arrQuery.length; i < len; i++) { 121 _tmp = arrQuery[i].split('='); 122 if (i == len - 1) _tmp[1] += http; 123 query[_tmp[0]] = _tmp[1]; 124 } 125 126 return name ? query[name] : query; 127 }; 128 129 130 /** 131 * @description 在fn方法的前后通过键值设置两个传入的回调 132 * @param fn {function} 调用的方法 133 * @param beforeFnKey {string} 从context对象中获得的函数指针的键值,该函数在fn前执行 134 * @param afterFnKey {string} 从context对象中获得的函数指针的键值,该函数在fn后执行 135 * @param context {object} 执行环节的上下文 136 * @return {function} 137 */ 138 method.wrapmethod = method.insert = function (fn, beforeFnKey, afterFnKey, context) { 139 140 var scope = context || this; 141 var action = _.wrap(fn, function (func) { 142 143 _.callmethod(_.getNeedFn(beforeFnKey, scope), scope); 144 145 func.call(scope); 146 147 _.callmethod(_.getNeedFn(afterFnKey, scope), scope); 148 }); 149 150 return _.callmethod(action, scope); 151 }; 152 153 method.Hash = method.inherit({ 154 inisilize: function (opts) { 155 this.keys = []; 156 this.values = []; 157 }, 158 159 length: function () { 160 return this.keys.length; 161 }, 162 163 //传入order,若是数组中存在的话会将之放到最后,保证数组的唯一性,因为这个是hash,不能存在重复的键 164 push: function (key, value, order) { 165 if (_.isObject(key)) { 166 for (var i in key) { 167 if (key.hasOwnProperty(i)) this.push(i, key[i], order); 168 } 169 return; 170 } 171 172 var index = _.indexOf(key, this.keys); 173 174 if (index != -1 && !order) { 175 this.values[index] = value; 176 } else { 177 if (order) this.remove(key); 178 this.keys.push(key); 179 this.vaules.push(value); 180 } 181 182 }, 183 184 remove: function (key) { 185 return this.removeByIndex(_.indexOf(key, this.keys)); 186 }, 187 188 removeByIndex: function (index) { 189 if (index == -1) return this; 190 191 this.keys.splice(index, 1); 192 this.values.splice(index, 1); 193 194 return this; 195 }, 196 197 pop: function () { 198 if (!this.length()) return; 199 200 this.keys.pop(); 201 return this.values.pop(); 202 }, 203 204 //根据索引返回对应键值 205 indexOf: function (value) { 206 var index = _.indexOf(value, this.vaules); 207 if (index != -1) return this.keys[index]; 208 return -1; 209 }, 210 211 //移出栈底值 212 shift: function () { 213 if (!this.length()) return; 214 215 this.keys.shift(); 216 return this.values.shift(); 217 }, 218 219 //往栈顶压入值 220 unShift: function (key, vaule, order) { 221 if (_.isObject(key)) { 222 for (var i in key) { 223 if (key.hasOwnProperty(i)) this.unShift(i, key[i], order); 224 } 225 return; 226 } 227 if (order) this.remove(key); 228 this.keys.unshift(key); 229 this.vaules.unshift(value); 230 }, 231 232 //返回hash表的一段数据 233 // 234 slice: function (start, end) { 235 var keys = this.keys.slice(start, end || null); 236 var values = this.values.slice(start, end || null); 237 var hash = new _.Hash(); 238 239 for (var i = 0; i < keys.length; i++) { 240 hash.push(keys[i], values[i]); 241 } 242 243 return obj; 244 }, 245 246 //由start开始,移除元素 247 splice: function (start, count) { 248 var keys = this.keys.splice(start, end || null); 249 var values = this.values.splice(start, end || null); 250 var hash = new _.Hash(); 251 252 for (var i = 0; i < keys.length; i++) { 253 hash.push(keys[i], values[i]); 254 } 255 256 return obj; 257 }, 258 259 exist: function (key, value) { 260 var b = true; 261 262 if (_.indexOf(key, this.keys) == -1) b = false; 263 264 if (!_.isUndefined(value) && _.indexOf(value, this.values) == -1) b = false; 265 266 return b; 267 }, 268 269 270 filter: function () { 271 272 } 273 274 }); 275 276 _.extend(_, method); 277 278 279 // if (module && module.exports) 280 // module.exports = _; 281 282 }).call(this);
1 /** 2 * @description 静态日期操作类,封装系列日期操作方法 3 * @description 输入时候月份自动减一,输出时候自动加一 4 * @return {object} 返回操作方法 5 */ 6 var dateUtil = { 7 /** 8 * @description 数字操作, 9 * @return {string} 返回处理后的数字 10 */ 11 formatNum: function (n) { 12 if (n < 10) return '0' + n; 13 return n; 14 }, 15 /** 16 * @description 将字符串转换为日期,支持格式y-m-d ymd (y m r)以及标准的 17 * @return {Date} 返回日期对象 18 */ 19 parse: function (dateStr, formatStr) { 20 if (typeof dateStr === 'undefined') return null; 21 if (typeof formatStr === 'string') { 22 var _d = new Date(formatStr); 23 //首先取得顺序相关字符串 24 var arrStr = formatStr.replace(/[^ymd]/g, '').split(''); 25 if (!arrStr && arrStr.length != 3) return null; 26 27 var formatStr = formatStr.replace(/y|m|d/g, function (k) { 28 switch (k) { 29 case 'y': return '(\\d{4})'; 30 case 'm': ; 31 case 'd': return '(\\d{1,2})'; 32 } 33 }); 34 35 var reg = new RegExp(formatStr, 'g'); 36 var arr = reg.exec(dateStr) 37 38 var dateObj = {}; 39 for (var i = 0, len = arrStr.length; i < len; i++) { 40 dateObj[arrStr[i]] = arr[i + 1]; 41 } 42 return new Date(dateObj['y'], dateObj['m'] - 1, dateObj['d']); 43 } 44 return null; 45 }, 46 /** 47 * @description将日期格式化为字符串 48 * @return {string} 常用格式化字符串 49 */ 50 format: function (date, format) { 51 if (arguments.length < 2 && !date.getTime) { 52 format = date; 53 date = new Date(); 54 } 55 typeof format != 'string' && (format = 'Y年M月D日 H时F分S秒'); 56 return format.replace(/Y|y|M|m|D|d|H|h|F|f|S|s/g, function (a) { 57 switch (a) { 58 case "y": return (date.getFullYear() + "").slice(2); 59 case "Y": return date.getFullYear(); 60 case "m": return date.getMonth() + 1; 61 case "M": return dateUtil.formatNum(date.getMonth() + 1); 62 case "d": return date.getDate(); 63 case "D": return dateUtil.formatNum(date.getDate()); 64 case "h": return date.getHours(); 65 case "H": return dateUtil.formatNum(date.getHours()); 66 case "f": return date.getMinutes(); 67 case "F": return dateUtil.formatNum(date.getMinutes()); 68 case "s": return date.getSeconds(); 69 case "S": return dateUtil.formatNum(date.getSeconds()); 70 } 71 }); 72 }, 73 // @description 是否为为日期对象,该方法可能有坑,使用需要慎重 74 // @param year {num} 日期对象 75 // @return {boolean} 返回值 76 isDate: function (d) { 77 if ((typeof d == 'object') && (d instanceof Date)) return true; 78 return false; 79 }, 80 // @description 是否为闰年 81 // @param year {num} 可能是年份或者为一个date时间 82 // @return {boolean} 返回值 83 isLeapYear: function (year) { 84 //传入为时间格式需要处理 85 if (dateUtil.isDate(year)) year = year.getFullYear() 86 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 87 else return false; 88 }, 89 90 // @description 获取一个月份的天数 91 // @param year {num} 可能是年份或者为一个date时间 92 // @param year {num} 月份 93 // @return {num} 返回天数 94 getDaysOfMonth: function (year, month) { 95 //自动减一以便操作 96 month--; 97 if (dateUtil.isDate(year)) { 98 month = year.getMonth(); //注意此处月份要加1,所以我们要减一 99 year = year.getFullYear(); 100 } 101 return [31, dateUtil.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 102 }, 103 104 // @description 获取一个月份1号是星期几,注意此时的月份传入时需要自主减一 105 // @param year {num} 可能是年份或者为一个date时间 106 // @param year {num} 月份 107 // @return {num} 当月一号为星期几0-6 108 getBeginDayOfMouth: function (year, month) { 109 //自动减一以便操作 110 month--; 111 if ((typeof year == 'object') && (year instanceof Date)) { 112 month = year.getMonth(); 113 year = year.getFullYear(); 114 } 115 var d = new Date(year, month, 1); 116 return d.getDay(); 117 } 118 };
1 "use strict"; 2 3 // ------------------华丽的分割线--------------------- // 4 5 // @description 正式的声明Dalmatian框架的命名空间 6 var Dalmatian = Dalmatian || {}; 7 8 // @description 定义默认的template方法来自于underscore 9 Dalmatian.template = _.template; 10 Dalmatian.View = _.inherit({ 11 // @description 构造函数入口 12 initialize: function (options) { 13 this._initialize(); 14 this.handleOptions(options); 15 this._initRoot(); 16 17 }, 18 19 _initRoot: function () { 20 //根据html生成的dom包装对象 21 //有一种场景是用户的view本身就是一个只有一个包裹器的结构,他不想要多余的包裹器 22 this.root = $(this.defaultContainerTemplate); 23 this.root.attr('id', this.viewid); 24 }, 25 26 // @description 设置默认属性 27 _initialize: function () { 28 29 var DEFAULT_CONTAINER_TEMPLATE = '<section class="view" id="<%=viewid%>"><%=html%></section>'; 30 31 // @description view状态机 32 // this.statusSet = {}; 33 34 this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE; 35 36 // @override 37 // @description template集合,根据status做template的map 38 // @example 39 // { 0: '<ul><%_.each(list, function(item){%><li><%=item.name%></li><%});%></ul>' } 40 // this.templateSet = {}; 41 42 this.viewid = _.uniqueId('dalmatian-view-'); 43 44 }, 45 46 // @description 操作构造函数传入操作 47 handleOptions: function (options) { 48 // @description 从形参中获取key和value绑定在this上 49 if (_.isObject(options)) _.extend(this, options); 50 51 }, 52 53 // @description 通过模板和数据渲染具体的View 54 // @param status {enum} View的状态参数 55 // @param data {object} 匹配View的数据格式的具体数据 56 // @param callback {functiion} 执行完成之后的回调 57 render: function (status, data, callback) { 58 59 var templateSelected = this.templateSet[status]; 60 if (templateSelected) { 61 62 // @description 渲染view 63 var templateFn = Dalmatian.template(templateSelected); 64 this.html = templateFn(data); 65 66 //这里减少一次js编译 67 // this.root.html(''); 68 // this.root.append(this.html); 69 70 this.currentStatus = status; 71 72 _.callmethod(callback, this); 73 74 return this.html; 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, data); 93 } 94 }); 95 96 Dalmatian.Adapter = _.inherit({ 97 98 // @description 构造函数入口 99 initialize: function (options) { 100 this._initialize(); 101 this.handleOptions(options); 102 103 }, 104 105 // @description 设置默认属性 106 _initialize: function () { 107 this.observers = []; 108 // this.viewmodel = {}; 109 this.datamodel = {}; 110 }, 111 112 // @description 操作构造函数传入操作 113 handleOptions: function (options) { 114 // @description 从形参中获取key和value绑定在this上 115 if (_.isObject(options)) _.extend(this, options); 116 }, 117 118 // @override 119 // @description 设置 120 format: function (datamodel) { 121 return datamodel; 122 }, 123 124 getViewModel: function () { 125 return this.format(this.datamodel); 126 }, 127 128 registerObserver: function (viewcontroller) { 129 // @description 检查队列中如果没有viewcontroller,从队列尾部推入 130 if (!_.contains(this.observers, viewcontroller)) { 131 this.observers.push(viewcontroller); 132 } 133 }, 134 135 unregisterObserver: function (viewcontroller) { 136 // @description 从observers的队列中剔除viewcontroller 137 this.observers = _.without(this.observers, viewcontroller); 138 }, 139 140 //统一设置所有观察者的状态,因为对应观察者也许根本不具备相关状态,所以这里需要处理 141 // setStatus: function (status) { 142 // _.each(this.observers, function (viewcontroller) { 143 // if (_.isObject(viewcontroller)) 144 // viewcontroller.setViewStatus(status); 145 // }); 146 // }, 147 148 notifyDataChanged: function () { 149 // @description 通知所有注册的观察者被观察者的数据发生变化 150 var data = this.getViewModel(); 151 _.each(this.observers, function (viewcontroller) { 152 if (_.isObject(viewcontroller)) 153 _.callmethod(viewcontroller.update, viewcontroller, [data]); 154 }); 155 } 156 }); 157 158 Dalmatian.ViewController = _.inherit({ 159 160 _initialize: function () { 161 162 //用户设置的容器选择器,或者dom结构 163 this.container; 164 //根元素 165 this.$el; 166 167 //一定会出现 168 this.view; 169 //可能会出现 170 this.adapter; 171 //初始化的时候便需要设置view的状态,否则会渲染失败,这里给一个默认值 172 this.viewstatus = 'init'; 173 174 }, 175 176 // @description 构造函数入口 177 initialize: function (options) { 178 this._initialize(); 179 this.handleOptions(options); 180 this._handleAdapter(); 181 this.create(); 182 }, 183 184 //处理dataAdpter中的datamodel,为其注入view的默认容器数据 185 _handleAdapter: function () { 186 //不存在就不予理睬 187 if (!this.adapter) return; 188 this.adapter.registerObserver(this); 189 }, 190 191 // @description 操作构造函数传入操作 192 handleOptions: function (options) { 193 if (!options) return; 194 195 this._verify(options); 196 197 // @description 从形参中获取key和value绑定在this上 198 if (_.isObject(options)) _.extend(this, options); 199 }, 200 201 setViewStatus: function (status) { 202 this.viewstatus = status; 203 }, 204 205 // @description 验证参数 206 _verify: function (options) { 207 //这个underscore方法新框架在报错 208 // if (!_.property('view')(options) && (!this.view)) throw Error('view必须在实例化的时候传入ViewController'); 209 if (options.view && (!this.view)) throw Error('view必须在实例化的时候传入ViewController'); 210 }, 211 212 // @description 当数据发生变化时调用onViewUpdate,如果onViewUpdate方法不存在的话,直接调用render方法重绘 213 update: function (data) { 214 215 // _.callmethod(this.hide, this); 216 217 if (this.onViewUpdate) { 218 _.callmethod(this.onViewUpdate, this, [data]); 219 return; 220 } 221 this.render(); 222 223 // _.callmethod(this.show, this); 224 }, 225 226 /** 227 * @override 228 */ 229 render: function () { 230 // @notation 这个方法需要被复写 231 this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 232 this.view.root.html(this.view.html); 233 }, 234 235 _create: function () { 236 this.render(); 237 238 //render 结束后构建好根元素dom结构 239 this.view.root.html(this.view.html); 240 this.$el = this.view.root; 241 }, 242 243 create: function () { 244 245 //l_wang这块不是很明白 246 //是否检查映射关系,不存在则recreate,但是在这里dom结构未必在document上 247 // if (!$('#' + this.view.viewid)[0]) { 248 // return _.callmethod(this.recreate, this); 249 // } 250 251 // @notation 在create方法调用前后设置onViewBeforeCreate和onViewAfterCreate两个回调 252 _.wrapmethod(this._create, 'onViewBeforeCreate', 'onViewAfterCreate', this); 253 254 }, 255 256 /** 257 * @description 如果进入create判断是否需要update一下页面,sync view和viewcontroller的数据 258 */ 259 _recreate: function () { 260 this.update(); 261 }, 262 263 recreate: function () { 264 _.wrapmethod(this._recreate, 'onViewBeforeRecreate', 'onViewAfterRecreate', this); 265 }, 266 267 //事件注册点 268 bindEvents: function (events) { 269 if (!(events || (events = _.result(this, 'events')))) return this; 270 this.unBindEvents(); 271 272 // @description 解析event参数的正则 273 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 274 var key, method, match, eventName, selector; 275 276 //注意,此处做简单的字符串数据解析即可,不做实际业务 277 for (key in events) { 278 method = events[key]; 279 if (!_.isFunction(method)) method = this[events[key]]; 280 if (!method) continue; 281 282 match = key.match(delegateEventSplitter); 283 eventName = match[1], selector = match[2]; 284 method = _.bind(method, this); 285 eventName += '.delegateEvents' + this.view.viewid; 286 287 if (selector === '') { 288 this.$el.on(eventName, method); 289 } else { 290 this.$el.on(eventName, selector, method); 291 } 292 } 293 294 return this; 295 }, 296 297 //取消所有事件 298 unBindEvents: function () { 299 this.$el.off('.delegateEvents' + this.view.viewid); 300 return this; 301 }, 302 303 _show: function () { 304 this.bindEvents(); 305 $(this.container).append(this.$el); 306 this.$el.show(); 307 }, 308 309 show: function () { 310 _.wrapmethod(this._show, 'onViewBeforeShow', 'onViewAfterShow', this); 311 }, 312 313 _hide: function () { 314 this.forze(); 315 this.$el.hide(); 316 }, 317 318 hide: function () { 319 _.wrapmethod(this._hide, 'onViewBeforeHide', 'onViewAfterHide', this); 320 }, 321 322 _forze: function () { 323 this.unBindEvents(); 324 }, 325 326 forze: function () { 327 _.wrapmethod(this._forze, 'onViewBeforeForzen', 'onViewAfterForzen', this); 328 }, 329 330 _destory: function () { 331 this.unBindEvents(); 332 this.$el.remove(); 333 // delete this; 334 }, 335 336 destory: function () { 337 _.wrapmethod(this._destory, 'onViewBeforeDestory', 'onViewAfterDestory', this); 338 } 339 });
代码效果如下:
每次点击刷新数据时候会模拟一次Ajax操作,将datamodel中的数据改变,然后会触发视图改变
1 events: { 2 'click .cui-btns-sure': function () { 3 var data = this.$el.find('#ajax_data').val(); 4 data = eval('(' + data + ')'); 5 this._handleAjax(data); 6 } 7 }
1 _handleAjax: function (data) { 2 this.setViewStatus('loading'); 3 this.adapter.ajaxLoading(); 4 getAjaxData($.proxy(function (data) { 5 this.setViewStatus('ajaxSuc'); 6 this.adapter.ajaxSuc(data); 7 }, this), data); 8 },
ajaxSuc: function (data) { this.datamodel.ajaxData = data; this.notifyDataChanged(); }
中间又一次状态变化,将视图变为loading状态,一次数据请求成功,我们要做的是,重写viewController的render方法
1 render: function (data) { 2 //这里用户明确知道自己有没有viewdata 3 //var viewdata = this.adapter.getViewModel(); 4 var wrapperSet = { 5 loading: '.cui-error-tips', 6 ajaxSuc: '.cui-error-tips' 7 }; 8 //view具有唯一包裹器 9 var root = this.view.root; 10 var selector = wrapperSet[this.viewstatus]; 11 12 if (selector) { 13 root = root.find(selector); 14 } 15 16 this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 17 18 root.html(this.view.html); 19 20 },
这块逻辑需要被用户重写,因为具体每次渲染后,形成的html装载在什么位置,我们并不能确定
这里我们再写一个例子,看一看共享一个Adapter的效果
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>ToDoList</title> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css"> 8 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css"> 9 </head> 10 <body> 11 <article class="container"> 12 </article> 13 14 <div style=" border: 1px solid black; margin: 10px; padding: 10px; ">上下组件共享Adapter</div> 15 16 <article class="list"> 17 </article> 18 19 <script type="text/underscore-template" id="template-todolist"> 20 <section class="row"> 21 <div class="col-xs-9"> 22 <form action=""> 23 <legend>To Do List -- Input</legend> 24 <input type="text" placeholer="ToDoList" id="todoinput"> 25 <button class="btn btn-primary" data-action="add">添加</button> 26 </form> 27 <ul id="todolist"> 28 <%_.each(list, function(item){%> 29 <li><%=item.content %></li> 30 <%})%> 31 </ul> 32 </div> 33 </section> 34 </script> 35 36 <script type="text/underscore-template" id="template-list"> 37 <ul> 38 <%for(var i =0, len = list.length; i < len; i++) {%> 39 <li><%=list[i].content %> - <span index="<%=i %>">删除</span></li> 40 <%}%> 41 </ul> 42 </script> 43 44 <script type="text/javascript" src="../../vendor/underscore.js"></script> 45 <script type="text/javascript" src="../../vendor/zepto.js"></script> 46 <script src="../../src/underscore-extend.js" type="text/javascript"></script> 47 <script src="../../src/mvc.js" type="text/javascript"></script> 48 <script type="text/javascript"> 49 50 var View = _.inherit(Dalmatian.View, { 51 _initialize: function ($super) { 52 //设置默认属性 53 $super(); 54 this.templateSet = { 55 init: $('#template-todolist').html() 56 }; 57 } 58 }); 59 60 var Adapter = _.inherit(Dalmatian.Adapter, { 61 _initialize: function ($super) { 62 $super(); 63 this.datamodel = { 64 list: [{content: '测试数据01'}, {content: '测试数据02'}] 65 }; 66 }, 67 68 addItem: function (item) { 69 this.datamodel.list.push(item); 70 this.notifyDataChanged(); 71 }, 72 73 removeItem: function (index) { 74 this.datamodel.list.splice(index, 1); 75 this.notifyDataChanged(); 76 } 77 78 }); 79 80 var adapter = new Adapter(); 81 82 var Controller = _.inherit(Dalmatian.ViewController, { 83 _initialize: function ($super) { 84 $super(); 85 this.view = new View(); 86 this.adapter = adapter; 87 this.container = '.container'; 88 }, 89 90 render: function () { 91 this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 92 this.view.root.html(this.view.html); 93 }, 94 95 events: { 96 'click button': 'action' 97 }, 98 action: function (e) { 99 e.preventDefault(); 100 var target = $(e.currentTarget).attr('data-action'); 101 var strategy = { 102 'add': function (e) { 103 var value = $('#todoinput').val(); 104 this.adapter.addItem({ content: value }); 105 var s = ''; 106 } 107 } 108 strategy[target].apply(this, [e]); 109 } 110 }); 111 112 var controller = new Controller(); 113 controller.show(); 114 115 116 var LView = _.inherit(Dalmatian.View, { 117 _initialize: function ($super) { 118 //设置默认属性 119 $super(); 120 this.templateSet = { 121 init: $('#template-list').html() 122 }; 123 } 124 }); 125 126 var LController = _.inherit(Dalmatian.ViewController, { 127 _initialize: function ($super) { 128 $super(); 129 this.view = new LView(); 130 this.adapter = adapter; 131 this.container = '.list'; 132 }, 133 134 render: function () { 135 this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 136 this.view.root.html(this.view.html); 137 }, 138 139 events: { 140 'click span': 'action' 141 }, 142 action: function (e) { 143 var index = $(e.currentTarget).attr('index'); 144 this.adapter.removeItem(index); 145 146 } 147 }); 148 149 var lcontroller = new LController(); 150 lcontroller.show(); 151 152 </script> 153 </body> 154 </html>
可以看到,上下的变化根源是数据操作,每次数据的变化是共享的
结语
今天更正了上一次留下来的wrapperSet思维,这里对自己所知的MVC做了一次梳理
然后框架在一次次修改中逐步成型了,是个好现象,慢慢来吧