【前端優化之拆分CSS】前端三劍客的分分合合

幾年前,咱們這樣寫前端代碼:javascript

<div id="el" style="......" onclick="......">測試</div>

慢慢的,咱們發現這樣作的不少弊端,單就樣式一塊,改一個樣式會涉及到多處調整,因此慢慢的dom標籤中的css所有去了一個獨立的css文件css

再後來,交互變得異常複雜,onclick也很差使了,因此js也分離開了,經典的html+css+javascript結構分離逐步清晰,三種代碼各司其職html

HTML+CSS+Javascript體現着結構、表現、交互分離的思想,分離到極致後,css相關便徹底由獨立團隊(UED)負責,會給出不包含javascript的「原型」demo前端

事有利弊,分離只是第一步,最終他們仍是得合到一塊兒,因此過分的拆分反而會有問題,最近工做中遇到了兩個使人頭疼的問題:java

① 框架UI組件的CSS在UED處,一旦在線的UI出了樣式問題,UED須要改動DOM結構和CSS的話,不管是框架仍是UED先發布一定會致使生產樣式問題(發佈系統分離)git

② H5站點會等依賴的CSS所有加載結束才能渲染頁面。框架的css文件尺寸一定過100K,3G狀況不穩定時要等很長時間,2G狀況下5S秒以上更是屢見不鮮github

PS:問題一是一個典型的發佈依賴問題,原本與今天的內容不太相關,可是在討論問題一的時候引出了問題二,解決問題二的時候又順便解決了問題一,因此這裏一併提出來,講述了前端html、css、javascript的分分合合web

作過全站前端優化的同窗都會明白,優化作到最後,法寶每每都是減小請求,減低尺寸,因此緩存、輕量級框架在前端比較流行,但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

文中有誤或者有不妥的地方請您提出

相關文章
相關標籤/搜索