跳到主要内容位置

Web Components 前端开发入门教程

Web components 是原生的组件化开发技术,它可以让我们创建自定义的 HTML 元素,并且功能和样式都会封装在组件内部,不影响其它的元素。Web Components 与现有的 React 和 Vue 等库不冲突,而是相辅相成的。

Web Components 的构成

Web Components 里有三个概念,分别是自定义元素,Shadow DOM(影子 DOM) 和 HTML 模板。其中:

  • 自定义元素是通过在 JavaScript 中继承 HTMLElement 或者现有的HTML DOM 对象来实现的。自定义元素有 4 个生命周期钩子,分别为 connectedCallback()、disconnectedCallback()、adoptedCallback()、attributeChangedCallback(),分别会在元素挂载时、卸载时、移动时、属性变化时调用。
  • Shadow DOM 与普通的 document 对象几乎一样,但是专门用来操作自定义的 HTML 元素的,它也是一个树形结构,但是 Shadow DOM 完全独立于普通 DOM,相当于是一个隔离区,需要把它挂载到一个普通的 DOM 节点上。
  • HTML 模板是为了方便编写自定义元素的 HTML 代码和 CSS 样式的,它包括两个标签 <template /><slot />,其中 <slot /> 与 Vue 中的 slot 类似,用于指定一些占位的插槽,后边可以用真实的元素替换掉。

示例

在了解了 Web Components 的基本概念之后,我们来看一个示例,这个示例将分别定义两个元素,一个是显示博客详情的 blog-post 元素和显示博客列表的 post-list 元素。首先创建一个项目目录,然后在里边创建一个 index.html 文件、一个 BlogPost.js 文件和一个 PostList.js 文件,在 index.html 文件中分别引入它们,并设置 type 为 module,这样两个 js 文件的代码就会变成模块化,从而不会冲突。

  <body>
<!-- type 必须为 module,否则变量名会冲突 -->
<script src="BlogPost.js" type="module"></script>
<script src="PostList.js" type="module"></script>
</body>

博客详情元素

先看博客详情元素,它会展示博客标题、博客文章和查看全文按钮,那么我们可以在 index.html 中先定义它的模板:

<template id="blog-post-template">
<div>
<h1></h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
</template>

template 标签给它设置了一个 id 属性,稍后会引用它,里边的 h1 用于显示标题,它会通过属性传递进来,<slot /> 则用于显示文章内容,这里可以使用任意元素来替换它,只需要和 slot 的 name 属性值相同即可。

在 BlogPost.js 中,定义一个 BlogPost class,并继承 HTMLElement 元素,这样就创建了一个独立的自定义元素,还可以通过继承类似 HTMLParagraphElement 这样的 DOM 对象来扩展现有的 p 元素。

class BlogPost extends HTMLElement {
constructor() {
super();
const template = document.getElementById("blog-post-template");
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);
}
}
customElements.define("blog-post", BlogPost);

BlogPost 类中的构造函数用于编写自定义元素的一些功能逻辑,它必须调用 super() 父类的构造函数才能正确初始化。之后获取 html 中的 template 标签,并调用 attachShadow 来获取 Shadow DOM 的根元素, mode 设置为 open,意思是允许通过 Shadow DOM 的 API 来操作和访问该自定义元素内部的 DOM 树。之后调用 appendChild() 把 template 的内容添加到根元素中,注意这里使用了 cloneNode() 复制了 template 的内容,这样多次使用 BlogPost 自定义元素时,内容都是独立的。最后调用 customElements.define() 把 BlogPost 元素注册到自定义元素注册表中,这里的名字必须带有中画线,目的是和原生 HTML 元素区分开,第 2 个参数把 BlogPost 类传递进去。

这样定义好后就可以直接在 HTML 中使用了,在 index.html 中,可以用这样的代码来使用 <blog-post />,article 元素通过设置 slot 属性值为 "content",就能把模板中 name 属性为 "content" 的 slot 替换掉:

<blog-post>
<article slot="content">这是博客内容</article>
</blog-post>
content

设置标题属性

不过这样没办法指定博客标题,我们接下来让 BlogPost 接收一个自定义的 title 属性来设置标题,那么先在 html 中添加上 title 属性:

<blog-post title="博客标题">
<article>这是博客内容</article>
</blog-post>

然后在 BlogPost 的构造函数中使用 this.shadowRoot.querySelector("h1") 来获取模板中的 h1 元素,这里 this.shadowRoot 是 shadow DOM 中的根元素,它在调用 this.attachShadow() 之后才可以使用,其中的 API 和 document 的几乎一样,可以使用 querySelector() 选择元素。

this.titleEle = this.shadowRoot.querySelector("h1");

接下来需要使用 attributeChangedCallback() 生命周期来给 h1 赋值,它接收 3 个参数,分别是属性名、旧值和新值,这里判断属性名如果为 title 则直接把新值赋给 h1:

attributeChangedCallback(name, oldValue, newValue) {
if (name === "title") {
this.titleEle.textContent = newValue;
}
}

为了让 attributeChangedCallback() 生效,需要自行设置要监听的属性,这里需要定义一个 static get observedAttributes() 方法,里边返回要监听的属性名数组,这里设置监听一个 title 属性:

static get observedAttributes() {
return ["title"];
}

这样 title 属性值就能放到 h1 元素中了。

content

设置样式

我们可以给 BlogPost 元素设置一下样式,直接在 <template /> 标签中使用 <style /> 标签编写样式:

<template id="blog-post-template">
<style>
div {}
h1 {}
button {}
</style>
<div>
<h1></h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
</template>

这里为了节省空间省略了 CSS 代码,都是一些简单的,可以在源代码中查看。在 <template> 中写的 css 样式只会应用到 template 内部的元素中,所以 CSS 选择器也只会选择其中的元素。添加样式后的效果类似于这样:

content

现在可以通过 chrome 开发者工具看一下源代码,应该显示的是类似于这样的:

content

定义 template 到 js 中

在 html 中定义多个 template 会占用大量的空间和代码,不好维护,而导入 HTML 的功能已经被定性为过时了,那么这样可以在 js 定义中模板代码,可以更好的封装自定义元素:

const template = document.createElement("template");
template.innerHTML = `
<style>
div {}
h1 {}
button {}
</style>
<div>
<h1></h1>
<slot name="content"></slot>
<button>查看全文</button>
</div>
`;

这里只需要创建一个 template 元素,然后把模板代码使用字符串的形式赋值给 innerHTML 属性即可,然后在构造函数中就不用获取 template 元素了,可以直接使用这个 template 对象:

this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);

之后再删掉 index.html 中相关的模板代码就可以了。

博客列表元素

接下来定义博客列表元素,博客列表元素会加载远程数据并使用循环展示多个博客详情元素。在 PostList.js 中编写基础代码,定义模板、添加到 Shadow DOM 中,注册 <post-list> 标签:

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);

<post-list> 元素只有一个 div 标签,里边的内容都是动态创建的。因为我们会使用 article 来替换 <blog-post/> 中的 <slot />,所以在样式中直接对 article 元素进行了美化,即使现在还没有 article 元素。

接着我们使用 connectedCallback() 生命周期方法,在 <post-list /> 元素挂载之后加载博客列表数据:

  async connectedCallback() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await res.json();
this.initPosts(posts);
}

生命周期方法可以定义为 async 的,这样就可以在里边使用异步操作了。最后调用了 initPosts() 来创建博客列表,看一下它的代码:

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;
});
}

这段代码进行了这些操作:

  • 获取模板中的 div 元素
  • 遍历 posts 数据,生成博客列表。
  • 每次遍历都给 div 元素添加一个新的 blog-post 自定义元素。
  • 设置 blog-post 元素的标题属性
  • 给 blog-post 元素添加 article 子元素,并设置 slot 属性为 "content",替换掉 blog-post 的 slot。
  • 把 article 的内容设置为博客文章内容。

之后在 Index.html 中,删除示例的 blog-post 元素,然后使用 <post-list> 元素:

<post-list />

这样就可以显示博客列表了:

content

假设有一个需求,博客列表默认只显示 60 个字符的摘要,点击查看全文才展示完整的文章,那么在 Web Components 中应该怎么做呢?这里就需要添加事件和维护一些状态了。

显示摘要

首先看默认显示摘要,这时需要先获取替换掉 slot 的真实元素,才能修改它的内容。在 BlogPost 构造函数中先获取原本的 slot 元素对象:

this.articleSlot = this.shadowRoot.querySelector("slot");

然后定义一个 content 属性保存全文、定义一个 article 属性保存替换后的元素对象:

this.content = "";
this.article = null;

之后需要监听 slot 的 slotchange 事件来获取真实的替换元素,这里在 connectedCallback() 生命周期中给 slot 添加一个事件监听器:

  connectedCallback() {
this.articleSlot.addEventListener("slotchange", this.slotChange.bind(this));
}

注意这里给监听器绑定了 this,来让它可以访问类中的属性。

监听器 slotChange 的操作是:

  • 通过 assignedElements() 获取真实的替换元素数组。
  • 这里因为我们知道只有一个 article 元素,所以直接取数组第 0 个元素
  • 把真实的 article 元素保存到 article 属性中,把博客全文保存到 content 属性中,
  • 再把 article 中的博客全文改成摘要。
  slotChange() {
const elements = this.articleSlot.assignedElements();
const article = elements[0];
this.article = article;
this.content = this.article.innerHTML;
this.article.innerHTML = this.getExcept();
}

获取摘要的方法直接返回了内容的前 60 个字符和 3 个点:

  getExcept() {
return this.content.slice(0, 60) + "...";
}

现在博客内容只显示摘要了,接下来需要给按钮添加点击事件来切换全文和摘要。

切换摘要与全文

在构造函数中获取 button 对象,然后定义 showFullArticle 状态,true 为显示全文,false 为显示摘要:

this.buttonEle = this.shadowRoot.querySelector("button");
this.showFullArticle = false;

接着定义一个事件处理方法 toggleFull():

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 = "查看全文";
}
}

这个方法每次在调用时会对 showFullArticle 的值进行取反,然后切换显示摘要和全文,并且也会改变按钮的文案。之后在 connectedCallback() 生命周期中给按钮注册事件,这里同样需要绑定 this:

this.buttonEle.addEventListener("click", this.toggleFull.bind(this));

这时再点击按钮就可以切换全文和摘要了。最后还可以在 disconnectedCallback() 生命周期中卸载事件监听,在组件销毁的时候释放内存:

  disconnectedCallback() {
this.buttonEle.removeEventListener("click", this.toggleFull());
this.articleSlot.removeEventListener("slotchange", this.slotChange);
}

好了,这个就是使用 Web Components 创建自定义元素的过程了,主要步骤为:

  • 编写模板代码和样式。
  • 创建自定义元素的 class,继承 HTMLElement。
  • 使用 customElements.define() 注册元素。
  • 在构造函数中使用 super() 调用父类构造函数,并编写初始化逻辑
  • 使用生命周期加载数据、注册监听和卸载监听。

示例代码可以在视频简介中的 Github 仓库中查看。如果觉得视频有帮助请三连,想优雅的学前端,请关注峰华前端工程师,感谢观看!

提示

一系列的课程让你成为高级前端工程师。课程覆盖工作中所有常用的知识点和背后的使用逻辑,示例全部都为工作项目简化而来,学完即可直接上手开发!

即使你已经是高级前端工程师,在课程里也可能会发现新的知识点和技巧,让你的工作更加轻松!

《React 完全指南》课程,包含 React、React Router 和 Redux 详细介绍,所有示例改编自真实工作代码。点击查看详情。

《Vue 3.x 全家桶完全指南与实战》课程,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。

《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。

《JavaScript 基础语法详解》本人所著图书,包含 JavaScript 全面的语法知识和新特性, 可在京东、当当、淘宝等各大电商购买