WebComponent是个什么东西?

WebComponent是个什么东西?

web component是官方定义的自定义组件实现方式,它可以让开发者不依赖任何第三方框架(如Vue,React)来实现自定义页面组件;达到组件复用效果。

怎么使用webcomponent来实现自定义组件

接下来,我会用一步一步实现一个简单的自定义组件,该组件包含一个button和ul列表,点击按钮会动态添加列表项。

先来看下最终的效果

webComponent05.gif

定义和使用web component三部曲

  1. 继承HTMLElement实现自定义类

    class MyButton extends HTMLElement {
        constructor() {
            super();
        }
    }
    复制代码
  2. 定义组件模板

    <template>
    </template>
    复制代码
  3. 注册自定义元素

    window.customElements.define(name, ComponentClass)
    复制代码

来完成一个最简单的自定义组件

  1. 定义自定组件

    class MyButton extends HTMLElement {
        constructor () {
            super();
            const template = document.getElementById('mybutton');
            const content = template.content.cloneNode(true);
            this.appendChild(content);
        }
    }
    复制代码
  2. 定义组件模板

    <template id="mybutton">
        <button>Add</button>
    </template>
    复制代码
  3. 注册组件

    window.customElements.define('my-button', MyButton);
    复制代码
  4. 使用组件

    <body>
        <my-button></my-button>
    </body>
    复制代码

通过以上四个步骤,就可以定义一个自定义组件并成功渲染到页面。

最终效果如下

webcomponent01.png

总结一下这一小节的知识点

  • 使用es6类继承HTMLElement类,而且要在子类的构造函数中调用super(),在构造函数中获取template内容,并添加在当前实例中
  • 定义组件模板,使用<template>标签
  • 通过全局api window.customElements.define()注册自定义组件,不然这个组件是无法使用
  • 像使用一般标签元素一样使用自定义组件

有一点需要注意的,const content = template.content.cloneNode(true);这一行代码深度复制了一份模板,这是因为这个模板可能在多个地方被使用,如果直接使用模板本身,就会相互影响。

以下是完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Web Component</title>
</head>
<body>
  <my-button></my-button>
  <template id="mybutton">
    <button>Add</button>
  </template>
</body>
<script>

  class MyButton extends HTMLElement {
    constructor () {
      super()
      const template = document.getElementById('mybutton');
      const content = template.content.cloneNode(true);
      this.appendChild(content);
    }
  }

  window.customElements.define('my-button', MyButton)
</script>
</html>
复制代码

接下来我们一步一步的完善和丰富这个自定义组件。

给组件添加样式

我们在上面仅仅定义了一个组件的模板骨架,并没有使用CSS来修饰我们的组件,在template中可以为组件添加样式。

<style>
  button {
    width: 60px;
    height: 30px;
    cursor: pointer;
    color: blue;
    border: 0;
    border-radius: 5px;
    background-color: #F0F0F0;
  }
</style>
<template>
	<button>
        Add
    </button>
</template>
复制代码

添加样式后的自定义按钮

webcomponent02.png

怎么接收外部参数?

一个组件如果仅仅是具备上面的能力,那么复用性就会很差,因为它无法根据参数动态变化;WebComponent可以接收外部参数,那么开发者就能根据参数来对组件进行不同的处理与渲染。

  1. 在自定义组件上添加需要传递的属性
<body>
    <my-button
    	text="Hello"           
    ></my-button>
</body>
复制代码
  1. 自定义组件类中获取属性值,并按需使用
class MyButton extends HTMLElement {
    constructor () {
        super()
        const template = document.getElementById('mybutton');
        const content = template.content.cloneNode(true);
+       const text = this.getAttribute('text');
+       content.querySelector('#btn').innerText = text;
        this.appendChild(content);
    }
}
复制代码

添加到组件上的属性可以通过this.getAttribute(name)获取。我们可以给组件添加任意多的属性。

最终渲染的结果如下

webcomponent03.png

怎么给外部发送参数?

如果组件需要和外部交互,就需要具备向外部发送数据的能力。

有两种方式,1)在自定义组件上添加自定义方法,并在组件内监听DOM元素事件,在需要的地方调用组件的自定义方法;2)使用元素的自定义事件

1) 添加自定义方法

class MyButton extends HTMLElement {
    constructor () {
        super()
        const template = document.getElementById('mybutton');
        const content = template.content.cloneNode(true);
        const button = content.querySelector('#btn');
        const text = this.getAttribute('text');
        button.innerText = text;
+       button.addEventListener('click', (evt) => {
+           this.onClick("Hello from within the Custom Element");
+       })
        this.appendChild(content);
    }
}
复制代码
document.querySelector('my-button').onClick = value => {
    console.log(value);
}
复制代码

2)使用元素自定义事件

  class MyButton extends HTMLElement {
    constructor () {
      super()
      const template = document.getElementById('mybutton');
      const content = template.content.cloneNode(true);
      const button = content.querySelector('#btn');
      const text = this.getAttribute('text');
      button.innerText = text;
      button.addEventListener('click', (evt) => {
+       this.dispatchEvent(
+        new CustomEvent('onClick', {
+           detail: 'Hello fom within the Custom Element'
+        })
        )
      })
      this.appendChild(content);
    }
  }
+	document.querySelector('my-button').addEventListener('onClick', (value) => 			
+	console.log(value));
复制代码

可以像使用Vue插槽slot一样定义原生slot

现在来思考以下,能不能像使用Vue的slot一样,在webComponent中使用插槽呢?同样也是可以的,实际上,Vue是参考了webComponent的规范实现slot的。下面就看看如何在web component中使用slot。

首先要在模板里定义一个slot标签插槽占位,而且插槽可以添加默认显示内容

<template id="mybutton">
    <style>
        button {
            color:blue;
            border: 0;
            border-radius: 5px;
            background-color: brown;
        }
    </style>
    <button id="btn">My Button</button>
+   <p><slot name="my-text">My Default Text</slot></p>
</template>
复制代码

然后使用组件的时候,给内容添加slot属性,指定需要填充的slot

<my-button>
+  <p>Another Text from outside</p>
</my-button>
复制代码

最后渲染出来的效果如下

webcomponent04.png

监听属性变化

在“怎么接收外部参数?”一节,我们知道怎么给组件传递数据;如果传递进来的数据发生变化了,我们期望的结果是组件内容也会随之变化,和Vue数据绑定一样;在web component中也能实现类似的功能。

先来认识三个新函数

  1. static get observedAttributes()
  2. attributeChangedCallback(name, oldVal, newVal)
  3. render()

第一个函数是静态get函数,它的作用是定义那些属性需要被监听;

第二个函数是属性变化时的回调函数,也就是说,每一个被监听的属性,只要属性值发生变化,都会调用这个函数;

第三个函数是渲染函数,属性更新后,如果要重新渲染组件,就要调用这个函数。

下面就来实现如何监听属性并更新组件(以下所有函数都是添加到自定义组件类中,也就是本文例子的MyButton类)

  1. 定义需要监听的属性
static get observedAttributes() {
    return ['text'];
}
复制代码
  1. 定义回调函数
attributeChangedCallback(name, oldVal, newVal) {
    this[name] = newVal;
    this.render();
}
复制代码
  1. 重新渲染
render() {
    this.$button.innerText = this.text;
}
复制代码

this.$button需要在构造函数中定义

完整的类定义如下

class MyButton extends HTMLElement {
    constructor () {
        super()
        const template = document.getElementById('mybutton');
        const content = template.content.cloneNode(true);
        const button = content.querySelector('#btn');
        const text = this.getAttribute('text');
+       this.$button = button;
        button.innerText = text;
        button.addEventListener('click', (evt) => {
            this.dispatchEvent(
                new CustomEvent('onClick', {
                    detail: 'Hello fom within the Custom Element'
                })
            )
        })
        this.attachShadow({ mode: 'open' }).appendChild(content);
    }
+   static get observedAttributes() {
+       return ['text'];
+   }
+   attributeChangedCallback(name, oldVal, newVal) {
+       this[name] = newVal;
+       this.render();
+   }
+   render() {
+       this.$button.innerText = this.text;
+   }
}
复制代码

可以通过代码动态改变自定义组件上的属性值,下面通过在Chrome devtool来模拟一下

webComponent06.gif

定义属性的set、get函数

在上面的例子中,有两个不是很方便的点,1)每次属性更新,都需要在attributeChangedCallback函数中手动赋值;2)属性只能添加到DOM元素上;我们希望能直接在JavaScript代码里像给对象赋值一样改变属性值。

给每一个需要被监听的属性添加get函数

get text () {
    return this.getAttribute('text');
}
复制代码

再定义属性的set函数

set text (value) {
    this.setAttribute('text', value);
}
复制代码

现在我们就能直接给组件对象定义属性并赋值,而不需要定义在组件DOM元素上。如下

const mybutton = document.querySelector('my-button');
mybutton.text = 'Hi!';
复制代码

生命周期

除了上面提到的知识点,我们还需要了解一下web component的生命周期,可以在自定义组件的不同时期做我们需要的操作。

  1. connectedCallback

当web component被添加到DOM时,会调用这个回调函数,这个函数只会被执行一次。可以在这个回调函数中完成一些初始化操作,比如更加参数设置组件的样式。

  1. disconnectedCallback

当web component从文档DOM中删除时执行。

  1. adoptedCallback

当web component被移动到新文档时执行。

  1. attributeChangedCallback

被监听的属性发生变化时执行。

下面是完整的代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <my-button>
    <p slot="my-text">Another text from outside</p>
  </my-button>
  <template id="mybutton">
    <style>
      button {
        width: 60px;
        height: 30px;
        cursor: pointer;
        color: blue;
        border: 0;
        border-radius: 5px;
        background-color: #F0F0F0;
      }
    </style>
    <div>
      <button id="btn">Add</button>
      <p><slot name="my-text">My Default Text</slot></p>
    </div>
  </template>
</body>
<script>

  class MyButton extends HTMLElement {
    constructor () {
      super()
      const template = document.getElementById('mybutton');
      const content = template.content.cloneNode(true);
      const button = content.querySelector('#btn');
      this.$button = button;
      button.addEventListener('click', (evt) => {
        this.dispatchEvent(
          new CustomEvent('onClick', {
            detail: 'Hello fom within the Custom Element'
          })
        )
      })
      this.attachShadow({ mode: 'open' }).appendChild(content);
    }
    get text () {
      return this.getAttribute('text');
    }
    set text (value) {
      this.setAttribute('text', value);
    }
    static get observedAttributes() {
      return ['text'];
    }
    attributeChangedCallback(name, oldVal, newVal) {
      this.render();
    }
    render() {
      this.$button.innerText = this.text;
    }
  }
  const mybutton = document.querySelector('my-button');
  mybutton.addEventListener('onClick', (value) => {
    console.log(value)
    mybutton.text = value
  });
  window.customElements.define('my-button', MyButton)
</script>
</html>
复制代码

止步于此?

通过上面的步骤,我们已经把web component的必须知识点都过了一遍;不过我们是直接把组件定义在HTML文档里,这样在实际项目中无法达到复用的效果,因为我们不可能把代码直接些到其他人的页面上,而是通过引用的方式来调用,所以我们要实现组件模块化。

可以把web component定义在一个js文件中,然后通过script标签被其他人引用。下面就来对前面的组件进行改造。

使用模板字符串定义组件模板、编写自定义组件类、注册,新建一个MyButton.js文件

完整的代码如下

const template = document.createElement('template');
template.innerHTML = `
  <style>
  button {
    width: 60px;
    height: 30px;
    cursor: pointer;
    color: blue;
    border: 0;
    border-radius: 5px;
    background-color: #F0F0F0;
  }
  </style>
  <div>
  <button id="btn">Add</button>
    <p id="message"><slot name="my-text">My Default Text</slot></p>
    <ul id="text-list"></ul>
  </div>
`;
const Texts = [
  'My lady, Hello!',
  'BuiBuiBui',
  'BiliBili',
  'Haiwei is NO.1'
]
class MyButton extends HTMLElement {
  constructor () {
    super()
    const content = template.content.cloneNode(true);
    const button = content.querySelector('#btn');
    const textList = content.querySelector('#text-list');
    this.$button = button;
    this.$message = content.querySelector('#message');
    button.addEventListener('click', (evt) => {
      const li = document.createElement('li');
      li.innerText = Texts[Math.floor(Math.random() * 4)];
      textList.appendChild(li);
      this.dispatchEvent(
        new CustomEvent('onClick', {
          detail: 'Hello fom within the Custom Element'
        })
      )
    })
    this.attachShadow({ mode: 'open' }).appendChild(content);
  }
  get text () {
    return this.getAttribute('text');
  }
  set text (value) {
    this.setAttribute('text', value);
  }
  static get observedAttributes() {
    return ['text'];
  }
  attributeChangedCallback(name, oldVal, newVal) {
    this.render();
  }
  render() {
    this.$message.innerText = this.text;
  }
}

window.customElements.define('my-button', MyButton)
复制代码

在文档中调用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./MyButton.js"></script>
</head>
<body>
  <my-button> 
    <p slot="my-text">Another text from outside</p>
  </my-button>
</body>
<script>
  const mybutton = document.querySelector('my-button');
  mybutton.addEventListener('onClick', (value) => {
    console.log(value)
    mybutton.text = value.detail
  });
</script>
</html>
复制代码

到这里,我们就完成了一个可供复用的web component了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享