likes
comments
collection
share

Web Components基础与应用

作者站长头像
站长
· 阅读数 5

前言

Web Component 目前包含三项主要技术,这三项技术可以单独或一起使用来创建封装功能的定制元素,可以在任何地方重用,不必担心代码冲突。

1. 初识Web Components

Web Components是一组基于HTML、CSS和JavaScript构建的组件。它们是HTML5中新增的标准之一,可以用于创建可重用的组件,并将它们组合在一起构建Web应用程序。

2. Web Components的主要优势

  • 可重用、可组合性:Web Components 可以轻松地创建可重用的代码块,这有助于减少重复代码并提高代码的可维护性。
  • 灵活性:Web Components 可以定制 HTML 标签,从而使 HTML 更加灵活和强大。也可以创建自定义的标签、组件和属性,以满足特定的需求。
  • 跨平台兼容性:Web Components 是 HTML5 的一个重要组成部分,因此它们在所有主流浏览器中都是支持的。我们可以轻松地编写跨平台的代码,从而提高代码的通用性和可移植性。
  • 组件化和模块化程度高:通过 Shadow DOM,可以将一个组件的 HTML、CSS 和 JavaScript 封装在一起,形成一个独立的模块。避免了样式冲突和命名空间污染,而不会影响到其他元素;将组件的实现细节隐藏起来,只暴露必要的接口,从而保护了组件的完整性和稳定性。
  • 互操作性:组件可以超越框架并用于不同技术栈的项目中,不需要考虑技术栈版本升级带来的不兼容问题。

3. Web Components包含的三项主要技术

  1. Custom elements(自定义元素):一组 JavaScript API,可用于定义自定义元素及其行为,然后可以根据需要在用户界面中使用这些元素。
  2. Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  3. HTML templates(HTML 模板): 使用<template> 和 <slot> 元素可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

实现web component的基本方法通常如下所示:

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

 

4. Web Components 组件的 4 个生命周期函数

准确来说应该称为:生命周期回调函数

  • connectedCallback:当组件第一次被添加到 DOM 文档后调用
  • disconnectedCallback:当组件从 DOM 文档移除后调用
  • adoptedCallback:当组件在 DOM 文档中被移动到其他 DOM 节点时调用
  • attributeChangedCallback:当组件的某个属性发生改变后调用。 这里的属性改变 包含:新增、移除、修改属性值 这 3 种情况

5. 如何使用Web Components?

要使用Web Components,需要在HTML中嵌入组件元素,并将它们用于构建Web应用程序。以下是一些使用Web Components的示例:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Web Components Example</title>
    </head>
   <body>
        <h1>自定义</h1>
        <dialog-element></dialog-element>
        <script>
            // 创建一个类或函数来指定web组件的功能
            class Dialog extends HTMLElement {
                constructor() {
                    // 调用父类的属性和方法
                    super();
                    const p = document.createElement('p');
                    p.textContent = 'Web Components Example Custom elements(自定义元素)';
                    // this表示自定义元素实例。	
                    this.appendChild(p);
                }
            }
            // 用 customElements.define() 方法注册自定义的元素 ,并且指定component名称,以及创建的类。
            customElements.define('dialog-element', Dialog);
        </script>
    </body>
</html>

注意点:

  • 自定义元素的名称,一个 DOMString 标准的字符串,为了防止自定义元素的冲突,必须是一个带短横线连接的名称(e.g. custom-tag)。这个也是 Vue 自定义组件命名推荐的使用方式。
  • Class 类必须调用 super()。
  • constructor。自定义元素构造器,包含组件的生命周期的定义。
<!DOCTYPE html>
<html>
    <head>
    <meta charset="utf-8">
    <title></title>
    <style>
      div{
        color: #f00;
        font-size: 80px
      }
    </style>
    </head>
    <body>			
        <h1>影子 DOM </h1>
        <div>test</div>		
        <host-element>
            <span slot="data">哈哈哈</span>
        </host-element>
        <script>
        // 创建一个类或函数来指定web组件的功能
        class HostElement extends HTMLElement {
            constructor() {
                super()
              // 将影子 DOM 树附加到指定元素,并返回对其 ShadowRoot 的引用。
              // mode是必传的值,代表是否允许外部访问和修改ShadowRoot的属性,
              // open表示开放(允许),closed表示关闭(不允许)。
                this.attachShadow({
                    mode: "open"
                })
              // this.shadowRoot.textContent = "host-element"
              this.shadowRoot.innerHTML = `<div>hello world <slot name="data"></slot></div>`
              this.addStyle()
            }
            addStyle() {
               const styleEle = document.createElement("style");
               //  :host伪类,指代自定义元素本身。
               styleEle.textContent = `
                :host {
                    font-size:20px;
                    color:lightblue;
                }
                div {
                    border:1px solid blue;
                    display:inline-block;
                    padding:20px;
                    border-radius:8px;
                    margin-top:20px;
                }`
                this.shadowRoot.appendChild(styleEle)
            }
        }
        customElements.define("host-element", HostElement)
        </script>
    </body>
</html>

Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 Shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样:

Web Components基础与应用

影子 DOM 术语

  • 影子宿主(Shadow host): 影子 DOM 附加到的常规 DOM 节点。
  • 影子树(Shadow tree): 影子 DOM 内部的 DOM 树。
  • 影子边界(Shadow boundary): 影子 DOM 终止,常规 DOM 开始的地方。
  • 影子根(Shadow root): 影子树的根节点

Reveal box 是一个自定义的 Web 组件(内联块元素)。它用另一个盒子隐藏一个盒子的内容。当用户将鼠标悬停在盒子上,顶部盒子将移开,以便用户可以看到隐藏盒子的内容。

Web Components基础与应用

详细链接: Reveal box

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Web Components Example</title>
  <style type="text/css">
  	img{
	  width: 300px;
	  margin-top:30px;
	}
  </style>
</head>
<body>
    <h1>使用 Templates</h1>
    <template>
      <div>
        <div>
           这是 template 标签内的子节点内容
        </div>	
        <img src="https://semantic-ui.com/images/avatar2/large/kristy.png"/>
        </div>
    </template>
    <script>		
        // 获取 template 元素
        const templateEle = document.querySelector("template");
        // 获取 template 元素包含的文档片段
        const content = templateEle.content;
        // content 可以当做正常的 document 来使用
        const node = content.querySelector("div");
        // 追加节点到当前文档 
        // 由于将 Templates 代码片段内部的 div 追加到了当前文档结构,
        // 所以 Templates 内部的 div 节点消失
        document.body.appendChild(node);
        // 避免修改内容模板内部的 DOM 结构,我们可以先克隆模板内部的元素节点,再将克隆的节点追到到当前文档
        // 导入 node 到 当前文档
        // 必须要有这一步
        // const cloneNode = document.importNode(node, true);
        // 也可以使用 cloneNode
        // const cloneNode = node.cloneNode(true);
        // document.body.appendChild(cloneNode);
    </script>
</body>
</html>

html 模版是方便于编写自定义元素的html结构和css样式。它包括两个标签:template和slot。这里的slot与vue中的slot类似,用于指定一些占位的插槽,在外边可以用真实的元素替换掉。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Web Components Example</title>
</head>
<body>
    <h1>使用 Templates</h1>
   <!--在模板中使用 slot 进行占位-->
   <template id="cardTmp">
       <div class="header">MY CARD</div>
       <div class="details">
           My name is <slot name="userName">test</slot>
       </div>
   </template>
   
   <!--在使用上面模板的自定义元素中给 slot 传值-->
   <my-card>
       <span slot="userName">插槽传值</slot>
   </my-card>
   
   <my-card>
       <span slot="userName">web Components</slot>
   </my-card>
   
   <my-card>
   </my-card>
   <script>
        class MyCard extends HTMLElement {
            constructor () {
                super();
                const template = document.getElementById('cardTmp');
                const templateContent = template.content;

                this.attachShadow({mode: 'open'}).appendChild(
                    templateContent.cloneNode(true)
                );
            }
        }
        customElements.define('my-card', MyCard);
    </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Components Template 传参示例</title>
</head>
<body>
    <!--相当于自定义一个属性message-->
    <hello-world message="好好学习"></hello-world>
    <br>
    <hello-world message="天天向上"></hello-world>
    <template id="hw-template">
      <p>Hello World</p>
      <style>
        p {
            padding: 10px;
            background-color: #f40;
            color: #fff
        }
      </style>
    </template>
</body>

<script>
  class HelloWorld extends HTMLElement {
     constructor() {
        super();
        const templateContent = document.querySelector('#hw-template').content
        const shadowRoot = this.attachShadow({
                mode: 'open'
        })shadowRoot.appendChild(templateContent.cloneNode(true))
        //获取属性
        const message = this.getAttribute('message')
        shadowRoot.querySelector('p').innerText = message
     }
  }
  customElements.define('hello-world',HelloWorld)
</script>

</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Components Click Event Example</title>
</head>
<body>
    <my-button id="myButton">点击我</my-button>
<script>
    class MyButton extends HTMLElement {
        constructor() {
            super();
            this.addEventListener('click', this.handleClick);
        }

        connectedCallback() {
            this.innerHTML = '点击我';
        }

        handleClick(event) {
            console.log('按钮被点击了');
        }
    }

    customElements.define('my-button', MyButton);

</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <template id="mybutton">
        <style>
          button {
            width: auto;
            height: 30px;
            cursor: pointer;
            color: blue;
            border: 0;
            border-radius: 5px;
            background-color: #F0F0F0;
          }
        </style>
        <button id="btn">Add</button>
        <slot name="my-text">My Default Text</slot>
    </template>

    <my-button text="hello">
      <p slot="my-text">Another Text from outside</p>
    </my-button>

<script>
   // WebComponent 实例
   class MyButton extends HTMLElement {
      constructor() {
        super();
        const template = document.getElementById('mybutton'); // 获取dom节点
        const content = template.content.cloneNode(true); // 深度克隆dom节点,使组件可以用在不同的地方并且相互之间不影响
        const text = this.getAttribute('text');
        const button = content.querySelector('#btn');
        this.$button = button; // 把按钮对象挂载到this上
        button.innerText = text;
        const event = new CustomEvent('onClick', { // 生成新的自定义事件️
            detail: 'Hello fom within the Custom Element', // 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值
            bubbles: false, // 表示该事件能否冒泡
            cancelable: false // 表示该事件是否可以取消
        })
        button.addEventListener('click', e => {
            this.dispatchEvent(event) // 在按钮被点击的时候,触发自定义事件
        })
        // this.appendChild(content); // 添加元素
        this.attachShadow({
                mode: 'open'
        }).appendChild(content);
     }

    // 静态get函数,它的作用是定义那些属性需要被监听
    static get observedAttributes() {
       return ['text'];
    }

    // 监听text属性变化
    get text() {
        return this.getAttribute('text');
    }

    // 赋值text属性
    set text(value) {
        this.setAttribute('text', value);
    }

    /**
     属性变化时的回调函数,也就是说,每一个被监听的属性,只要属性值发生变化,都会调用这个函数;
     */
    attributeChangedCallback(name, oldVal, newVal) {
        this.render();
    }

    // 渲染函数,属性更新后,如果要重新渲染组件,就要调用这个函数。
    render() {
        this.$button.innerText = this.text;
    }
 }
    window.customElements.define('my-button', MyButton);
    // 这俩需要带在调用方的组件里
    const mybutton = document.querySelector('my-button')
    mybutton.addEventListener('onClick', (value) => {
        console.log(value.detail);
        mybutton.text = value.detail
    });
</script>
</body>
</html>

6. Vue 与 Web Components

默认情况下,Vue 会将任何非原生的 HTML 标签优先当作 Vue 组件处理,而将“渲染一个自定义元素”作为后备选项。这会在开发时导致 Vue 抛出一个“解析组件失败”的警告。要让 Vue 知晓特定元素应该被视为自定义元素并跳过组件解析,我们可以指定 compilerOptions.isCustomElement

// 将所有标签前缀为 `ion-` 的标签视为自定义元素
app.config.compilerOptions.isCustomElement = (tag) => {
  return tag.startsWith('ion-')
}

详细链接:Vue 与 Web Components | Vue.js (vuejs.org)

7. 相关扩展

  1. MicroApp:是一款基于WebComponents的思想实现的高性能低成本微前端框架,提供了js沙箱样式隔离元素隔离路由隔离预加载数据通信等一系列完善的功能,做了诸多兼容,在任何前端框架中都可以正常运行。。
  2. Lit: 用于使用 lit-html 创建快速、轻量级的 Web Components。
  3. Omi:腾讯开源的前端跨框架跨平台的框架,是 Web Components + JSX/TSX 融合为一个框架,小巧的尺寸和高性能,融合和 React 和 Web Components 各自的优势。
  4. Stencil:由 Ionic 团队构建,是一个用于构建可重用、可扩展的设计系统的工具链。生成可在每个浏览器中运行的小型、极快且 100% 基于标准的 Web Components。
  5. Slim.js:开源的轻量级 Web Components 库,它为组件提供数据绑定和扩展能力,使用 es6 原生类继承。不依赖于其他框架,也提供了良好的拓展性,开发者可以自由拓展。
  6. Polymer:是 Google 推出的 Web Components 库,支持数据的单向和双向绑定,兼容性较好,跨浏览器性能也较好。
  7. X-Tag:是微软推出的开源库,支持 Web Components 规范,兼容Web Components。是一个开源 JavaScript 库,包装了 W3C 标准 Web Components API 系列,只需要自定义元素 API 支持即可运行。