博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【shadow dom入UI】web components思想如何应用于实际项目
阅读量:5815 次
发布时间:2019-06-18

本文共 12089 字,大约阅读时间需要 40 分钟。

回顾

经过昨天的优化处理(),我们在UI一块做了几个关键动作:

① CSS入UI

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

这样做基本可以规避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事实上也是一个文档碎片,我们甚至可以将之作为jQuery包装对象处理:

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

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

另一个比较重要的差别是,外部为组件定义的事件,比如click事件的e.target便只能是组件div了,也就是这个组件事实上只有一层,一个标签,内部的结构不会被暴露!

引入框架

原来我们的组件是这样的结构:

1 
2
3
4
1个
6
7
8

框架会主动创建一个包裹层,包裹层内才是组件dom,经过昨天的处理,组件变成了这样:

1 
2
3
4
7 8
9
10

如果这里我们使用shadow dom技术的话,整个结构会变成这样:

1 
2 #shadow-root3
4
5
6

组件自动创建的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 dom13   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 dom137 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 //开始创建dom236 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 else294 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 else309 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的意义

web components的提出,旨在解决UI重用的问题、解决相同功能接口各异的问题,大规模的用于生产似乎不太接地气,但是shadow dom技术对于webapp却是个好东西。

上文还只是在UI层面上应用shadow dom技术,webapp中每个view页面片如果可以应用shadow dom技术的话,各个View将不必考虑id重复污染、css样式污染、javascript变量污染,并且效率还比原来高多了,因为对于页面来说,他就仅仅是一个标签而已,如此一来,大规模的webapp的网站可能真的会到来了!

demo地址:

代码地址:

博主正在学习web components技术,并且尝试将之用于项目,文中有误或者有不妥的地方请您提出

转载地址:http://rlmbx.baihongyu.com/

你可能感兴趣的文章
通过vb.net 和NPOI实现对excel的读操作
查看>>
TCP segmentation offload
查看>>
java数据类型
查看>>
数据结构——串的朴素模式和KMP匹配算法
查看>>
FreeMarker-Built-ins for strings
查看>>
验证DataGridView控件的数据输入
查看>>
POJ1033
查看>>
argparse - 命令行选项与参数解析(转)
查看>>
一维数组
查看>>
Linux学习笔记之三
查看>>
CentOS 6.6 FTP install
查看>>
图解Ajax工作原理
查看>>
oracle导入导出小记
查看>>
聊一聊log4j2配置文件log4j2.xml
查看>>
NeHe OpenGL教程 第七课:光照和键盘
查看>>
修改上一篇文章的node.js代码,支持默认页及支持中文
查看>>
Php实现版本比较接口
查看>>
删除设备和驱动器中软件图标
查看>>
第四章 TCP粘包/拆包问题的解决之道---4.1---
查看>>
html语言
查看>>