幾年前,我們這樣寫前端程式碼:
<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 });
1 define(['UIView', getAppUITemplatePath('ui.num'), getAppUICssPath('ui.num')], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 6 this.datamodel = { 7 min: 1, 8 max: 9, 9 curNum: 1, 10 unit: '', 11 needText: false 12 }; 13 14 this.template = template; 15 16 this.events = { 17 'click .js_num_minus': 'minusAction', 18 'click .js_num_plus': 'addAction', 19 'focus .js_cur_num': 'txtFocus', 20 'blur .js_cur_num': 'txtBlur' 21 }; 22 23 this.needRootWrapper = false; 24 25 }, 26 27 initElement: function () { 28 this.curNum = this.$('.js_cur_num'); 29 }, 30 31 txtFocus: function () { 32 this.curNum.html(''); 33 }, 34 35 txtBlur: function () { 36 this.setVal(this.curNum.html()); 37 }, 38 39 addAction: function () { 40 this.setVal(this.datamodel.curNum + 1); 41 }, 42 43 minusAction: function () { 44 this.setVal(this.datamodel.curNum - 1); 45 }, 46 47 //用於重寫 48 changed: function (num) { 49 console.log('num changed ' + num); 50 }, 51 52 getVal: function () { 53 return this.datamodel.curNum; 54 }, 55 56 setVal: function (v) { 57 var isChange = true; 58 var tmp = this.datamodel.curNum; 59 if (v === '') v = tmp; 60 if (v == parseInt(v)) { 61 //設定值不等的時候才觸發reset 62 v = parseInt(v); 63 this.datamodel.curNum = v; 64 if (v < this.datamodel.min) { 65 this.datamodel.curNum = this.datamodel.min; 66 } 67 if (v > this.datamodel.max) { 68 this.datamodel.curNum = this.datamodel.max; 69 } 70 this.curNum.val(this.datamodel.curNum); 71 isChange = (this.datamodel.curNum != tmp); 72 } 73 74 this.resetNum(isChange); 75 76 }, 77 78 //重置當前值,由於數值不滿足條件 79 resetNum: function (isChange) { 80 this.refresh(); 81 if (isChange) this.changed.call(this, this.datamodel.curNum); 82 }, 83 84 initialize: function ($super, opts) { 85 $super(opts); 86 }, 87 88 //這裡需要做資料驗證 89 resetPropery: function () { 90 if (this.datamodel.curNum > this.datamodel.max) { 91 this.datamodel.curNum = this.datamodel.max; 92 } else if (this.datamodel.curNum < this.datamodel.min) { 93 this.datamodel.curNum = this.datamodel.min; 94 } 95 }, 96 97 addEvent: function ($super) { 98 $super(); 99 } 100 101 }); 102 103 104 });
1 <div class="cm-num-adjust"> 2 <span class="cm-adjust-minus js_num_minus <% if(min == curNum) { %> disabled <% } %> "></span><span class="cm-adjust-view js_cur_num " <%if(needText == true){ %>contenteditable="true"<%} %>><%=curNum %><%=unit %></span> 3 <span class="cm-adjust-plus js_num_plus <% if(max == curNum) { %> disabled <% } %>"></span> 4 </div>
1 .cm-num-adjust { height: 33px; color: #099fde; background-color: #fff; display: inline-block; border-radius: 4px; } 2 .cm-num-adjust .cm-adjust-minus, .cm-num-adjust .cm-adjust-plus, .cm-num-adjust .cm-adjust-view { width: 33px; height: 33px; line-height: 31px; text-align: center; float: left; -webkit-box-sizing: border-box; box-sizing: border-box; } 3 .cm-num-adjust .cm-adjust-minus, .cm-num-adjust .cm-adjust-plus { cursor: pointer; border: 1px solid #099fde; } 4 .cm-num-adjust .cm-adjust-minus.disabled, .cm-num-adjust .cm-adjust-plus.disabled { cursor: default !important; background-color: #fff !important; border-color: #999 !important; } 5 .cm-num-adjust .cm-adjust-minus.disabled::before, .cm-num-adjust .cm-adjust-minus.disabled::after, .cm-num-adjust .cm-adjust-plus.disabled::before, .cm-num-adjust .cm-adjust-plus.disabled::after { background-color: #999 !important; } 6 .cm-num-adjust .cm-adjust-minus:active, .cm-num-adjust .cm-adjust-minus:hover, .cm-num-adjust .cm-adjust-plus:active, .cm-num-adjust .cm-adjust-plus:hover { background-color: #099fde; } 7 .cm-num-adjust .cm-adjust-minus:active::before, .cm-num-adjust .cm-adjust-minus:active::after, .cm-num-adjust .cm-adjust-minus:hover::before, .cm-num-adjust .cm-adjust-minus:hover::after, .cm-num-adjust .cm-adjust-plus:active::before, .cm-num-adjust .cm-adjust-plus:active::after, .cm-num-adjust .cm-adjust-plus:hover::before, .cm-num-adjust .cm-adjust-plus:hover::after { background-color: #fff; } 8 .cm-num-adjust .cm-adjust-minus { border-right: none; border-radius: 4px 0 0 4px; position: relative; } 9 .cm-num-adjust .cm-adjust-minus::before { content: ""; height: 2px; width: 16px; background-color: #099fde; position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); } 10 .cm-num-adjust .cm-adjust-minus + .cm-adjust-plus { border-left: 1px solid #099fde; } 11 .cm-num-adjust .cm-adjust-plus { border-left: none; border-radius: 0 4px 4px 0; position: relative; } 12 .cm-num-adjust .cm-adjust-plus::before, .cm-num-adjust .cm-adjust-plus::after { content: ""; width: 16px; height: 2px; background-color: #099fde; position: absolute; top: 50%; left: 50%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); } 13 .cm-num-adjust .cm-adjust-plus::after { width: 2px; height: 16px; } 14 .cm-num-adjust .cm-adjust-view { border: 1px solid #099fde; overflow: hidden; }
斷點一看,對應文字拿出來了:
因為這個特性是全元件共有的,我們將之做到統一的基類ui.abstract.view中即可:
1 /** 2 * @File ui.abstract.view.js 3 * @Description: UI元件基類 4 * @author l_wang@ctrip.com 5 * @date 2014-10-09 6 * @version V1.0 7 */ 8 9 /** 10 * UI元件基類,提供一個UI類基本功能,並可註冊各個事件點: 11 ① onPreCreate 在dom建立時觸發,只觸發一次 12 ② onCreate 在dom建立後觸發,只觸發一次 13 14 * @namespace UIView 15 */ 16 define([], function () { 17 18 /** 19 * @description 閉包儲存所有UI共用的資訊,這裡是z-index 20 * @method getBiggerzIndex 21 * @param {Number} level 22 * @returns {Number} 23 */ 24 var getBiggerzIndex = (function () { 25 var index = 3000; 26 return function (level) { 27 return level + (++index); 28 }; 29 })(); 30 31 return _.inherit({ 32 33 /** 34 * @description 設定例項預設屬性 35 * @method propertys 36 */ 37 propertys: function () { 38 //模板狀態 39 this.wrapper = $('body'); 40 this.id = _.uniqueId('ui-view-'); 41 42 this.template = ''; 43 44 //與模板對應的css檔案,預設不存在,需要各個元件複寫 45 this.uiStyle = null; 46 //儲存樣式格式化結束的字串 47 this.formateStyle = null; 48 49 this.datamodel = {}; 50 this.events = {}; 51 52 //自定義事件 53 //此處需要注意mask 繫結事件前後問題,考慮scroll.radio外掛型別的mask應用,考慮元件通訊 54 this.eventArr = {}; 55 56 //初始狀態為例項化 57 this.status = 'init'; 58 59 this.animateShowAction = null; 60 this.animateHideAction = null; 61 62 // this.availableFn = function () { } 63 64 }, 65 66 /** 67 * @description 繫結事件點回撥,這裡應該提供一個方法,表明是insert 或者 push,這樣有一定手段可以控制各個同一事件集合的執行順序 68 * @param {String} type 69 * @param {Function} fn 70 * @param {Boolean} insert 71 * @method on 72 */ 73 on: function (type, fn, insert) { 74 if (!this.eventArr[type]) this.eventArr[type] = []; 75 76 //頭部插入 77 if (insert) { 78 this.eventArr[type].splice(0, 0, fn); 79 } else { 80 this.eventArr[type].push(fn); 81 } 82 }, 83 84 /** 85 * @description 移除某一事件回撥點集合中的一項 86 * @param {String} type 87 * @param {Function} fn 88 * @method off 89 */ 90 off: function (type, fn) { 91 if (!this.eventArr[type]) return; 92 if (fn) { 93 this.eventArr[type] = _.without(this.eventArr[type], fn); 94 } else { 95 this.eventArr[type] = []; 96 } 97 }, 98 99 /** 100 * @description 觸發某一事件點集合回撥,按順序觸發 101 * @method 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 });
波及到的程式碼片段是:
1 createRoot: function (html) { 2 3 var style = this.createInlineStyle(); 4 if (style) { 5 this.formateStyle = '<style id="' + this.id + '_style">' + style + '</style>'; 6 html = this.formateStyle + html; 7 } 8 9 this.$el = $('<div class="view" style="display: none; " id="' + this.id + '"></div>'); 10 this.$el.html(html); 11 }, 12 13 //建立內嵌style相關 14 createInlineStyle: function () { 15 //如果不存在便不予理睬 16 if (!_.isString(this.uiStyle)) return null; 17 var style = '', uid = this.id; 18 19 //建立定製化的style字串,會模擬一個沙箱,該元件樣式不會對外影響,實現原理便是加上#id 字首 20 style = this.uiStyle.replace(/(\s*)([^\{\}]+)\{/g, function (a, b, c) { 21 return b + c.replace(/([^,]+)/g, '#' + uid + ' $1') + '{'; 22 }); 23 24 return style; 25 26 }, 27 28 refresh: function (needEvent) { 29 var html = ''; 30 this.resetPropery(); 31 if (needEvent) { 32 this.create(); 33 } else { 34 html = this.render(); 35 this.$el.html(this.formateStyle ? this.formateStyle + html : html); 36 } 37 this.initElement(); 38 if (this.status == 'show') this.show(); 39 this.trigger('onRefresh'); 40 },
這個時候對應ui.num.js只需要一點點變化即可:
1 define(['UIView', getAppUITemplatePath('ui.num'), getAppUICssPath('ui.num')], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 6 this.datamodel = { 7 min: 1, 8 max: 9, 9 curNum: 1, 10 unit: '', 11 needText: false 12 }; 13 14 this.template = template; 15 this.uiStyle = style; 16 17 this.events = { 18 'click .js_num_minus': 'minusAction', 19 'click .js_num_plus': 'addAction', 20 'focus .js_cur_num': 'txtFocus', 21 'blur .js_cur_num': 'txtBlur' 22 }; 23 24 this.needRootWrapper = false; 25 26 }, 27 28 initElement: function () { 29 this.curNum = this.$('.js_cur_num'); 30 }, 31 32 txtFocus: function () { 33 this.curNum.html(''); 34 }, 35 36 txtBlur: function () { 37 this.setVal(this.curNum.html()); 38 }, 39 40 addAction: function () { 41 this.setVal(this.datamodel.curNum + 1); 42 }, 43 44 minusAction: function () { 45 this.setVal(this.datamodel.curNum - 1); 46 }, 47 48 //用於重寫 49 changed: function (num) { 50 console.log('num changed ' + num); 51 }, 52 53 getVal: function () { 54 return this.datamodel.curNum; 55 }, 56 57 setVal: function (v) { 58 var isChange = true; 59 var tmp = this.datamodel.curNum; 60 if (v === '') v = tmp; 61 if (v == parseInt(v)) { 62 //設定值不等的時候才觸發reset 63 v = parseInt(v); 64 this.datamodel.curNum = v; 65 if (v < this.datamodel.min) { 66 this.datamodel.curNum = this.datamodel.min; 67 } 68 if (v > this.datamodel.max) { 69 this.datamodel.curNum = this.datamodel.max; 70 } 71 this.curNum.val(this.datamodel.curNum); 72 isChange = (this.datamodel.curNum != tmp); 73 } 74 75 this.resetNum(isChange); 76 77 }, 78 79 //重置當前值,由於數值不滿足條件 80 resetNum: function (isChange) { 81 this.refresh(); 82 if (isChange) this.changed.call(this, this.datamodel.curNum); 83 }, 84 85 initialize: function ($super, opts) { 86 $super(opts); 87 }, 88 89 //這裡需要做資料驗證 90 resetPropery: function () { 91 if (this.datamodel.curNum > this.datamodel.max) { 92 this.datamodel.curNum = this.datamodel.max; 93 } else if (this.datamodel.curNum < this.datamodel.min) { 94 this.datamodel.curNum = this.datamodel.min; 95 } 96 }, 97 98 addEvent: function ($super) { 99 $super(); 100 } 101 102 }); 103 104 105 });
1 define(['UIView', getAppUITemplatePath('ui.num'), getAppUICssPath('ui.num')], function (UIView, template, style) { 2 return _.inherit(UIView, { 3 propertys: function ($super) { 4 $super(); 5 //...... 6 7 this.template = template; 8 this.uiStyle = style; 9 10 //...... 11 } 12 13 //...... 14 }); 15 });
這個時候形成的dom結構變成了這個樣子:
如圖所示,對應的css被格式化為帶id的選擇器了,不會對外汙染,這個樣子解決了幾個問題:
① html、css、js統一歸UI管理,不存在釋出不同步的問題
② css也可以按需載入
③ 一定程度解決元件css汙染問題
④ 元件destroy時候樣式節點會被移除
但是也引起了一些新的問題:
① ui佔用節點增多,不destroy元件的情況下,是否會引起手機效能問題,對於webapp尤其重要
② 其中的css依然是UED分拆過來的,是否會引起更新不同步問題
③ html是不能跨域的,css是否會有同樣問題,未做實際驗證
④ css通用模組需要得到處理,防治重複程式碼
......
拋開以上問題不管,實現了相關功能的js鉤子保持一致的情況下,甚至可以以一個開關/版本號管理當前究竟顯示哪個樣式的元件,比如我們將html與css還原到以前:
到底使用V1版本或者標準版本,完全控制到requireJS的管理,這裡簡單依賴於這兩個方法的實現:
window.getAppUITemplatePath = function (path) { return 'text!' + app + 'ui/' + path + '.html'; } window.getAppUICssPath = function (path) { return 'text!' + app + 'ui/' + path + '.css'; }
我們可以簡單的在這裡定製開關,我們也可以在一個頁面裡面讓兩個元件同時出現,並且他們是同一個控制器,ver不同顯示的版本就不一樣:
1 //在此設定版本號,或者由url取出或者由伺服器取出... 2 var ver = 'v1'; 3 window.getAppUITemplatePath = function (path) { 4 return 'text!' + app + 'ui/' + path + (ver ? '_' + ver : '') + '.html'; 5 } 6 window.getAppUICssPath = function (path) { 7 return 'text!' + app + 'ui/' + path + (ver ? '_' + ver : '') + '.css'; 8 }
當然,也可以走更加合理的模組管理路線,我們這裡不做論述,這裡做一番總結,便結束今天的學習。
該問題的引出最初是由於釋出配合問題,結果上升了一下便成了效能優化問題,最後發現居然是解耦的問題,HTML、CSS、Javascript應該分離,但是業務應該在一塊,過度分離反而會引起開發效率問題,上面處理的方式,依舊是主動由UED將需要的CSS拿了回來,因為三者密不可分。
demo地址:http://yexiaochai.github.io/cssui/demo/debug.html#num
程式碼地址:https://github.com/yexiaochai/cssui/tree/gh-pages
文中有誤或者有不妥的地方請您提出