首页 > 代码库 > 【前端优化之拆分CSS】前端三剑客的分分合合
【前端优化之拆分CSS】前端三剑客的分分合合
几年前,我们这样写前端代码:
<div id="el" style="......" onclick="......">测试</div>
慢慢的,我们发现这样做的很多弊端,单就样式一块,改一个样式会涉及到多处调整,所以慢慢的dom标签中的css全部去了一个独立的css文件
再后来,交互变得异常复杂,onclick也不好使了,所以js也分离开了,经典的html+css+javascript结构分离逐步清晰,三种代码各司其职
HTML+CSS+Javascript体现着结构、表现、交互分离的思想,分离到极致后,css相关便完全由独立团队(UED)负责,会给出不包含javascript的“原型”demo
事有利弊,分离只是第一步,最终他们还是得合到一起,所以过度的拆分反而会有问题,最近工作中遇到了两个令人头疼的问题:
① 框架UI组件的CSS在UED处,一旦在线的UI出了样式问题,UED需要改动DOM结构和CSS的话,无论是框架还是UED先发布必定会导致生产样式问题(发布系统分离)
② H5站点会等依赖的CSS全部加载结束才能渲染页面。框架的css文件尺寸必定过100K,3G情况不稳定时要等很长时间,2G情况下5S秒以上更是家常便饭
PS:问题一是一个典型的发布依赖问题,本来与今天的内容不太相关,但是在讨论问题一的时候引出了问题二,解决问题二的时候又顺便解决了问题一,所以这里一并提出来,讲述了前端html、css、javascript的分分合合
做过全站前端优化的同学都会明白,优化做到最后,法宝往往都是减少请求,减低尺寸,所以缓存、轻量级框架在前端比较流行,但CSS却不容易被拆分,css业务分离还带来了重用性与发布依赖的问题,分离是问题产生的主要原因。而“分离”也是这里的优化手段:
① 分离:将全站的css“分离”到各个UI中② 合并:将分离的html、css、javascript重新“合并”
css非常容易引起变量“污染”,UI中的css应该最大程度的保证不影响业务css,并且不被影响,这一前提若是完全依赖与.css文件很难处理。
传说中web应用的未来:Web Components也提将HTML、CSS、JS封装到一起。其中比较令人惊讶的是不论js还是css会处于一沙箱中不会对外污染,学习web components的过程中意识到将css放到各自UI中的方案是可行的,也是上面问题的一种解决方案:
Web Components:组件相关html、css、js全部处于一个模块!
所以,似乎我应该将框架css分为两部分:① 核心通用css(10k作用)② 各部分UI样式
框架加载时候只需要加载10k的通用部分,或者常用UI;剩下的UI对应样式以及js文件便按需加载,并且UI的样式还不会互相影响,于是一个“奇怪”的做法出现了,以num组件为例
原来num组件包括两个文件:
① ui.num.js② ui.num.html
文件一为核心控制器,文件二为html实体,对应样式在全局css中,现在新增文件三:
① ui.num.js② ui.num.html③ ui.num.css
这个时候将全局css中对应的UI样式给抽出来了,放到了具体UI中,以实际代码为例我们数字组件变成了这个样子:
这里涉及到的文件有:
1 /** 2 * UI组件基类,提供一个UI类基本功能,并可注册各个事件点: 3 ① onPreCreate 在dom创建时触发,只触发一次 4 ② onCreate 在dom创建后触发,只触发一次 5 6 * @namespace UIView 7 */ 8 define([], function () { 9 10 /** 11 * @description 闭包保存所有UI共用的信息,这里是z-index 12 * @method getBiggerzIndex 13 * @param {Number} level 14 * @returns {Number} 15 */ 16 var getBiggerzIndex = (function () { 17 var index = 3000; 18 return function (level) { 19 return level + (++index); 20 }; 21 })(); 22 23 return _.inherit({ 24 25 /** 26 * @description 设置实例默认属性 27 * @method propertys 28 */ 29 propertys: function () { 30 //模板状态 31 this.wrapper = $(‘body‘); 32 this.id = _.uniqueId(‘ui-view-‘); 33 34 this.template = ‘‘; 35 this.datamodel = {}; 36 this.events = {}; 37 38 //自定义事件 39 //此处需要注意mask 绑定事件前后问题,考虑scroll.radio插件类型的mask应用,考虑组件通信 40 this.eventArr = {}; 41 42 //初始状态为实例化 43 this.status = ‘init‘; 44 45 this.animateShowAction = null; 46 this.animateHideAction = null; 47 48 // this.availableFn = function () { } 49 50 }, 51 52 /** 53 * @description 绑定事件点回调,这里应该提供一个方法,表明是insert 或者 push,这样有一定手段可以控制各个同一事件集合的执行顺序 54 * @param {String} type 55 * @param {Function} fn 56 * @param {Boolean} insert 57 * @method on 58 */ 59 on: function (type, fn, insert) { 60 if (!this.eventArr[type]) this.eventArr[type] = []; 61 62 //头部插入 63 if (insert) { 64 this.eventArr[type].splice(0, 0, fn); 65 } else { 66 this.eventArr[type].push(fn); 67 } 68 }, 69 70 /** 71 * @description 移除某一事件回调点集合中的一项 72 * @param {String} type 73 * @param {Function} fn 74 * @method off 75 */ 76 off: function (type, fn) { 77 if (!this.eventArr[type]) return; 78 if (fn) { 79 this.eventArr[type] = _.without(this.eventArr[type], fn); 80 } else { 81 this.eventArr[type] = []; 82 } 83 }, 84 85 /** 86 * @description 触发某一事件点集合回调,按顺序触发 87 * @method trigger 88 * @param {String} type 89 * @returns {Array} 90 */ 91 //PS:这里做的好点还可以参考js事件机制,冒泡捕获处于阶段 92 trigger: function (type) { 93 var _slice = Array.prototype.slice; 94 var args = _slice.call(arguments, 1); 95 var events = this.eventArr; 96 var results = [], i, l; 97 98 if (events[type]) { 99 for (i = 0, l = events[type].length; i < l; i++) {100 results[results.length] = events[type][i].apply(this, args);101 }102 }103 return results;104 },105 106 /**107 * @description 创建dom根元素,并组装形成UI Dom树108 * @override 这里可以重写该接口,比如有些场景不希望自己创建div为包裹层109 * @method createRoot110 * @param {String} html111 */112 createRoot: function (html) {113 this.$el = $(‘<div class="view" style="display: none; " id="‘ + this.id + ‘"></div>‘);114 this.$el.html(html);115 },116 117 _isAddEvent: function (key) {118 if (key == ‘onCreate‘ || key == ‘onPreShow‘ || key == ‘onShow‘ || key == ‘onRefresh‘ || key == ‘onHide‘)119 return true;120 return false;121 },122 123 /**124 * @description 设置参数,重写默认属性125 * @override 126 * @method setOption127 * @param {Object} options128 */129 setOption: function (options) {130 //这里可以写成switch,开始没有想到有这么多分支131 for (var k in options) {132 if (k == ‘datamodel‘ || k == ‘events‘) {133 _.extend(this[k], options[k]);134 continue;135 } else if (this._isAddEvent(k)) {136 this.on(k, options[k])137 continue;138 }139 this[k] = options[k];140 }141 // _.extend(this, options);142 },143 144 /**145 * @description 构造函数146 * @method initialize147 * @param {Object} opts148 */149 initialize: function (opts) {150 this.propertys();151 this.setOption(opts);152 this.resetPropery();153 //添加系统级别事件154 this.addEvent();155 //开始创建dom156 this.create();157 this.addSysEvents();158 159 this.initElement();160 161 },162 163 //内部重置event,加入全局控制类事件164 addSysEvents: function () {165 if (typeof this.availableFn != ‘function‘) return;166 this.removeSysEvents();167 this.$el.on(‘click.system‘ + this.id, $.proxy(function (e) {168 if (!this.availableFn()) {169 e.preventDefault();170 e.stopImmediatePropagation && e.stopImmediatePropagation();171 }172 }, this));173 },174 175 removeSysEvents: function () {176 this.$el.off(‘.system‘ + this.id);177 },178 179 $: function (selector) {180 return this.$el.find(selector);181 },182 183 //提供属性重置功能,对属性做检查184 resetPropery: function () {185 },186 187 //各事件注册点,用于被继承188 addEvent: function () {189 },190 191 create: function () {192 this.trigger(‘onPreCreate‘);193 this.createRoot(this.render());194 195 this.status = ‘create‘;196 this.trigger(‘onCreate‘);197 },198 199 //实例化需要用到到dom元素200 initElement: function () { },201 202 render: function (callback) {203 data = http://www.mamicode.com/this.getViewModel() || {};204 var html = this.template;205 if (!this.template) return ‘‘;206 if (data) {207 html = _.template(this.template)(data);208 }209 typeof callback == ‘function‘ && callback.call(this);210 return html;211 },212 213 //刷新根据传入参数判断是否走onCreate事件214 //这里原来的dom会被移除,事件会全部丢失 需要修复*****************************215 refresh: function (needEvent) {216 this.resetPropery();217 if (needEvent) {218 this.create();219 } else {220 this.$el.html(this.render());221 }222 this.initElement();223 if (this.status == ‘show‘) this.show();224 this.trigger(‘onRefresh‘);225 },226 227 show: function () {228 if (!this.wrapper[0] || !this.$el[0]) return;229 //如果包含就不要乱搞了230 if (!$.contains(this.wrapper[0], this.$el[0])) {231 this.wrapper.append(this.$el);232 }233 234 this.trigger(‘onPreShow‘);235 236 if (typeof this.animateShowAction == ‘function‘)237 this.animateShowAction.call(this, this.$el);238 else239 this.$el.show();240 241 this.status = ‘show‘;242 this.bindEvents();243 this.trigger(‘onShow‘);244 },245 246 hide: function () {247 if (!this.$el || this.status !== ‘show‘) return;248 249 this.trigger(‘onPreHide‘);250 251 if (typeof this.animateHideAction == ‘function‘)252 this.animateHideAction.call(this, this.$el);253 else254 this.$el.hide();255 256 this.status = ‘hide‘;257 this.unBindEvents();258 this.removeSysEvents();259 this.trigger(‘onHide‘);260 },261 262 destroy: function () {263 this.status = ‘destroy‘;264 this.unBindEvents();265 this.removeSysEvents();266 this.$el.remove();267 this.trigger(‘onDestroy‘);268 delete this;269 },270 271 getViewModel: function () {272 return this.datamodel;273 },274 275 setzIndexTop: function (el, level) {276 if (!el) el = this.$el;277 if (!level || level > 10) level = 0;278 level = level * 1000;279 el.css(‘z-index‘, getBiggerzIndex(level));280 281 },282 283 /**284 * 解析events,根据events的设置在dom上设置事件285 */286 bindEvents: function () {287 var events = this.events;288 289 if (!(events || (events = _.result(this, ‘events‘)))) return this;290 this.unBindEvents();291 292 // 解析event参数的正则293 var delegateEventSplitter = /^(\S+)\s*(.*)$/;294 var key, method, match, eventName, selector;295 296 // 做简单的字符串数据解析297 for (key in events) {298 method = events[key];299 if (!_.isFunction(method)) method = this[events[key]];300 if (!method) continue;301 302 match = key.match(delegateEventSplitter);303 eventName = match[1], selector = match[2];304 method = _.bind(method, this);305 eventName += ‘.delegateUIEvents‘ + this.id;306 307 if (selector === ‘‘) {308 this.$el.on(eventName, method);309 } else {310 this.$el.on(eventName, selector, method);311 }312 }313 314 return this;315 },316 317 /**318 * 冻结dom上所有元素的所有事件319 *320 * @return {object} 执行作用域321 */322 unBindEvents: function () {323 this.$el.off(‘.delegateUIEvents‘ + this.id);324 return this;325 }326 327 });328 329 });
1 define([‘UIView‘, getAppUITemplatePath(‘ui.num‘), getAppUICssPath(‘ui.num‘)], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 6 this.datamodel = { 7 min: 1, 8 max: 9, 9 curNum: 1, 10 unit: ‘‘, 11 needText: false 12 }; 13 14 this.template = template; 15 16 this.events = { 17 ‘click .js_num_minus‘: ‘minusAction‘, 18 ‘click .js_num_plus‘: ‘addAction‘, 19 ‘focus .js_cur_num‘: ‘txtFocus‘, 20 ‘blur .js_cur_num‘: ‘txtBlur‘ 21 }; 22 23 this.needRootWrapper = false; 24 25 }, 26 27 initElement: function () { 28 this.curNum = this.$(‘.js_cur_num‘); 29 }, 30 31 txtFocus: function () { 32 this.curNum.html(‘‘); 33 }, 34 35 txtBlur: function () { 36 this.setVal(this.curNum.html()); 37 }, 38 39 addAction: function () { 40 this.setVal(this.datamodel.curNum + 1); 41 }, 42 43 minusAction: function () { 44 this.setVal(this.datamodel.curNum - 1); 45 }, 46 47 //用于重写 48 changed: function (num) { 49 console.log(‘num changed ‘ + num); 50 }, 51 52 getVal: function () { 53 return this.datamodel.curNum; 54 }, 55 56 setVal: function (v) { 57 var isChange = true; 58 var tmp = this.datamodel.curNum; 59 if (v === ‘‘) v = tmp; 60 if (v == parseInt(v)) { 61 //设置值不等的时候才触发reset 62 v = parseInt(v); 63 this.datamodel.curNum = v; 64 if (v < this.datamodel.min) { 65 this.datamodel.curNum = this.datamodel.min; 66 } 67 if (v > this.datamodel.max) { 68 this.datamodel.curNum = this.datamodel.max; 69 } 70 this.curNum.val(this.datamodel.curNum); 71 isChange = (this.datamodel.curNum != tmp); 72 } 73 74 this.resetNum(isChange); 75 76 }, 77 78 //重置当前值,由于数值不满足条件 79 resetNum: function (isChange) { 80 this.refresh(); 81 if (isChange) this.changed.call(this, this.datamodel.curNum); 82 }, 83 84 initialize: function ($super, opts) { 85 $super(opts); 86 }, 87 88 //这里需要做数据验证 89 resetPropery: function () { 90 if (this.datamodel.curNum > this.datamodel.max) { 91 this.datamodel.curNum = this.datamodel.max; 92 } else if (this.datamodel.curNum < this.datamodel.min) { 93 this.datamodel.curNum = this.datamodel.min; 94 } 95 }, 96 97 addEvent: function ($super) { 98 $super(); 99 }100 101 });102 103 104 });
1 <div class="cm-num-adjust">2 <span class="cm-adjust-minus js_num_minus <% if(min == curNum) { %> disabled <% } %> "></span><span class="cm-adjust-view js_cur_num " <%if(needText == true){ %>contenteditable="true"<%} %>><%=curNum %><%=unit %></span>3 <span class="cm-adjust-plus js_num_plus <% if(max == curNum) { %> disabled <% } %>"></span>4 </div>
1 .cm-num-adjust { height: 33px; color: #099fde; background-color: #fff; display: inline-block; border-radius: 4px; } 2 .cm-num-adjust .cm-adjust-minus, .cm-num-adjust .cm-adjust-plus, .cm-num-adjust .cm-adjust-view { width: 33px; height: 33px; line-height: 31px; text-align: center; float: left; -webkit-box-sizing: border-box; box-sizing: border-box; } 3 .cm-num-adjust .cm-adjust-minus, .cm-num-adjust .cm-adjust-plus { cursor: pointer; border: 1px solid #099fde; } 4 .cm-num-adjust .cm-adjust-minus.disabled, .cm-num-adjust .cm-adjust-plus.disabled { cursor: default !important; background-color: #fff !important; border-color: #999 !important; } 5 .cm-num-adjust .cm-adjust-minus.disabled::before, .cm-num-adjust .cm-adjust-minus.disabled::after, .cm-num-adjust .cm-adjust-plus.disabled::before, .cm-num-adjust .cm-adjust-plus.disabled::after { background-color: #999 !important; } 6 .cm-num-adjust .cm-adjust-minus:active, .cm-num-adjust .cm-adjust-minus:hover, .cm-num-adjust .cm-adjust-plus:active, .cm-num-adjust .cm-adjust-plus:hover { background-color: #099fde; } 7 .cm-num-adjust .cm-adjust-minus:active::before, .cm-num-adjust .cm-adjust-minus:active::after, .cm-num-adjust .cm-adjust-minus:hover::before, .cm-num-adjust .cm-adjust-minus:hover::after, .cm-num-adjust .cm-adjust-plus:active::before, .cm-num-adjust .cm-adjust-plus:active::after, .cm-num-adjust .cm-adjust-plus:hover::before, .cm-num-adjust .cm-adjust-plus:hover::after { background-color: #fff; } 8 .cm-num-adjust .cm-adjust-minus { border-right: none; border-radius: 4px 0 0 4px; position: relative; } 9 .cm-num-adjust .cm-adjust-minus::before { content: ""; height: 2px; width: 16px; background-color: #099fde; position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); }10 .cm-num-adjust .cm-adjust-minus + .cm-adjust-plus { border-left: 1px solid #099fde; }11 .cm-num-adjust .cm-adjust-plus { border-left: none; border-radius: 0 4px 4px 0; position: relative; }12 .cm-num-adjust .cm-adjust-plus::before, .cm-num-adjust .cm-adjust-plus::after { content: ""; width: 16px; height: 2px; background-color: #099fde; position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); }13 .cm-num-adjust .cm-adjust-plus::after { width: 2px; height: 16px; }14 .cm-num-adjust .cm-adjust-view { border: 1px solid #099fde; overflow: hidden; }
断点一看,对应文本拿出来了:
因为这个特性是全组件共有的,我们将之做到统一的基类ui.abstract.view中即可:
1 /** 2 * @File ui.abstract.view.js 3 * @Description: UI组件基类 4 * @author l_wang@ctrip.com 5 * @date 2014-10-09 6 * @version V1.0 7 */ 8 9 /** 10 * UI组件基类,提供一个UI类基本功能,并可注册各个事件点: 11 ① onPreCreate 在dom创建时触发,只触发一次 12 ② onCreate 在dom创建后触发,只触发一次 13 14 * @namespace UIView 15 */ 16 define([], function () { 17 18 /** 19 * @description 闭包保存所有UI共用的信息,这里是z-index 20 * @method getBiggerzIndex 21 * @param {Number} level 22 * @returns {Number} 23 */ 24 var getBiggerzIndex = (function () { 25 var index = 3000; 26 return function (level) { 27 return level + (++index); 28 }; 29 })(); 30 31 return _.inherit({ 32 33 /** 34 * @description 设置实例默认属性 35 * @method propertys 36 */ 37 propertys: function () { 38 //模板状态 39 this.wrapper = $(‘body‘); 40 this.id = _.uniqueId(‘ui-view-‘); 41 42 this.template = ‘‘; 43 44 //与模板对应的css文件,默认不存在,需要各个组件复写 45 this.uiStyle = null; 46 //保存样式格式化结束的字符串 47 this.formateStyle = null; 48 49 this.datamodel = {}; 50 this.events = {}; 51 52 //自定义事件 53 //此处需要注意mask 绑定事件前后问题,考虑scroll.radio插件类型的mask应用,考虑组件通信 54 this.eventArr = {}; 55 56 //初始状态为实例化 57 this.status = ‘init‘; 58 59 this.animateShowAction = null; 60 this.animateHideAction = null; 61 62 // this.availableFn = function () { } 63 64 }, 65 66 /** 67 * @description 绑定事件点回调,这里应该提供一个方法,表明是insert 或者 push,这样有一定手段可以控制各个同一事件集合的执行顺序 68 * @param {String} type 69 * @param {Function} fn 70 * @param {Boolean} insert 71 * @method on 72 */ 73 on: function (type, fn, insert) { 74 if (!this.eventArr[type]) this.eventArr[type] = []; 75 76 //头部插入 77 if (insert) { 78 this.eventArr[type].splice(0, 0, fn); 79 } else { 80 this.eventArr[type].push(fn); 81 } 82 }, 83 84 /** 85 * @description 移除某一事件回调点集合中的一项 86 * @param {String} type 87 * @param {Function} fn 88 * @method off 89 */ 90 off: function (type, fn) { 91 if (!this.eventArr[type]) return; 92 if (fn) { 93 this.eventArr[type] = _.without(this.eventArr[type], fn); 94 } else { 95 this.eventArr[type] = []; 96 } 97 }, 98 99 /**100 * @description 触发某一事件点集合回调,按顺序触发101 * @method trigger102 * @param {String} type103 * @returns {Array}104 */105 //PS:这里做的好点还可以参考js事件机制,冒泡捕获处于阶段106 trigger: function (type) {107 var _slice = Array.prototype.slice;108 var args = _slice.call(arguments, 1);109 var events = this.eventArr;110 var results = [], i, l;111 112 if (events[type]) {113 for (i = 0, l = events[type].length; i < l; i++) {114 results[results.length] = events[type][i].apply(this, args);115 }116 }117 return results;118 },119 120 /**121 * @description 创建dom根元素,并组装形成UI Dom树122 * @override 这里可以重写该接口,比如有些场景不希望自己创建div为包裹层123 * @method createRoot124 * @param {String} html125 */126 createRoot: function (html) {127 128 var style = this.createInlineStyle();129 if (style) {130 this.formateStyle = ‘<style id="‘ + this.id + ‘_style">‘ + style + ‘</style>‘;131 html = this.formateStyle + html;132 }133 134 this.$el = $(‘<div class="view" style="display: none; " id="‘ + this.id + ‘"></div>‘);135 this.$el.html(html);136 },137 138 //创建内嵌style相关139 createInlineStyle: function () {140 //如果不存在便不予理睬141 if (!_.isString(this.uiStyle)) return null;142 var style = ‘‘, uid = this.id;143 144 //创建定制化的style字符串,会模拟一个沙箱,该组件样式不会对外影响,实现原理便是加上#id 前缀145 style = this.uiStyle.replace(/(\s*)([^\{\}]+)\{/g, function (a, b, c) {146 return b + c.replace(/([^,]+)/g, ‘#‘ + uid + ‘ $1‘) + ‘{‘;147 });148 149 return style;150 151 },152 153 _isAddEvent: function (key) {154 if (key == ‘onCreate‘ || key == ‘onPreShow‘ || key == ‘onShow‘ || key == ‘onRefresh‘ || key == ‘onHide‘)155 return true;156 return false;157 },158 159 /**160 * @description 设置参数,重写默认属性161 * @override 162 * @method setOption163 * @param {Object} options164 */165 setOption: function (options) {166 //这里可以写成switch,开始没有想到有这么多分支167 for (var k in options) {168 if (k == ‘datamodel‘ || k == ‘events‘) {169 _.extend(this[k], options[k]);170 continue;171 } else if (this._isAddEvent(k)) {172 this.on(k, options[k])173 continue;174 }175 this[k] = options[k];176 }177 // _.extend(this, options);178 },179 180 /**181 * @description 构造函数182 * @method initialize183 * @param {Object} opts184 */185 initialize: function (opts) {186 this.propertys();187 this.setOption(opts);188 this.resetPropery();189 //添加系统级别事件190 this.addEvent();191 //开始创建dom192 this.create();193 this.addSysEvents();194 195 this.initElement();196 197 },198 199 //内部重置event,加入全局控制类事件200 addSysEvents: function () {201 if (typeof this.availableFn != ‘function‘) return;202 this.removeSysEvents();203 this.$el.on(‘click.system‘ + this.id, $.proxy(function (e) {204 if (!this.availableFn()) {205 e.preventDefault();206 e.stopImmediatePropagation && e.stopImmediatePropagation();207 }208 }, this));209 },210 211 removeSysEvents: function () {212 this.$el.off(‘.system‘ + this.id);213 },214 215 $: function (selector) {216 return this.$el.find(selector);217 },218 219 //提供属性重置功能,对属性做检查220 resetPropery: function () {221 },222 223 //各事件注册点,用于被继承224 addEvent: function () {225 },226 227 create: function () {228 this.trigger(‘onPreCreate‘);229 this.createRoot(this.render());230 231 this.status = ‘create‘;232 this.trigger(‘onCreate‘);233 },234 235 //实例化需要用到到dom元素236 initElement: function () { },237 238 render: function (callback) {239 data = http://www.mamicode.com/this.getViewModel() || {};240 var html = this.template;241 if (!this.template) return ‘‘;242 if (data) {243 html = _.template(this.template)(data);244 }245 typeof callback == ‘function‘ && callback.call(this);246 return html;247 },248 249 //刷新根据传入参数判断是否走onCreate事件250 //这里原来的dom会被移除,事件会全部丢失 需要修复*****************************251 refresh: function (needEvent) {252 var html = ‘‘;253 this.resetPropery();254 if (needEvent) {255 this.create();256 } else {257 html = this.render();258 this.$el.html(this.formateStyle ? this.formateStyle + html : html);259 }260 this.initElement();261 if (this.status == ‘show‘) this.show();262 this.trigger(‘onRefresh‘);263 },264 265 show: function () {266 if (!this.wrapper[0] || !this.$el[0]) return;267 //如果包含就不要乱搞了268 if (!$.contains(this.wrapper[0], this.$el[0])) {269 this.wrapper.append(this.$el);270 }271 272 this.trigger(‘onPreShow‘);273 274 if (typeof this.animateShowAction == ‘function‘)275 this.animateShowAction.call(this, this.$el);276 else277 this.$el.show();278 279 this.status = ‘show‘;280 this.bindEvents();281 this.trigger(‘onShow‘);282 },283 284 hide: function () {285 if (!this.$el || this.status !== ‘show‘) return;286 287 this.trigger(‘onPreHide‘);288 289 if (typeof this.animateHideAction == ‘function‘)290 this.animateHideAction.call(this, this.$el);291 else292 this.$el.hide();293 294 this.status = ‘hide‘;295 this.unBindEvents();296 this.removeSysEvents();297 this.trigger(‘onHide‘);298 },299 300 destroy: function () {301 this.status = ‘destroy‘;302 this.unBindEvents();303 this.removeSysEvents();304 this.$el.remove();305 this.trigger(‘onDestroy‘);306 delete this;307 },308 309 getViewModel: function () {310 return this.datamodel;311 },312 313 setzIndexTop: function (el, level) {314 if (!el) el = this.$el;315 if (!level || level > 10) level = 0;316 level = level * 1000;317 el.css(‘z-index‘, getBiggerzIndex(level));318 319 },320 321 /**322 * 解析events,根据events的设置在dom上设置事件323 */324 bindEvents: function () {325 var events = this.events;326 327 if (!(events || (events = _.result(this, ‘events‘)))) return this;328 this.unBindEvents();329 330 // 解析event参数的正则331 var delegateEventSplitter = /^(\S+)\s*(.*)$/;332 var key, method, match, eventName, selector;333 334 // 做简单的字符串数据解析335 for (key in events) {336 method = events[key];337 if (!_.isFunction(method)) method = this[events[key]];338 if (!method) continue;339 340 match = key.match(delegateEventSplitter);341 eventName = match[1], selector = match[2];342 method = _.bind(method, this);343 eventName += ‘.delegateUIEvents‘ + this.id;344 345 if (selector === ‘‘) {346 this.$el.on(eventName, method);347 } else {348 this.$el.on(eventName, selector, method);349 }350 }351 352 return this;353 },354 355 /**356 * 冻结dom上所有元素的所有事件357 *358 * @return {object} 执行作用域359 */360 unBindEvents: function () {361 this.$el.off(‘.delegateUIEvents‘ + this.id);362 return this;363 }364 365 });366 367 });
波及到的代码片段是:
1 createRoot: function (html) { 2 3 var style = this.createInlineStyle(); 4 if (style) { 5 this.formateStyle = ‘<style id="‘ + this.id + ‘_style">‘ + style + ‘</style>‘; 6 html = this.formateStyle + html; 7 } 8 9 this.$el = $(‘<div class="view" style="display: none; " id="‘ + this.id + ‘"></div>‘);10 this.$el.html(html);11 },12 13 //创建内嵌style相关14 createInlineStyle: function () {15 //如果不存在便不予理睬16 if (!_.isString(this.uiStyle)) return null;17 var style = ‘‘, uid = this.id;18 19 //创建定制化的style字符串,会模拟一个沙箱,该组件样式不会对外影响,实现原理便是加上#id 前缀20 style = this.uiStyle.replace(/(\s*)([^\{\}]+)\{/g, function (a, b, c) {21 return b + c.replace(/([^,]+)/g, ‘#‘ + uid + ‘ $1‘) + ‘{‘;22 });23 24 return style;25 26 },27 28 refresh: function (needEvent) {29 var html = ‘‘;30 this.resetPropery();31 if (needEvent) {32 this.create();33 } else {34 html = this.render();35 this.$el.html(this.formateStyle ? this.formateStyle + html : html);36 }37 this.initElement();38 if (this.status == ‘show‘) this.show();39 this.trigger(‘onRefresh‘);40 },
这个时候对应ui.num.js只需要一点点变化即可:
1 define([‘UIView‘, getAppUITemplatePath(‘ui.num‘), getAppUICssPath(‘ui.num‘)], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 6 this.datamodel = { 7 min: 1, 8 max: 9, 9 curNum: 1, 10 unit: ‘‘, 11 needText: false 12 }; 13 14 this.template = template; 15 this.uiStyle = style; 16 17 this.events = { 18 ‘click .js_num_minus‘: ‘minusAction‘, 19 ‘click .js_num_plus‘: ‘addAction‘, 20 ‘focus .js_cur_num‘: ‘txtFocus‘, 21 ‘blur .js_cur_num‘: ‘txtBlur‘ 22 }; 23 24 this.needRootWrapper = false; 25 26 }, 27 28 initElement: function () { 29 this.curNum = this.$(‘.js_cur_num‘); 30 }, 31 32 txtFocus: function () { 33 this.curNum.html(‘‘); 34 }, 35 36 txtBlur: function () { 37 this.setVal(this.curNum.html()); 38 }, 39 40 addAction: function () { 41 this.setVal(this.datamodel.curNum + 1); 42 }, 43 44 minusAction: function () { 45 this.setVal(this.datamodel.curNum - 1); 46 }, 47 48 //用于重写 49 changed: function (num) { 50 console.log(‘num changed ‘ + num); 51 }, 52 53 getVal: function () { 54 return this.datamodel.curNum; 55 }, 56 57 setVal: function (v) { 58 var isChange = true; 59 var tmp = this.datamodel.curNum; 60 if (v === ‘‘) v = tmp; 61 if (v == parseInt(v)) { 62 //设置值不等的时候才触发reset 63 v = parseInt(v); 64 this.datamodel.curNum = v; 65 if (v < this.datamodel.min) { 66 this.datamodel.curNum = this.datamodel.min; 67 } 68 if (v > this.datamodel.max) { 69 this.datamodel.curNum = this.datamodel.max; 70 } 71 this.curNum.val(this.datamodel.curNum); 72 isChange = (this.datamodel.curNum != tmp); 73 } 74 75 this.resetNum(isChange); 76 77 }, 78 79 //重置当前值,由于数值不满足条件 80 resetNum: function (isChange) { 81 this.refresh(); 82 if (isChange) this.changed.call(this, this.datamodel.curNum); 83 }, 84 85 initialize: function ($super, opts) { 86 $super(opts); 87 }, 88 89 //这里需要做数据验证 90 resetPropery: function () { 91 if (this.datamodel.curNum > this.datamodel.max) { 92 this.datamodel.curNum = this.datamodel.max; 93 } else if (this.datamodel.curNum < this.datamodel.min) { 94 this.datamodel.curNum = this.datamodel.min; 95 } 96 }, 97 98 addEvent: function ($super) { 99 $super();100 }101 102 });103 104 105 });
1 define([‘UIView‘, getAppUITemplatePath(‘ui.num‘), getAppUICssPath(‘ui.num‘)], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 //...... 6 7 this.template = template; 8 this.uiStyle = style; 9 10 //......11 }12 13 //......14 });15 });
这个时候形成的dom结构变成了这个样子:
如图所示,对应的css被格式化为带id的选择器了,不会对外污染,这个样子解决了几个问题:
① html、css、js统一归UI管理,不存在发布不同步的问题
② css也可以按需加载
③ 一定程度解决组件css污染问题
④ 组件destroy时候样式节点会被移除
但是也引起了一些新的问题:
① ui占用节点增多,不destroy组件的情况下,是否会引起手机性能问题,对于webapp尤其重要
② 其中的css依然是UED分拆过来的,是否会引起更新不同步问题
③ html是不能跨域的,css是否会有同样问题,未做实际验证
④ css通用模块需要得到处理,防治重复代码
......
抛开以上问题不管,实现了相关功能的js钩子保持一致的情况下,甚至可以以一个开关/版本号管理当前究竟显示哪个样式的组件,比如我们将html与css还原到以前:
到底使用V1版本或者标准版本,完全控制到requireJS的管理,这里简单依赖于这两个方法的实现:
window.getAppUITemplatePath = function (path) { return ‘text!‘ + app + ‘ui/‘ + path + ‘.html‘;}window.getAppUICssPath = function (path) { return ‘text!‘ + app + ‘ui/‘ + path + ‘.css‘;}
我们可以简单的在这里定制开关,我们也可以在一个页面里面让两个组件同时出现,并且他们是同一个控制器,ver不同显示的版本就不一样:
1 //在此设置版本号,或者由url取出或者由服务器取出...2 var ver = ‘v1‘;3 window.getAppUITemplatePath = function (path) {4 return ‘text!‘ + app + ‘ui/‘ + path + (ver ? ‘_‘ + ver : ‘‘) + ‘.html‘;5 }6 window.getAppUICssPath = function (path) {7 return ‘text!‘ + app + ‘ui/‘ + path + (ver ? ‘_‘ + ver : ‘‘) + ‘.css‘;8 }
当然,也可以走更加合理的模块管理路线,我们这里不做论述,这里做一番总结,便结束今天的学习。
该问题的引出最初是由于发布配合问题,结果上升了一下便成了性能优化问题,最后发现居然是解耦的问题,HTML、CSS、Javascript应该分离,但是业务应该在一块,过度分离反而会引起开发效率问题,上面处理的方式,依旧是主动由UED将需要的CSS拿了回来,因为三者密不可分。
demo地址:http://yexiaochai.github.io/cssui/demo/debug.html#num
代码地址:https://github.com/yexiaochai/cssui/tree/gh-pages
文中有误或者有不妥的地方请您提出
【前端优化之拆分CSS】前端三剑客的分分合合