跳到主要内容

Web Components

备注

什么是Web Components

行走在新时代的我们面对前端开发身边围绕着各声各色的“组件化”,使用最多的比如 Vue 中的模版语法、React 中的JSX,都致力于将结构、样式、逻辑封装成一个组件,采用组件复用来提高开发效率。但是这些框架的组件化并不是真正的组件化,虽然我们写代码时确实是写的组件化代码,但经过编译后就不再是组件化了,比如在使用ElementUI 的时候我们会使用 组件,在经过编译过后显示在页面上的就会变成 div 等标签,这种就像是CSS预处理器。Web Components 不同的是,它是原生支持的组件化,不依赖任何库或框架以及各种编译打包工具就可以在浏览器中运行,并且其可以与MVVM框架共存。我们在使用 ElementUI 时,会或多或少受到其样式的影响,Web Components 可以完美避开这个问题,它可以将每个组件渲染在独立的 DOM 树中,天然支持模块间样式和逻辑的隔离。不会干扰页面上的其他代码。

注意看,下面使我们实现的一个简单 Web 组件在浏览器中的使用效果(是直接显示为自定义标签的诶~):

1676616483601

特性

完整demo代码

web component 由三项主要技术组成:

  • Custom element 一组 JavaScript API,可以定义 custom elements 及其行为,然后在用户界面中按照需要使用它们。

  • Shadow DOM 一组 JavaScript API,用于将封装的 Shadow DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

  • HTML template

    <template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

custom elements

  • 注册 custom elements

    // 定义一个名为 <a-b-c> 的组件
    customElements.define('a-b-c', class extends HTMLElement {})
    // 元素叫做 word-count,类对象是 WordCount, 继承自 <p> 元素
    customElements.define('word-count', WordCount, { extends: 'p' });

    /*参数说明*/
    1. 表示所创建的元素名称的符合 DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线。
    2. 用于定义元素行为的 类 。
    3. 可选参数,一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
  • 获取组件的构造函数

    // 获取 <a-b-c> 组件的构造函数
    customElements.get('a-b-c')
  • 升级为自定义元素

    // 创建一个 <a-b-c> 的元素,名为 el
    const el = document.createElement('a-b-c')
    // 升级 el 元素
    customElements.upgrade(el)
  • 自定义组件定义后

    // 当 <a-b-c> 组件定义后
    customElements.whenDefined('a-b-c').then(() => { /* 当 <a-b-c> 组件定义后的回掉函数 */ })
  • 生命周期函数

    • connectedCallback:当 custom element 首次被插入文档 DOM 时,被调用。
    • disconnectedCallback:当 custom element 从文档 DOM 中删除时,被调用。
    • adoptedCallback:当 custom element 被移动到新的文档时,被调用。
    • attributeChangedCallback: 当 custom element 增加、删除、修改自身属性时,被调用。
    constructor () {
    super()
    // 相当于 Vue 的 setup
    console.log('先运行构造函数')
    }
    // 当 custom element 首次被插入文档 DOM 时,被调用
    connectedCallback () {
    // 相当于 Vue 的 mounted
    console.log('再运行连接回调')
    }
    disconnectedCallback () {
    // 相当于 Vue 的 unmounted
    console.log('当删除组件时才会运行失联回调')
    }
    adoptedCallback () {
    // 当使用 document.adoptNode 后会触发该生命周期
    console.log('当使用 document.adoptNode 后会运行收养回调')
    }
    // 相当于 Vue 的 data
    static observedAttributes = ['id']
    // 也可以写成下面这样:
    // static get observedAttributes () { return ['id'] }

    // getter 和 setter 配合 attributeChangedCallback 打造属性特性联动同步
    get id () { return this.getAttribute('id') }
    set id (value) { this.setAttribute('id', value) }
    attributeChangedCallback (name, oldValue, newValue) {
    // 相当于 Vue 的 watch
    if (oldValue === newValue) return

    switch (name) {
    case 'id':
    console.log(`oldValue: ${oldValue}, newValue: ${newValue}`)
    }
    }

Shadow DOM

Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。 -- MDN

Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。不同的是,Shadow DOM 内部的元素始终不会影响到它外部的元素(除了 :focus-within)。

Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
Shadow tree:Shadow DOM 内部的 DOM 树。
Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
Shadow root: Shadow tree 的根节点。

img

<body>
<my-shadow></my-shadow>
<script>
customElements.define(
'my-shadow',
class extends HTMLElement {
constructor() {
super()
// 1. 创建 shadow root
const shadow = this.attachShadow({ mode: 'open' })

// 2. 创建 shadow DOM 结构
const wrapper = document.createElement('span')
wrapper.setAttribute('class', 'wrapper')
wrapper.innerHTML = '<h1>shadow DOM</h1>'

// 3. 将所创建的元素添加到 Shadow DOM 上
shadow.appendChild(wrapper)
}
}
)
</script>
</body>

HTML Template

  • template

    <template> 元素是一种保护客户端内容机制,该内容在加载页面时不会呈现,但随后可以在运行时使用 JavaScript 实例化。

    <body>
    <template id="my-paragraph">
    <p>My paragraph</p>
    <style>
    p {
    color: white;
    background-color: #666;
    padding: 5px;
    }
    </style>
    </template>
    <my-paragraph></my-paragraph>

    <script>
    customElements.define(
    'my-paragraph',
    class extends HTMLElement {
    constructor() {
    super()
    let template = document.getElementById('my-paragraph')
    let templateContent = template.content.cloneNode(true)

    this.appendChild(templateContent)
    }
    }
    )
    </script>
    </body>
  • slot

    • 使用模版我们只能传递一些文本变量,这很有局限性,于是 Web Components 引入了<slot>(插槽)的概念来增加编码的灵活度。
    • 我们可以使用 slot 来实现基于模版的部分自定义内容(标签、样式)的渲染,slot 插槽需要在 Shadow DOM 里才能生效。
    <body>   
    <my-slot>
    <span slot="my-text">Let's have some different text!</span>
    </my-slot>
    <script>
    // slot
    customElements.define(
    'my-slot',
    class extends HTMLElement {
    constructor() {
    super()
    let template = document.getElementById('my-paragraph')
    let templateContent = template.content.cloneNode(true)

    const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(
    templateContent.cloneNode(true)
    )
    }
    }
    )
    </script>
    </body>

特性兼容性&polyfill

兼容性

  • Custom Elements customElement兼容性
  • Shadow DOM shadowDom兼容性
  • HTML Template HTMLTemplate兼容性

我们通过上述数据可以看到目前的兼容性已经达到了96%以上

polyfill

如果对于上述的兼容还不够完美, 我们可以找到相关的 polyfill 来进行使用:Web Components Polyfills 使用了 polyfill 之后, 兼容程度是这样:

PolyfillEdgeIE11+Chrome*Firefox*Safari 9+*Chrome Android*Mobile Safari*
Custom Elements
Shady CSS/DOM

*表示浏览器的当前版本 现在加上了 polyfill 之后, 兼容性就比较稳妥了

Web Components 实现总结

  1. 创建一个类或函数来指定 web 组件的功能
  2. 使用 CustomElementRegistry.define() 方法注册您的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素
  3. 如果需要的话,使用 Element.attachShadow() 方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
  4. 如果需要的话,使用 <template><slot> 定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的 shadow DOM 中。
  5. 在页面任何您喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样。

相关拓展

下面列出几个基于 WebComponents 的框架:

  • MicroApp:是一款基于WebComponents的思想实现的高性能低成本微前端框架
  • LitElement: 是一个简单的基类,用于使用 lit-html 创建快速、轻量级的 Web Components
  • Omi:是 Web Components + JSX/TSX 融合为一个框架,小巧的尺寸和高性能,融合和 React 和 Web Components 各自的优势
  • Stencil:是一个用于构建可重用、可扩展的设计系统的工具链。生成可在每个浏览器中运行的小型、极快且 100% 基于标准的 Web Components。