Web Components简介
Web Components由以下三项技术组成:
- Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
- Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates(HTML模板): 和
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。 Web Components - MDN:
可以到这个站点找到一些好用的Web Component:
创建
- 定义一个类继承HTMLElement
- 在constructor中创建元素
- 使用
window.customElements.define
定义Web Component - 在HTML中使用此Web Component
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <user-card></user-card> <script> class UserCard extends HTMLElement { constructor() { super(); let image = document.createElement('img'); image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png'; image.width = '100' image.height = '100' image.classList.add('image'); let container = document.createElement('div'); container.classList.add('container'); let name = document.createElement('p'); name.classList.add('name'); name.innerText = 'User Name'; let email = document.createElement('p'); email.classList.add('email'); email.innerText = 'yourmail@some-email.com'; let button = document.createElement('button'); button.classList.add('button'); button.innerText = 'Follow'; container.append(name, email, button); this.append(image, container); } } window.customElements.define('user-card', UserCard); </script> </body> </html>
效果:
模板
通过上面的方式可以穿件Web Component,但过程比较繁琐,可以通过模板的方式创建。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <user-card></user-card> <template id="userCardTemplate"> <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image" width="100" height="100"> <div class="container"> <p class="name">User Name</p> <p class="email">yourmail@some-email.com</p> <button class="button">Follow</button> </div> </template> <script> class UserCard extends HTMLElement { constructor() { super(); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); this.appendChild(content); } } window.customElements.define('user-card', UserCard); </script> </body> </html>
样式
上面创建的Web Component并没有样式,可以直接在template中嵌入style书写样式。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <user-card></user-card> <template id="userCardTemplate"> <style> :host { display: flex; align-items: center; width: 450px; height: 180px; background-color: #d4d4d4; border: 1px solid #d5d5d5; box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); border-radius: 3px; overflow: hidden; padding: 10px; box-sizing: border-box; font-family: 'Poppins', sans-serif; } .image { flex: 0 0 auto; width: 160px; height: 160px; vertical-align: middle; border-radius: 5px; } .container { box-sizing: border-box; padding: 20px; height: 160px; } .container > .name { font-size: 20px; font-weight: 600; line-height: 1; margin: 0; margin-bottom: 5px; } .container > .email { font-size: 12px; opacity: 0.75; line-height: 1; margin: 0; margin-bottom: 15px; } .container > .button { padding: 10px 25px; font-size: 12px; border-radius: 5px; text-transform: uppercase; } </style> <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image" width="100" height="100"> <div class="container"> <p class="name">User Name</p> <p class="email">yourmail@some-email.com</p> <button class="button">Follow</button> </div> </template> <script> class UserCard extends HTMLElement { constructor() { super(); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); this.appendChild(content); } } window.customElements.define('user-card', UserCard); </script> </body> </html>
效果:
插槽
跟Vue差不多,可以通过slot插槽传入元素:
<user-card> <img width="200" height="200" slot="img" src="https://semantic-ui.com/images/avatar2/large/kristy.png"> </user-card> <template id="userCardTemplate"> <style> ... </style> <slot name="img"></slot> <div class="container"> <p class="name"></p> <p class="email"></p> <button class="button">Follow John</button> </div> </template>
参数
通过往Web Component中传递参数,可以在创建的时候通过以下方式获取参数并设置值:
let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); this.appendChild(content);
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <user-card image="https://semantic-ui.com/images/avatar2/large/kristy.png" name="User Name" email="yourmail@some-email.com" ></user-card> <template id="userCardTemplate"> <style> :host { display: flex; align-items: center; width: 450px; height: 180px; background-color: #d4d4d4; border: 1px solid #d5d5d5; box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); border-radius: 3px; overflow: hidden; padding: 10px; box-sizing: border-box; font-family: 'Poppins', sans-serif; } .image { flex: 0 0 auto; width: 160px; height: 160px; vertical-align: middle; border-radius: 5px; } .container { box-sizing: border-box; padding: 20px; height: 160px; } .container > .name { font-size: 20px; font-weight: 600; line-height: 1; margin: 0; margin-bottom: 5px; } .container > .email { font-size: 12px; opacity: 0.75; line-height: 1; margin: 0; margin-bottom: 15px; } .container > .button { padding: 10px 25px; font-size: 12px; border-radius: 5px; text-transform: uppercase; } </style> <img class="image"> <div class="container"> <p class="name"></p> <p class="email"></p> <button class="button">Follow John</button> </div> </template> <script> class UserCard extends HTMLElement { constructor() { super(); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); this.appendChild(content); } } window.customElements.define('user-card', UserCard); </script> </body> </html>
Shadow DOM
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。
这里,有一些 Shadow DOM 特有的术语需要我们了解:Shadow host
:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。Shadow tree
:Shadow DOM内部的DOM树。Shadow boundary
:Shadow DOM结束的地方,也是常规 DOM开始的地方。Shadow root
: Shadow tree的根节点。
Shadow DOM 并不是一个新事物——在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的
标签,实际上,在它的 Shadow DOM 中,包含了一系列的按钮和其他控制器。Shadow DOM 标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。自定义元素的
this.attachShadow()
方法开启 Shadow DOM,详见下面的代码。class UserCard extends HTMLElement { constructor() { super(); let shadow = this.attachShadow( { mode: 'closed' } ); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); shadow.appendChild(content); } } window.customElements.define('user-card', UserCard);
上面代码中,
this.attachShadow()
方法的参数{ mode: 'closed' }
,表示 Shadow DOM 是封闭的,不允许外部访问。通过Shadow DOM技术可以使
:host
的样式生效:打开控制台,可以看到没有Shadow DOM和有Shadow DOM的区别。
没有Shadow DOM:
有Shadow DOM:Shadow DOM 的一大优点是能将 DOM 结构、样式、行为与 Document DOM 隔离开,非常适合做组件的封装,因此它能成为 Web Component 的重要组成部分之一。
交互
跟普通的DOM事件绑定一样,获取到元素后通过
addEventListener
绑定事件即可。class UserCard extends HTMLElement { constructor() { super(); let shadow = this.attachShadow( { mode: 'closed' } ); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); shadow.appendChild(content); this.$button = shadow.querySelector('button'); this.$button.addEventListener('click', () => { alert('click') }); } } window.customElements.define('user-card', UserCard);
封装
封装的目的在昱可复用,思路很多。
第一种方式
将模板换为字符串,通过window.document.body.innerHTML
追加到body中,然后在html中引入此js。components/user-card.js
let templateStr = ` <template id="userCardTemplate"> <style> :host { display: flex; align-items: center; width: 450px; height: 180px; background-color: #d4d4d4; border: 1px solid #d5d5d5; box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); border-radius: 3px; overflow: hidden; padding: 10px; box-sizing: border-box; font-family: 'Poppins', sans-serif; } .image { flex: 0 0 auto; width: 160px; height: 160px; vertical-align: middle; border-radius: 5px; } .container { box-sizing: border-box; padding: 20px; height: 160px; } .container > .name { font-size: 20px; font-weight: 600; line-height: 1; margin: 0; margin-bottom: 5px; } .container > .email { font-size: 12px; opacity: 0.75; line-height: 1; margin: 0; margin-bottom: 15px; } .container > .button { padding: 10px 25px; font-size: 12px; border-radius: 5px; text-transform: uppercase; } </style> <slot name="img"></slot> <div class="container"> <p class="name"></p> <p class="email"></p> <button class="button">Follow John</button> </div> </template> ` window.document.body.innerHTML += templateStr class UserCard extends HTMLElement { constructor() { super(); let shadow = this.attachShadow( { mode: 'closed' } ); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); shadow.appendChild(content); this.$button = shadow.querySelector('button'); this.$button.addEventListener('click', () => { alert('click') }); } } window.customElements.define('user-card', UserCard);
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <user-card image="https://semantic-ui.com/images/avatar2/large/kristy.png" name="User Name" email="yourmail@some-email.com" > <img slot="img" src="https://semantic-ui.com/images/avatar2/large/kristy.png" width="200" height="200"> </user-card> <script src="./components/user-card.js"></script> </body> </html>
需要注意的是,这种方式只能将script引入放于body之中或之后,否则找不到body。
在上面的基础上,我们还可以把样式单独提出来,
let templateStr = ` <template id="userCardTemplate"> <slot name="img"></slot> <div class="container"> <p class="name"></p> <p class="email"></p> <button class="button">Follow John</button> </div> </template> ` let styleStr = ` :host { display: flex; align-items: center; width: 450px; height: 180px; background-color: #d4d4d4; border: 1px solid #d5d5d5; box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); border-radius: 3px; overflow: hidden; padding: 10px; box-sizing: border-box; font-family: 'Poppins', sans-serif; } .image { flex: 0 0 auto; width: 160px; height: 160px; vertical-align: middle; border-radius: 5px; } .container { box-sizing: border-box; padding: 20px; height: 160px; } .container > .name { font-size: 20px; font-weight: 600; line-height: 1; margin: 0; margin-bottom: 5px; } .container > .email { font-size: 12px; opacity: 0.75; line-height: 1; margin: 0; margin-bottom: 15px; } .container > .button { padding: 10px 25px; font-size: 12px; border-radius: 5px; text-transform: uppercase; } ` window.document.body.innerHTML += templateStr class UserCard extends HTMLElement { constructor() { super(); let shadow = this.attachShadow( { mode: 'closed' } ); // 创建样式并为shadow Dom添加样式 const style = document.createElement('style'); style.textContent = styleStr shadow.appendChild(style); let templateElem = document.getElementById('userCardTemplate'); let content = templateElem.content.cloneNode(true); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); shadow.appendChild(content); this.$button = shadow.querySelector('button'); this.$button.addEventListener('click', () => { alert('click') }); } } window.customElements.define('user-card', UserCard);
参考资料