【shadow dom入UI】web components思想如何应用于实际项目

回顾

经过昨天的优化处理(【前端优化之拆分CSS】前端三剑客的分分合合),我们在UI一块做了几个关键动作:

① CSS入UI

② CSS作为组件的一个节点而存在,并且会被“格式化”,即选择器带id前缀,形成的组件如图所示:

【shadow dom入UI】web components思想如何应用于实际项目_第1张图片

这样做基本可以规避css污染的问题,解决绝大多数问题,但是更优的方案总是存在,比如web components中的shadow dom!

javascript的组件基本是不可重用的,几个核心原因是:

① 组件实例与实例之间的html、css、Javascript很容易互相污染(id污染、class污染、js变量污染......)
② 一个组件依赖于HTML、CSS、Javascript,而三者之间是分离的,而组件内部控制于js,更改后外部可能出问题
通过昨天的处理,我们将一个组件所用到的全部合到了一起,却又分离成了三个文件:

① ui.js
② ui.html
③ ui.css

这种处理一方面透露着解耦的思想,另一方面体现着解依赖的想法,在这个基础上想引入shadow dom技术,变得非常轻易。

什么是shadow dom

shadow dom是一种浏览器行为,他允许在document文档中渲染时插入一个独立的dom子树,但这个dom树与主dom树完全分离的,不会互相影响。
从一张图来看:

【shadow dom入UI】web components思想如何应用于实际项目_第2张图片

shadow dom事实上也是一个文档碎片,我们甚至可以将之作为jQuery包装对象处理:

【shadow dom入UI】web components思想如何应用于实际项目_第3张图片

存在在shadow dom中的元素是不可被选择器找到的,比如这种做法会徒劳无功:

$('沙箱中的一个元素') => []

另一个比较重要的差别是,外部为组件定义的事件,比如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   
 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 = $('');
 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 = $('
' + html + '
'); 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 = $('');
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 = $('
' + html + '
'); 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 = ''; 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 });
View Code

基类代码改动结束,一旦开启shadow dom开关,每个组件便会走shadow逻辑,否则走原逻辑:

【shadow dom入UI】web components思想如何应用于实际项目_第4张图片

关闭接口的话,又变成了这个样子了:

【shadow dom入UI】web components思想如何应用于实际项目_第5张图片

引入shadow dom的意义

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技术,并且尝试将之用于项目,文中有误或者有不妥的地方请您提出

你可能感兴趣的:(【shadow dom入UI】web components思想如何应用于实际项目)