通過昨天的優化處理(【前端優化之拆分CSS】前端三劍客的分分合合),咱們在UI一塊作了幾個關鍵動做:javascript
① CSS入UIcss
② CSS做爲組件的一個節點而存在,而且會被「格式化」,即選擇器帶id前綴,造成的組件如圖所示:html
這樣作基本能夠規避css污染的問題,解決絕大多數問題,可是更優的方案老是存在,好比web components中的shadow dom!前端
javascript的組件基本是不可重用的,幾個核心緣由是:java
① 組件實例與實例之間的html、css、Javascript很容易互相污染(id污染、class污染、js變量污染......)
② 一個組件依賴於HTML、CSS、Javascript,而三者之間是分離的,而組件內部控制於js,更改後外部可能出問題
經過昨天的處理,咱們將一個組件所用到的所有合到了一塊兒,卻又分離成了三個文件:git
① ui.js
② ui.html
③ ui.css
這種處理一方面透露着解耦的思想,另外一方面體現着解依賴的想法,在這個基礎上想引入shadow dom技術,變得很是輕易。github
shadow dom是一種瀏覽器行爲,他容許在document文檔中渲染時插入一個獨立的dom子樹,但這個dom樹與主dom樹徹底分離的,不會互相影響。
從一張圖來看:web
shadow dom事實上也是一個文檔碎片,咱們甚至能夠將之做爲jQuery包裝對象處理:瀏覽器
存在在shadow dom中的元素是不可被選擇器找到的,好比這種作法會徒勞無功:app
$('沙箱中的一個元素') => []
另外一個比較重要的差異是,外部爲組件定義的事件,好比click事件的e.target便只能是組件div了,也就是這個組件事實上只有一層,一個標籤,內部的結構不會被暴露!
原來咱們的組件是這樣的結構:
1 <div id="ui-view-16" style=""> 2 <div class="cm-num-adjust"> 3 <div class="cm-num-adjust"> 4 <span class="cm-adjust-minus js_num_minus disabled "></span><span class="cm-adjust-view js_cur_num " 5 contenteditable="true">1個</span> <span class="cm-adjust-plus js_num_plus "></span> 6 </div> 7 </div> 8 </div>
框架會主動建立一個包裹層,包裹層內纔是組件dom,通過昨天的處理,組件變成了這樣:
1 <!--組件生成的包裹層--> 2 <div id="wrapper" > 3 <!--組件格式化後的樣式--> 4 <style> 5 #wrapper { ......} 6 </style> 7 8 <!--組件真實的dom結構--> 9 <div></div> 10 <div>
若是這裏咱們使用shadow dom技術的話,整個結構會變成這樣:
1 <div id="wrapper"> 2 #shadow-root 3 <style></style> 4 <div> 5 </div> 6 <div>
組件自動建立的dom包裹層,裏面神馬都沒有了,由於事件代理是進不去的,因此開啓shadow dom方式的組件須要將事件綁定至shadow節點
固然,並非全部瀏覽器都支持shadow dom技術,當此之時,也不是全部的shadow dom都合適;因此UI基類須要作一個開關,最大限度的避免生產風險,而又能引入新的技術
1 //與模板對應的css文件,默認不存在,須要各個組件複寫 2 this.uiStyle = null; 3 4 //保存樣式格式化結束的字符串 5 // this.formateStyle = null; 6 7 //保存shadow dom的引用,用於事件代理 8 this.shadowDom = null; 9 this.shadowStyle = null; 10 this.shadowRoot = null; 11 12 //框架統一開關,是否開啓shadow dom 13 this.openShadowDom = true; 14 15 // this.openShadowDom = false; 16 17 //不支持建立接口便關閉,也許有其它因素致使,這個後期已接口放出 18 if (!this.wrapper[0].createShadowRoot) { 19 this.openShadowDom = false; 20 }
基類會多出幾個屬性處理,shadow邏輯,而後在建立UI dom節點時候須要進行特殊處理
1 createRoot: function (html) { 2 3 this.$el = $('<div class="view" style="display: none; " id="' + this.id + '"></div>'); 4 var style = this.getInlineStyle(); 5 6 //若是存在shadow dom接口,而且框架開啓了shadow dom 7 if (this.openShadowDom) { 8 //在框架建立的子元素層面建立沙箱 9 this.shadowRoot = $(this.$el[0].createShadowRoot()); 10 11 this.shadowDom = $('<div class="js_shadow_root">' + html + '</div>'); 12 this.shadowStyle = $(style); 13 14 //開啓shadow dom狀況下,組件須要被包裹起來 15 this.shadowRoot.append(this.shadowStyle); 16 this.shadowRoot.append(this.shadowDom); 17 18 } else { 19 20 this.$el.html(style + html); 21 } 22 },
在開啓shadow dom功能的狀況下,便會爲根節點建立shadow root,將style節點與html節點裝載進去,這個時候UI結構基本出來了,事件便綁定至shadow root便可,這裏是所有代碼:
1 define([], function () { 2 3 var getBiggerzIndex = (function () { 4 var index = 3000; 5 return function (level) { 6 return level + (++index); 7 }; 8 })(); 9 10 return _.inherit({ 11 propertys: function () { 12 //模板狀態 13 this.wrapper = $('body'); 14 this.id = _.uniqueId('ui-view-'); 15 16 this.template = ''; 17 18 //與模板對應的css文件,默認不存在,須要各個組件複寫 19 this.uiStyle = null; 20 21 //保存樣式格式化結束的字符串 22 // this.formateStyle = null; 23 24 //保存shadow dom的引用,用於事件代理 25 this.shadowDom = null; 26 this.shadowStyle = null; 27 this.shadowRoot = null; 28 29 //框架統一開關,是否開啓shadow dom 30 this.openShadowDom = true; 31 32 // this.openShadowDom = false; 33 34 //不支持建立接口便關閉,也許有其它因素致使,這個後期已接口放出 35 if (!this.wrapper[0].createShadowRoot) { 36 this.openShadowDom = false; 37 } 38 39 this.datamodel = {}; 40 this.events = {}; 41 42 //自定義事件 43 //此處須要注意mask 綁定事件先後問題,考慮scroll.radio插件類型的mask應用,考慮組件通訊 44 this.eventArr = {}; 45 46 //初始狀態爲實例化 47 this.status = 'init'; 48 49 this.animateShowAction = null; 50 this.animateHideAction = null; 51 52 // this.availableFn = function () { } 53 54 }, 55 56 on: function (type, fn, insert) { 57 if (!this.eventArr[type]) this.eventArr[type] = []; 58 59 //頭部插入 60 if (insert) { 61 this.eventArr[type].splice(0, 0, fn); 62 } else { 63 this.eventArr[type].push(fn); 64 } 65 }, 66 67 off: function (type, fn) { 68 if (!this.eventArr[type]) return; 69 if (fn) { 70 this.eventArr[type] = _.without(this.eventArr[type], fn); 71 } else { 72 this.eventArr[type] = []; 73 } 74 }, 75 76 trigger: function (type) { 77 var _slice = Array.prototype.slice; 78 var args = _slice.call(arguments, 1); 79 var events = this.eventArr; 80 var results = [], i, l; 81 82 if (events[type]) { 83 for (i = 0, l = events[type].length; i < l; i++) { 84 results[results.length] = events[type][i].apply(this, args); 85 } 86 } 87 return results; 88 }, 89 90 bindEvents: function () { 91 var events = this.events; 92 var el = this.$el; 93 if (this.openShadowDom) el = this.shadowRoot; 94 95 if (!(events || (events = _.result(this, 'events')))) return this; 96 this.unBindEvents(); 97 98 // 解析event參數的正則 99 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 100 var key, method, match, eventName, selector; 101 102 // 作簡單的字符串數據解析 103 for (key in events) { 104 method = events[key]; 105 if (!_.isFunction(method)) method = this[events[key]]; 106 if (!method) continue; 107 108 match = key.match(delegateEventSplitter); 109 eventName = match[1], selector = match[2]; 110 method = _.bind(method, this); 111 eventName += '.delegateUIEvents' + this.id; 112 113 if (selector === '') { 114 el.on(eventName, method); 115 } else { 116 el.on(eventName, selector, method); 117 } 118 } 119 120 return this; 121 }, 122 123 unBindEvents: function () { 124 var el = this.$el; 125 if (this.openShadowDom) el = this.shadowRoot; 126 127 el.off('.delegateUIEvents' + this.id); 128 return this; 129 }, 130 131 createRoot: function (html) { 132 133 this.$el = $('<div class="view" style="display: none; " id="' + this.id + '"></div>'); 134 var style = this.getInlineStyle(); 135 136 //若是存在shadow dom接口,而且框架開啓了shadow dom 137 if (this.openShadowDom) { 138 //在框架建立的子元素層面建立沙箱 139 this.shadowRoot = $(this.$el[0].createShadowRoot()); 140 141 this.shadowDom = $('<div class="js_shadow_root">' + html + '</div>'); 142 this.shadowStyle = $(style); 143 144 //開啓shadow dom狀況下,組件須要被包裹起來 145 this.shadowRoot.append(this.shadowStyle); 146 this.shadowRoot.append(this.shadowDom); 147 148 } else { 149 150 this.$el.html(style + html); 151 } 152 }, 153 154 getInlineStyle: function () { 155 //若是不存在便不予理睬 156 if (!_.isString(this.uiStyle)) return null; 157 var style = this.uiStyle, uid = this.id; 158 159 //在此處理shadow dom的樣式,直接返回處理結束後的html字符串 160 if (!this.openShadowDom) { 161 //建立定製化的style字符串,會模擬一個沙箱,該組件樣式不會對外影響,實現原理即是加上#id 前綴 162 style = style.replace(/(\s*)([^\{\}]+)\{/g, function (a, b, c) { 163 return b + c.replace(/([^,]+)/g, '#' + uid + ' $1') + '{'; 164 }); 165 } 166 167 style = '<style >' + style + '</style>'; 168 this.formateStyle = style; 169 return style; 170 }, 171 172 render: function (callback) { 173 var data = this.getViewModel() || {}; 174 175 var html = this.template; 176 if (!this.template) return ''; 177 if (data) { 178 html = _.template(this.template)(data); 179 } 180 181 typeof callback == 'function' && callback.call(this); 182 return html; 183 }, 184 185 //刷新根據傳入參數判斷是否走onCreate事件 186 //這裏原來的dom會被移除,事件會所有丟失 須要修復***************************** 187 refresh: function (needEvent) { 188 var html = ''; 189 this.resetPropery(); 190 //若是開啓了沙箱便只能從新渲染了 191 if (needEvent) { 192 this.create(); 193 } else { 194 html = this.render(); 195 if (this.openShadowDom) { 196 //將解析後的style與html字符串裝載進沙箱 197 //************* 198 this.shadowDom.html(html); 199 } else { 200 this.$el.html(this.formateStyle + html); 201 } 202 } 203 this.initElement(); 204 if (this.status != 'hide') this.show(); 205 this.trigger('onRefresh'); 206 }, 207 208 _isAddEvent: function (key) { 209 if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide') 210 return true; 211 return false; 212 }, 213 214 setOption: function (options) { 215 //這裏能夠寫成switch,開始沒有想到有這麼多分支 216 for (var k in options) { 217 if (k == 'datamodel' || k == 'events') { 218 _.extend(this[k], options[k]); 219 continue; 220 } else if (this._isAddEvent(k)) { 221 this.on(k, options[k]) 222 continue; 223 } 224 this[k] = options[k]; 225 } 226 // _.extend(this, options); 227 }, 228 229 initialize: function (opts) { 230 this.propertys(); 231 this.setOption(opts); 232 this.resetPropery(); 233 //添加系統級別事件 234 this.addEvent(); 235 //開始建立dom 236 this.create(); 237 this.addSysEvents(); 238 239 this.initElement(); 240 241 }, 242 243 //內部重置event,加入全局控制類事件 244 addSysEvents: function () { 245 if (typeof this.availableFn != 'function') return; 246 this.removeSysEvents(); 247 this.$el.on('click.system' + this.id, $.proxy(function (e) { 248 if (!this.availableFn()) { 249 e.preventDefault(); 250 e.stopImmediatePropagation && e.stopImmediatePropagation(); 251 } 252 }, this)); 253 }, 254 255 removeSysEvents: function () { 256 this.$el.off('.system' + this.id); 257 }, 258 259 $: function (selector) { 260 return this.openShadowDom ? this.shadowDom.find(selector) : this.$el.find(selector); 261 }, 262 263 //提供屬性重置功能,對屬性作檢查 264 resetPropery: function () { 265 }, 266 267 //各事件註冊點,用於被繼承 268 addEvent: function () { 269 }, 270 271 create: function () { 272 this.trigger('onPreCreate'); 273 this.createRoot(this.render()); 274 275 this.status = 'create'; 276 this.trigger('onCreate'); 277 }, 278 279 //實例化須要用到到dom元素 280 initElement: function () { }, 281 282 show: function () { 283 if (!this.wrapper[0] || !this.$el[0]) return; 284 //若是包含就不要亂搞了 285 if (!$.contains(this.wrapper[0], this.$el[0])) { 286 this.wrapper.append(this.$el); 287 } 288 289 this.trigger('onPreShow'); 290 291 if (typeof this.animateShowAction == 'function') 292 this.animateShowAction.call(this, this.$el); 293 else 294 this.$el.show(); 295 296 this.status = 'show'; 297 this.bindEvents(); 298 this.trigger('onShow'); 299 }, 300 301 hide: function () { 302 if (!this.$el || this.status !== 'show') return; 303 304 this.trigger('onPreHide'); 305 306 if (typeof this.animateHideAction == 'function') 307 this.animateHideAction.call(this, this.$el); 308 else 309 this.$el.hide(); 310 311 this.status = 'hide'; 312 this.unBindEvents(); 313 this.removeSysEvents(); 314 this.trigger('onHide'); 315 }, 316 317 destroy: function () { 318 this.status = 'destroy'; 319 this.unBindEvents(); 320 this.removeSysEvents(); 321 this.$el.remove(); 322 this.trigger('onDestroy'); 323 delete this; 324 }, 325 326 getViewModel: function () { 327 return this.datamodel; 328 }, 329 330 setzIndexTop: function (el, level) { 331 if (!el) el = this.$el; 332 if (!level || level > 10) level = 0; 333 level = level * 1000; 334 el.css('z-index', getBiggerzIndex(level)); 335 } 336 337 }); 338 339 });
基類代碼改動結束,一旦開啓shadow dom開關,每一個組件便會走shadow邏輯,不然走原邏輯:
關閉接口的話,又變成了這個樣子了:
web components的提出,旨在解決UI重用的問題、解決相同功能接口各異的問題,大規模的用於生產彷佛不太接地氣,可是shadow dom技術對於webapp倒是個好東西。
上文還只是在UI層面上應用shadow dom技術,webapp中每一個view頁面片若是能夠應用shadow dom技術的話,各個View將沒必要考慮id重複污染、css樣式污染、javascript變量污染,而且效率還比原來高多了,由於對於頁面來講,他就僅僅是一個標籤而已,如此一來,大規模的webapp的網站可能真的會到來了!
demo地址:http://yexiaochai.github.io/cssui/demo/debug.html#num
代碼地址:https://github.com/yexiaochai/cssui/tree/gh-pages
博主正在學習web components技術,而且嘗試將之用於項目,文中有誤或者有不妥的地方請您提出