Web Component #3 - Shadow DOM(影子 DOM)
什么是Shadow DOM?
Shadow DOM(影子 DOM),类似iframe
一个独立DOM容器,可以封装子元素,里面的样式与全局样式不会相互污染. 与iframe不同的是共用一个windows空间. JS事件又能互通的.ShadowDOM是WebCompnent组件重要的一部分.
ShadowDOM起到样式和脚本隔离.
先看例子,继续之前的button组件.
<style>
button{
color:red;
font-size: 18px;
}
</style>
<button>原生按扭</button>
<my-button>这是一个按扭</my-button>
<script>
class MyButton extends HTMLElement{
constructor(){
super();
}
connectedCallback() {
this.innerHTML=`
<style>
button{
background-color: #000;
color: #fff;
border: 0;
padding: 10px 20px;
border-radius: 5px;
}
</style>
<button>${this.innerHTML}</button>
`;
}
}
customElements.define('my-button', MyButton);
</script>
可以看到,自定义button和原生button 样式都相互污染.
在来看一样JS,外面有段按扭监听事件,会怎么样?
let btns = document.querySelectorAll('button');
// 监听事件
btns.forEach(btn => {
btn.addEventListener('click', function(e){
console.log("点击了",e.target.textContent);
});
})
结果
组件的核心就是封装,复用和隔离.现在样式和JS都没做到隔离.怎么办?
那么引出了ShadowDOM. 上面结果稍微修改一下.
class MyButton extends HTMLElement{
constructor(){
super();
// 启动用shadow
this.attachShadow({mode: 'open'});
}
connectedCallback() {
// 在影子DOM中插入内容
this.shadowRoot.innerHTML=`
<style>
button{
background-color: #000;
color: #fff;
border: 0;
padding: 10px 20px;
border-radius: 5px;
}
</style>
<button>${this.innerHTML}</button>
`;
}
}
上面的样式和JS问题都解决了. 再看一下生成的HTML结构.
核心,两句代码:
// 启用shadow,连续到customElement自定义组件中
this.attachShadow({mode: 'open'});
// 操作shadow 根节点
this.shadowRoot...
ShadowDOM结构?
ShaowDOM和常规DOM一样.也包含DOM树.
这是官方的图.
意思:
- shadow dom 树与 dom 树一样
- shadow dom 有独立的空间
- dom 树中的 shodow host === shadow root,就是shadow 容器.
- shadow 挂载(关联) 在 shodow host 上.
比如上面的my-button 的结构如下.
几个关键点:
- 挂载点
- 根节点
- slot插槽
可以看到外面的样式可以影响到里面,但shadow里面不能反向修改外面的样式.shadow DOM 对组件起到很大封装,隔离的作用.
内置shadow元素
其实shadow DOM不是什么新鲜的玩意,我们经常用到. 一些浏览器默认的元素就是通过shadow DOM 实现的封装.
比如: <input>
,<select>
,<textarea>
,<video>
,<progress>
,<details>
等就是封装了Shadow DOM.
chrome 打开设置,"user-agent shadow DOM" 配置开关就能看到内置shadow DOM里面的结构.
input结构
select,textarea
video
video 比较复杂,嵌套了input range 进度条.
details
展开和隐藏更多, 多个命名slot. 内嵌样式.:host
选择器
创建Shadow DOM
参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM
基本用法:
把一个shadow 绑定到elementRef 元素上. 使其拥有shadow 特性.
let shadow = elementRef.attachShadow({ mode: "open" });
let shadow = elementRef.attachShadow({ mode: "closed" });
mode参数: open
与 closed
差别不大,不建议用closed.
- open: 外面可以直接访问shadow里面的内容.
- closed: 外面不可以使用shadow,获取里面的元素.
获取shadow 内容,通常使用:
elementRef.shadowRoot
当mode 设为"closed" ,elementRef.shadowRoot
为 null
.
为什么不建议用closed呢? 因为js 没有有私有属性.其他人可以通过其他方式绕过限制 ,访问root或修改mode. 不如直接设置open方便调试.
类似浏览器自带的input 为什么不能访问到shadow DOM呢?因为他使用的useragent 代理访问. 属性第三情况.
这就涉及到elementRef,哪些元素不支持attachShadow. 系统不自带的shadow,并且不能直接访问.也不能重新关联.
哪些能用shadow DOM?
- 任何带有有效的名称且可独立存在的(autonomous)自定义元素
<article>
<aside>
<blockquote>
<body>
<div>
<footer>
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
<header>
<main>
<nav>
<p>
<section>
<span>
例子:
const div=document.createElement("div");
// 创建shadow 并关联div,成功返回shadowRoot
const shadowRoot=div.attachShadow({ mode: "closed" });
// 不能重复创建shadow
// div.attachShadow({ mode: "closed" });
// mode:closed 时,div.shadowRoot 返回空
shadowRoot.innerHTML=`
<style>
:host{
color:red;
}
</style>
<h1>Hello World</h1>
`
document.body.appendChild(div);
// 内部可以访问
console.log(shadowRoot?.querySelector("h1"));
// 外部不能访问
console.log(div.shadowRoot?.querySelector("h1"));
安全设置
为了隐藏全局变量const shadowRoot
,可以使用闭包函数
(function(){
const div=document.createElement("div");
const shadowRoot=div.attachShadow({ mode: "closed" });
shadowRoot.innerHTML=`
<style>
:host{
color:red;
}
</style>
<h1>Hello World</h1>
`
document.body.appendChild(div);
})()
你以为现在就安全啦?
把 attachShadow
函数修改. 现在mode配置失效.
HTMLDivElement.prototype._attachShadow = HTMLDivElement.prototype.attachShadow;
HTMLDivElement.prototype.attachShadow = function () {
// 强制 改mod:open
return this._attachShadow( { mode: "open" } );
};
通过删除shadowRoot属性,防止外面直接访问
// 删除shadowRoot属性
Object.defineProperty(div, "shadowRoot", {
enumerable:false,
writable:false,
value:null,
configurable:false
})
破坏者,但破坏者可禁用 Object.defineProperty
使用
// 禁用配置属性
Object._defineProperty=Object.defineProperty;
Object.defineProperty=function(obj,prop,conf){
if(obj instanceof HTMLDivElement){
return ;
}
return Object._defineProperty(obj,prop,conf);
};
// 不允许别人修改
Object.freeze(Object)
所以,之前说closed
只能防君子,不能防小人.还影响调试. 无解.
Shadow DOM 常用API
- Element.attachShadow(): 创建
- ShadowRoot.innerHTML: 获取/设置内容
- ShadowRoot.querySelector() : 获取
- ShadowRoot.host: 获取宿主.
- ShadowRoot.mode: 获取mode模式,open 或 closed
- ShadowRoot.adoptedStyleSheets: 将外部样式表(CSSStyleSheet)引入到 Shadow DOM 内,实现样式的隔离和共享。后面细节样式.
const shadowRoot = element.shadowRoot;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync('p { color: blue; }');
shadowRoot.adoptedStyleSheets = [styleSheet];
- ShadowRoot.styleSheets: 获取样式
- ShadowRoot.activeElement: 获取焦点所在的元素.
document.activeElement
是一样的 - DocumentOrShadowRoot.getSelection: 光标位置
- ShadowRoot.slotAssignment : slot 分配方式. slot 节解.
专题大纲
- Web Component #1 - 自定义组件简介
- Web Component #2 - 自定义元素
- Web Component #3 - Shadow DOM(影子 DOM)
- Web Component #4 - template模板和Slot插槽
- Web Component #5 - 样式
- Web Component #6 - 生命周期
- Web Component #7 - 属性getter/setter
- Web Component #8 - 自定义事件
- Web Component #9 - 组件module封装
- Web Component #10 - Form表单组件
- Web Component #11 - Seo优化
- Web Component #12 - 优秀组件库推荐
原作者:阿金
本文地址:https://hi-arkin.com/archives/web-components-3.html