概念:
web components是原生的组件化开发技术,它可以让我们创建自定义的html元素,并且功能和样式都会封装在组件内部,不会影响其他的元素。
注意:web components与现有的react、vue等库不冲突,是相辅相成的。
web components里面有三个概念:
1. Custom elements(自定义元素)
2. Shadow Dom(影子元素)
3. HTML templates(HTML模版)
Custom elements
自定义元素是在js中通过继承HTMLElement或现有的HTML DOM对象来实现的。
class CustomElement extends HTMLElement {
...
}
生命周期:
- connectedCallback(挂载时): 当自定义元素第一次被连接到文档DOM时被调用。
- disconnectedCallback(卸载时): 当自定义元素与文档DOM断开连接时被调用。
- adoptedCallback(移动时): 当自定义元素被移动到新文档时被调用。
- attributeChangedCallback(属性变化时): 当自定义元素的一个属性被增加、移除或更改时被调用。
Shadow Dom
shadow dom与普通的document对象几乎一样,它是专门用来操作自定义的html元素,它也是一个树形结构,但是shadow dom完全独立于普通的dom,相当于是一个隔离区,需要把它挂载到一个普通的dom节点上。
HTML templates
html 模版是方便于编写自定义元素的html结构和css样式。它包括两个标签:template和slot。这里的slot与vue中的slot类似,用于指定一些占位的插槽,在外边可以用真实的元素替换掉。
<template>
<style>
</style>
<div>
<h1></h1>
<slot name="content"></slot>
</div>
</template>
举例
<blog-post>
- index.html定义模版
<template id="blog-post-template">
<div>
<h1>博客标题</h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
</template>
- 在BlogPost.js中定义BlogPost类
class BlogPost extends HTMLElement {
constructor() {
// 调用父类的构造函数才能初始化
super()
const template = document.getElementById('blog-post-template')
// attachShadow获取shadow dom的根元素,mode为open意思是允许通过shadow dom的api来操作和访问该自定义元素内部的dom树。使用appendChild来添加到根元素中。这里cloneNode ,这样子可以使得多次使用自定元素时,内容都是独立的。
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
)
}
}
// 将BlogPost注册到自定义元素注册表中,这里的名字必须带有中划线,目的是和原生的html元素区分开
customElements.define("blog-post", BlogPost)
- 在index.html使用
<body>
<blog-post>
<article slot="content">这是博客内容</article>
</blog-post>
<!-- type 必须为 module,否则变量名会冲突 -->
<script src="BlogPost.js" type="module"></script>
</body>
- 设置自定义的title属性
index.html:
<blog-post title="博客标题">
<article slot="content">这是博客内容</articel>
</blog-post>
BlogPost.js
class BlogPost extends HTMLElement {
constructor() {
// ...
// this.shadowRoot是shadowDom中的根元素,它要在调用this.attachShadow之后才能使用。其中的api和document中的几乎一样。
this.titleEle = this.shadowRoot.querySelector("h1")
}
static get observedAttributes() {
return ["title"]
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "title") {
this.titleEle.textContent = newValue
}
}
}
- 设置样式
在template中使用style标签编写样式: 在template中写的样式,只会应用到template内部的元素中。
<template>
<style>
div {}
h1 {}
button {}
</style>
<div>
<h1></h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
</template>
- 在js中定义template
在html中定义多个template会占用大量的空间和代码,不好维护。可以使用在js中定义template。
const template = document.createElement("template")
template.innerHTML = `
<style>
...
</style>
<div>
<h1></h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
`
class BlogPost extends HTMLElement {
constructor() {
super()
// const template = document.getElementById('blog-post-template')
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
)
}
}
<post-list>
- 定义模版、创建PostList类
const template = document.createElement("template")
template.innerHTML = `
<style>
div {
...
}
article {
...
}
</style>
<div></div>
`
class PostList extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);
}
}
customElements.define("post-list", PostList);
- 加载数据、创建博客列表
class PostList extends HTMLElement {
constructor() {
...
}
async connectedCallback() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts")
const posts = await res.json()
this.initPosts(posts)
}
// 创建博客列表
initPosts(posts) {
const div = this.shadowRoot.querySelector("div")
posts.forEach(post => {
const blogPostEle = div.appendChild(document.createElement("blog-post"))
// 博客标题
blogPostEle.setAttribute("title", post.title)
// 博客文章
const article = blogPostEle.appendChild(
document.createElement("article")
)
article.slot = "content"
article.innerHTML = post.body
})
}
}
customElements.define("post-list", PostList)
- 给添加事件
class BlogPost extends HTMLElement {
constructor() {
// ...
this.articleSlot = this.shadowRoot.querySelector("slot")
this.content = ""
this.article = null // 替换后的元素对象
}
slotChange() {
// assignedElements获取真实的替换元素数组
const elements = this.articleSlot.assignedElements()
const article = elements[0]
// 真实的article元素保存到article属性中
this.article = article
// 保存博客全文到content属性中
this.content = this.article.innerHTML
// 把article中的博客全文改成摘要
this.article.innerHTML = this.getExcept()
}
getExcept() {
return this.content.slice(0, 60) + "..."
}
connectedCallback() {
this.articleSlot.addEventListener("slotchange", this.slotChange.bind(this));
}
}
- 给按钮添加点击事件,维护显示/隐藏状态
class BlogPost extends HTMLElement {
constructor() {
// ...
this.buttonEle = this.shadowRoot.querySelector("button")
this.showFullArticle = false
}
/**
* 按钮点击事件,控制是否显示全文。
*/
toggleFull() {
this.showFullArticle = !this.showFullArticle
if (this.showFullArticle) {
this.article.innerHTML = this.content
this.buttonEle.textContent = "隐藏全文"
} else {
this.article.innerHTML = this.getExcept()
this.buttonEle.textContent = "查看全文"
}
}
connectedCallback() {
// ...
this.buttonEle.addEventListener("click", this.toggleFull.bind(this))
}
disconnectedCallback() {
// 卸载事件监听,在组件销毁的时候,释放内存
this.buttonEle.removeEventListener("click", this.toggleFull())
this.articleSlot.removeEventListener("slotchange", this.slotChange)
}
}
总结
创建自定义元素的步骤:
- 编写模版代码和样式
- 创建自定义元素class,继承HtmlElement
- 使用CustomeElement.define() 注册元素
- 在构造函数中使用super() 调用父类构造函数,并编写初始化逻辑
- 使用生命周期加载数据、注册监听和卸载监听
React与Web Component互相调用
Web Component可以在React中使用。但因为React有自己的模块化机制(Component),以及自己的事件系统(SyntheticEvent), 考虑到调用方式和事件系统的统一,官方推荐将web component包装为react component。
class HelloMessage extends React.Component{
render() {
return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
}
}
React模块也可作为Web Component使用。只需在attachedCallback中调用ReactDOM.render。
class XSearch extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement('span');
this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
customElements.define('x-search', XSearch);
文中有什么错误或者不足之处,欢迎大家指正...
网友评论