首页 > 代码库 > 【前端优化之拆分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 });
ui.abstract.view
  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 });
ui.num.js
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>
ui.num.html
 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.num.css

断点一看,对应文本拿出来了:

因为这个特性是全组件共有的,我们将之做到统一的基类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 });
View Code

波及到的代码片段是:

 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 });
View Code
 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】前端三剑客的分分合合