zoukankan      html  css  js  c++  java
  • 【前端优化之拆分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 createRoot
    110     * @param {String} html
    111     */
    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 setOption
    127     * @param {Object} options
    128     */
    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 initialize
    147     * @param {Object} opts
    148     */
    149     initialize: function (opts) {
    150       this.propertys();
    151       this.setOption(opts);
    152       this.resetPropery();
    153       //添加系统级别事件
    154       this.addEvent();
    155       //开始创建dom
    156       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 = 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       else
    239         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       else
    254         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 trigger
    102     * @param {String} type
    103     * @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 createRoot
    124     * @param {String} html
    125     */
    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 setOption
    163     * @param {Object} options
    164     */
    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 initialize
    183     * @param {Object} opts
    184     */
    185     initialize: function (opts) {
    186       this.propertys();
    187       this.setOption(opts);
    188       this.resetPropery();
    189       //添加系统级别事件
    190       this.addEvent();
    191       //开始创建dom
    192       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 = 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       else
    277         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       else
    292         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

    文中有误或者有不妥的地方请您提出

  • 相关阅读:
    去除Html标签
    asp.net弹出多个模态窗口
    window.returnValue的用法
    eTerm-用于报价的指令(GK状态码的使用)
    使用ffmpeg 操作音频文件前后部分静音移除.
    使用Visual Studio 2017开发python,并在iis上部署Python Django
    解决wampserver 服务无法启动
    网站优化记录-通过命令预编译Asp.net 网站,成功优化到毫秒级别。
    Scut游戏引擎改造兼容Codis。
    windows修改Host后未生效。
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/4165386.html
Copyright © 2011-2022 走看看