Web Components 详细入门教程

3,317 阅读5分钟

引用 MDN 的话:Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。

简单来说就是官方定义的自定义组件的方式,封装代码,提高代码的复用性。

基本用法

  1. 首先定义内容模板 template ,可以把它理解为一个内容片段,稍后使用 JS 进行实例化。解析器虽然会处理这个片段,但是不会渲染在页面上。
<template id="myTemplate">
  <h1 id="title">这是一个大标题</h1>
</template>
  1. 再使用该模板,定义一个类继承自 HTMLElement ,复制该模板并 append ,这里之所以用 cloneNode 复制是为了防止该组件被多处使用时,修改了一处,其他地方也跟着变化,参数为 true 表示递归克隆子元素。
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.querySelector('#myTemplate')
    const content = template.content.cloneNode(true)
    this.appendChild(content)
  }
}
  1. 注册自定义元素,使用 customElements 上的 define 方法。
window.customElements.define('my-component', MyComponent)
  1. 使用自定义元素。为了区别与非自定义组件,自定义组件名称通常以“-”分隔。
<my-component id="myComponent"></my-component>

此时可以看到页面呈现了刚才定义的内容:

image.png

添加样式

直接在模板中添加 style 标签:

<template id="myTemplate">
  <style>
    h1 {
      color: red
    }
  </style>
  <h1>这是一个大标题</h1>
</template>

刷新页面,文字变红:

image.png

自定义属性

为了提高可复用性,组件中需要根据传入参数展示不同的内容。

<my-component id="myComponent" title="这是传入的标题"></my-component>

传入一个 title 属性,用 this.getAttribute 方法可以取到该属性。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.querySelector('#myTemplate')
    const content = template.content.cloneNode(true)
    const title = content.querySelector('#title')
    const propTitle = this.getAttribute('title')
    title.innerText = propTitle
    this.appendChild(content)
  }
}

此时页面中标题的内容已变为传入的属性:

image.png

与父组件通信

其实自定义属性就是父子组件通信的方式,子父组件通信可以用自定义事件来实现。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.querySelector('#myTemplate')
    const content = template.content.cloneNode(true)
    const title = content.querySelector('#title')
    const myEvent = new CustomEvent('myEvent', {
      detail: '这是子组件传过来的消息'
    })
    title.addEventListener('click', () => {
      this.dispatchEvent(myEvent)
    })
    this.appendChild(content)
  }
}

自定义了一个 myEvent 事件(至于如何自定义事件就不详细展开了),注意携带的消息一定要放在 detail 属性中,放在其他属性会找不到,如果数据很多的话 detail 也可以是一个对象。当点击标题的时候把该事件 dispatch 出去。

const myComponent = document.querySelector('#myComponent')
myComponent.addEventListener('myEvent', val => {
  console.log(val)
})

myComponent 监听该事件,作出对应的处理。

当点击标题时,打开控制台可以看到该事件的详细信息,刚才传入的信息在 detail 属性中保存。

image.png

shadow DOM

当封装一个组件时,组件内部是不应该可以从外部修改的,但是我们现在封装的组件很明显可以在外部修改,在控制台输入 document.getElementById('title').innerText = '我要修改大标题' 发觉标题文字直接被改掉了:

image.png

这不符合预期,需要将结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起。这里就要用到 shadow DOMattachShadow() 方法来将一个 shadow root 附加到任何一个元素上,该方法返回一个 shadow root

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.querySelector('#myTemplate')
    const content = template.content.cloneNode(true)
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.appendChild(content)
  }
}

此时页面结构已经发生了变化:

image.png

多了一行 shadow-root(open)

再对标题进行修改直接报错,因为该 dom 节点已经变成了“shadow”:

image.png

CSS伪类

:host 该选择器可以选择组件的根元素。

:host {
  display: block;
  background: red;
}

在控制台中查看该自定义组件根节点的样式:

image.png

此时该组件背景变为了红色:

image.png

:defined 表示组件内任何已定义的元素。

:defined {
  font-style: red;
}

这时可以看到组件中的每一个元素都被添加了斜体样式:

image.png

页面上的文字也变为了斜体:

image.png

插槽

插槽可以将内容片段插入到指定的位置。插槽的内容由父组件控制,而插在哪里由子组件控制,提高了代码的复用性。

将需要插入的内容传入组件:

<my-component id="myComponent">
  <h2>这是副标题</h2>
</my-component>

slot 标签放在想要插入内容片段的位置:

<template id="myTemplate">
  <h1 id="title">这是一个大标题</h1>
  <slot></slot>
</template>

此时页面中有了副标题:

image.png

数据驱动视图

当传入的数据变化时,应当监听数据的变化,并对 dom 作出修改。

这里涉及到两个方法:

  1. static get observedAttributes 该静态方法需要返回一个数组,数组内是需要监听变化的属性名。
  2. attributeChangedCallback 当监听的属性变化时,会触发该函数,函数体内是监听的函数变化时需要执行的逻辑。
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.querySelector('#myTemplate')
    const content = template.content.cloneNode(true)
    const title = content.querySelector('#title')
    this.$title = title
    const propTitle = this.getAttribute('title')
    title.innerText = propTitle
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.appendChild(content)
  }

  static get observedAttributes() {
    return ['title']
  }

  attributeChangedCallback() {
    const propTitle = this.getAttribute('title')
    this.$title.innerText = propTitle
  }
}

给组件绑定一个点击事件,点击时修改传入的属性:

document.querySelector('#myComponent').addEventListener('click', e => {
  e.target.setAttribute('title', '传入的标题被修改了')
})

当点击标题时,标题的内容改变了:

image.png

生命周期

当组件在页面中被插入和移除时,有对应的处理函数去处理对应的逻辑:

  • constructor 当组件被创建时
  • connectedCallback 当组件被插入到 dom 中时调用
  • disconnectedCallback 当组件从 dom 中被移除时调用
  • adoptedCallback 当自定义元素移动到新文档时调用
  • attributeChangedCallback 当传入属性改变时调用
  • errorCallback 错误处理函数

写在后面

由于有些功能只有在 shadow DOM 中才会生效,例如 slot 插槽 、 :host 选择器等,所以在封装自定义组件时建议始终使用 shadow DOM

这些基本是 Web Components 最常用的一些功能了,用过 Vue 的朋友一定发现这些概念和用法都太像了,确实,Vue 在编写时参考了 Web Components 规范。最近这几年关于 Vue 的争议始终不断,经常能看到关于它模仿其他框架的评论。在我看来这也只是站在了巨人的肩膀上,取其精华去其糟粕,其实这也是它能够如此流行的原因,技术没有高低,好用就行了。