作为开发者,我们都知道尽可能多的重用代码是一个好主意。这对于自定义标记结构来说通常不是那么容易 — 想想复杂的HTML(以及相关的样式和脚本),有时您不得不写代码来呈现自定义UI控件,并且如果您不小心的话,多次使用它们会使您的页面变得一团糟。
Web Components旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
<template>
和 <slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
Firefox(从版本 63 开始),Chrome,Opera 和 Safari 默认支持 Shadow DOM。基于 Chromium 的新 Edge 也支持 Shadow DOM;而旧 Edge 未能撑到支持此特性。
shadow DOM有以下特点:
总结起来就是,shadow DOM可以把一部分html代码隔离起来,与外部完全不会互相干扰。
<html>
<head>
<meta charset="utf-8" />
<title>shadow DOM</title>
</head>
<body>
<style>
body {
background-color: #f3f3f3;
}
.text {
color: red;
}
</style>
<div id="div1"><p class="text">这是外面页面的text类文字</p></div>
<div id="div2">
<p class="text">这是原本就在html上的dom元素,3秒后添加到shadow-host里</p>
</div>
<div id="shadow-host">
<p>这是shadow-host下的,与shadow-root平级的兄弟元素,将不会显示</p>
</div>
<script>
function addShadow() {
const shadowHost = document.querySelector("#shadow-host");
// 通过attachShadow创建一个shadow Root
const shadow = shadowHost.attachShadow({ mode: "open" });
const shadowDiv = document.createElement("div");
shadowDiv.setAttribute("class", "text");
shadowDiv.innerText = "shadow DOM内部的text类文字";
// 为shadow dom创建一个style标签,一开始这个style.isConnected为false,把他添加给shadow Root后 isConnected就为true了
const style = document.createElement("style");
console.log(style.isConnected);
style.textContent = `
.text {
color: green
}
`;
// 为shadow dom添加元素
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(shadowDiv);
console.log(document.querySelectorAll(".text"));
console.log(shadow.querySelectorAll(".text"));
setTimeout(() => {
shadow.appendChild(document.querySelector("#div2"));
}, 3000);
}
setTimeout(() => {
addShadow();
}, 2000);
</script>
</body>
</html>
使用外部引用样式
// 将外部引用的样式添加到 Shadow DOM 上
const linkElem = document.createElement("link");
linkElem.setAttribute("rel", "stylesheet");
linkElem.setAttribute("href", "shadow.css");
// 将所创建的元素添加到 Shadow DOM 上
shadow.appendChild(linkElem);
注意
通过attachShadow创建一个shadow Root,那么shadow Root同级的元素依旧存在但是不会显示。
<html>
<head>
<meta charset="utf-8" />
<title>shadow DOM</title>
</head>
<body>
<div id="div1"><p class="text">这是外面页面的text类文字</p></div>
<div id="div2">
<p class="text">这是原本就在html上的dom元素,3秒后添加到shadow-host里</p>
<style>
.text {
color: red;
}
</style>
</div>
<div id="shadow-host"></div>
<script>
function addShadow() {
const shadowHost = document.querySelector("#shadow-host");
// 通过attachShadow创建一个shadow Root
const shadow = shadowHost.attachShadow({ mode: "open" });
const shadowDiv = document.createElement("div");
shadowDiv.setAttribute("class", "text");
shadowDiv.innerText = "shadow DOM内部的text类文字";
shadow.appendChild(shadowDiv);
console.log(document.querySelectorAll(".text"));
console.log(shadow.querySelectorAll(".text"));
setTimeout(() => {
shadow.appendChild(document.querySelector("#div2"));
}, 3000);
}
addShadow();
</script>
</body>
</html>
这样我们会发现
我们把
div2
中的元素添加到shadow root里,里面的.text
样式也被添加了进去,并且外面的元素也不再受.text
样式的影响
可以使用 Element.attachShadow()
方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot
属性:
let myShadowDom = myCustomElem.shadowRoot;
如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode
设置为 closed
,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot
将会返回 null
。浏览器中的某些内置元素就是如此,例如<video>
,包含了不可访问的 Shadow DOM。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<z-div>123456</z-div>
<script>
class ZDiv extends HTMLElement {
constructor() {
// 必须首先调用 super 方法
super();
console.info(this.innerHTML);
}
}
customElements.define("z-div", ZDiv);
</script>
</body>
</html>
CustomElementRegistry.define()
方法用来注册一个 custom element,该方法接受以下参数:
DOMString
标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线。可选参数
,一个包含 extends
属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。或者这样
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<p is="z-div">123</p>
<script>
class ZDiv extends HTMLParagraphElement {
constructor() {
// 必须首先调用 super 方法
super();
console.info(this);
}
}
customElements.define("z-div", ZDiv, { extends: "p" });
</script>
</body>
</html>
这种方式构造方法必须和继承的元素一致。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<z-div text="123"></z-div>
<script defer>
class ZDiv extends HTMLElement {
constructor() {
// 必须首先调用 super 方法
super();
const shadow = this.attachShadow({ mode: "open" });
const shadowDiv = document.createElement("div");
shadowDiv.innerText = this.getAttribute("text");
shadow.appendChild(shadowDiv);
console.info(shadow);
}
}
customElements.define("z-div", ZDiv);
</script>
</body>
</html>
不论是iframe或者是sahdow dom数据消息交互我们就都可以用postMessage。
但是iframe内外的window对象是不一样的,但是sahdow dom内外是一样的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<z-div text="我是原信息"></z-div>
<button onclick="sendMsg()">发送事件</button>
<script defer>
class ZDiv extends HTMLElement {
constructor() {
// 必须首先调用 super 方法
super();
const shadow = this.attachShadow({ mode: "open" });
const shadowDiv = document.createElement("div");
shadowDiv.innerText = this.getAttribute("text");
shadow.appendChild(shadowDiv);
window.addEventListener(
"message",
function (event) {
shadowDiv.innerText =
event.origin + " 发来信息:\n " + JSON.stringify(event.data);
},
false
);
}
}
customElements.define("z-div", ZDiv);
function sendMsg() {
window.postMessage({
time: new Date().toLocaleString()
});
}
</script>
</body>
</html>
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message
将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
targetOrigin
通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”“(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。**如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是\。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。**
transfer
可选
是一串和message 同时传递的 Transferable
对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
接收时的message 的属性有:
data
从其他 window 中传递过来的对象。
origin
调用 postMessage
时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。例如 “https://example.org
(隐含端口 443
)”、“http://example.net
(隐含端口 80
)”、“http://example.com:8080
”。请注意,这个origin不能保证是该窗口的当前或未来origin,因为postMessage被调用后可能被导航到不同的位置。
source
对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。
如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。
如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。 任何窗口都可以向任何其他窗口发送消息,并且您不能保证未知发件人不会发送恶意消息。 但是,验证身份后,您仍然应该始终验证接收到的消息的语法。 否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。
当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*
。 恶意网站可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用postMessage发送的数据。