Web Components 上手指南
SegmentFault
共 13310字,需浏览 27分钟
·
2021-03-01 13:23
现在的前端开发基本离不开 React、Vue 这两个框架的支撑,而这两个框架下面又衍生出了许多的自定义组件库:
Element(Vue) Ant Design(React)
什么是 Web Components?
如何使用 Web Components?
Custom elements(自定义元素):一组 JavaScript API,用来创建自定义的 HTML标签,并允许标签创建或销毁时进行一些操作; Shadow DOM(影子DOM):一组 JavaScript API,用于将创建的 DOM Tree 插入到现有的元素中,且 DOM Tree 不能被外部修改,不用担心元素被其他地方影响; HTML templates(HTML模板):通过 <template>、<slot> 直接在 HTML 文件中编写模板,然后通过 DOM API 获取。
Custom elements(自定义元素)
自定义元素的名称,一个 DOMString 标准的字符串,为了防止自定义元素的冲突,必须是一个带短横线连接的名称(e.g. custom-tag)。 定义自定义元素的一些行为,类似于 React、Vue 中的生命周期。 扩展参数(可选),该参数类型为一个对象,且需要包含 extends 属性,用于指定创建的元素继承自哪一个内置元素(e.g. { extends: 'p' })。
创建一个新的 HTML 标签
class HelloUser extends HTMLElement {
constructor() {
// 必须调用 super 方法
super();
// 创建一个 div 标签
const $box = document.createElement("p");
let userName = "User Name";
if (this.hasAttribute("name")) {
// 如果存在 name 属性,读取 name 属性的值
userName = this.getAttribute("name");
}
// 设置 div 标签的文本内容
$box.innerText = `Hello ${userName}`;
// 创建一个 shadow 节点,创建的其他元素应附着在该节点上
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild($box);
}
}
// 定义一个名为 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
<hello-user name="Shenfq"></hello-user>
扩展已有的 HTML 标签
class SkillList extends HTMLUListElement {
constructor() {
// 必须调用 super 方法
super();
if (
this.hasAttribute("skills") &&
this.getAttribute("skills").includes(',')
) {
// 读取 skills 属性的值
const skills = this.getAttribute("skills").split(',');
skills.forEach(skill => {
const item = document.createElement("li");
item.innerText = skill;
this.appendChild(item);
})
}
}
}
// 对 <ul> 标签进行扩展
customElements.define("skill-list", SkillList, { extends: "ul" });<ul is="skill-list" skills="js,css,html"></ul>
生命周期
connectedCallback:当自定义元素被插入到页面的 DOM 文档时调用。 disconnectedCallback:当自定义元素从 DOM 文档中被删除时调用。 adoptedCallback:当自定义元素被移动时调用。 attributeChangedCallback: 当自定义元素增加、删除、修改自身属性时调用。
class HelloUser extends HTMLElement {
constructor() {
// 必须调用 super 方法
super();
// 创建一个 div 标签
const $box = document.createElement("p");
let userName = "User Name";
if (this.hasAttribute("name")) {
// 如果存在 name 属性,读取 name 属性的值
userName = this.getAttribute("name");
}
// 设置 div 标签的文本内容
$box.innerText = `Hello ${userName}`;
// 创建一个 shadow 节点,创建的其他元素应附着在该节点上
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild($box);
}
connectedCallback() {
console.log('创建元素')
// 5s 后移动元素到 iframe
setTimeout(() => {
const iframe = document.getElementsByTagName("iframe")[0]
iframe.contentWindow.document.adoptNode(this)
}, 5e3)
}
disconnectedCallback() {
console.log('删除元素')
}
adoptedCallback() {
console.log('移动元素')
}
}
<!-- 页面插入一个 iframe,将自定义元素移入其中 -->
<iframe width="0" height="0"></iframe>
<hello-user name="Shenfq"></hello-user>
Shadow DOM(影子DOM)
创建 Shadow DOM
<div id="root"></div>
<script>
// 获取页面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '创建一个 shadow 节点';
const shadow = $root.attachShadow({mode: 'open'});
shadow.appendChild($p);
</script>
mode 的差异
<div id="root"></div>
<script>
// 获取页面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '创建一个 shadow 节点';
const shadow = $root.attachShadow({mode: 'open'});
shadow.appendChild($p);
console.log('is open', $div.shadowRoot);
</script>
<div id="root"></div>
<script>
// 获取页面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '创建一个 shadow 节点';
const shadow = $root.attachShadow({mode: 'closed'});
shadow.appendChild($p);
console.log('is closed', $div.shadowRoot);
</script>
HTML templates(HTML模板)
使用模板
<template id="helloUserTpl">
<p class="name">Name</p>
<a target="blank" class="blog">##</a>
</template>
// 通过 ID 获取标签
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);
<hello-user name="Shenfq" blog="http://blog.shenfq.com" />
<script>
class HelloUser extends HTMLElement {
constructor() {
// 必须调用 super 方法
super();
// 通过 ID 获取标签
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);
if (this.hasAttribute('name')) {
const $name = content.querySelector('.name');
$name.innerText = this.getAttribute('name');
}
if (this.hasAttribute('blog')) {
const $blog = content.querySelector('.blog');
$blog.innerText = this.getAttribute('blog');
$blog.setAttribute('href', this.getAttribute('blog'));
}
// 创建一个 shadow 节点,创建的其他元素应附着在该节点上
const shadow = this.attachShadow({ mode: "closed" });
shadow.appendChild(content);
}
}
// 定义一个名为 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
</script>
添加样式
<template id="helloUserTpl">
<style>
:host {
display: flex;
flex-direction: column;
width: 200px;
padding: 20px;
background-color: #D4D4D4;
border-radius: 3px;
}
.name {
font-size: 20px;
font-weight: 600;
line-height: 1;
margin: 0;
margin-bottom: 5px;
}
.email {
font-size: 12px;
line-height: 1;
margin: 0;
margin-bottom: 15px;
}
</style>
<p class="name">User Name</p>
<a target="blank" class="blog">##</a>
</template>
占位元素
<template id="helloUserTpl">
<p class="name">User Name</p>
<a target="blank" class="blog">##</a>
<!--占位符-->
<slot name="desc"></slot>
</template>
<hello-user name="Shenfq" blog="http://blog.shenfq.com">
<p slot="desc">欢迎关注公众号:更了不起的前端</p>
</hello-user>
总结
浏览器原生支持,不需要引入额外的第三方库; 真正的内部私有化的 CSS,不会产生样式的冲突; 无需经过编译操作,即可实现的组件化方案,且与外部 DOM 隔离;
评论