Web Component 入门简介

2,514 阅读5分钟

Web Component

什么是Web Component

Web Component并非单一的技术,而是由一系列 W3C 定义的浏览器标准组成,使得开发者可以构建出浏览器原生支持的组件。这些标准包括:

  • HTML Templates(模板)and Slots(插槽) — 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口
  • Shadow DOM(影子节点) — 对标签和样式的一层 DOM 包装
  • Custom Elements(自定义元素) — 带有特定行为且用户自命名的 HTML 元素

关于Web Component的详细介绍及使用方法,可访问MDN - Web Component进行了解

为什么需要Web Component

通常我们编写通用的组件会不可避免地受限于使用的框架,每个框架都需要一套独立的组件库,例如Ant Design,除了其最初的React版本,又衍生出了Angular版本的NG-ZORRO,Vue版本的Ant Design Vue

可以想象angular和vue的开发者在等待组件库发布期间焦急的心情,基于某个特定框架开发一套组件库需要耗费大量人力,并且组件库版本和框架版本的强依赖也令开发者在升级时头痛不已,正因如此,W3C推动Web Component技术成为浏览器标准。Web Component的特点是一次开发,到处使用,开发者无需关注使用的框架,很好地解决了上述痛点

Shadow dom和Event

相关知识储备

Web component内部的事件有一部分可以穿透shadow dom边界:

  • Focus Events: blur, focus, focusin, focusout
  • Mouse Events: click, dblclick, mousedown, mouseenter, mousemove, etc.
  • Wheel Events: wheel
  • Input Events: beforeinput, input
  • Keyboard Events: keydown, keyup
  • Composition Events: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop, etc

React自己实现了一套高效的事件注册,存储,分发和重用逻辑,在DOM事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大化的解决了IE等浏览器的不兼容问题。与DOM事件体系相比,它有如下特点

  • React组件上声明的事件最终绑定到了document这个DOM节点上,而不是React组件对应的DOM节点。故只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销
  • React以队列的方式,从触发事件的组件向父组件回溯,调用它们在JSX中声明的callback。也就是React自身实现了一套事件冒泡机制
  • React有一套自己的合成事件SyntheticEvent,不同类型的事件会构造不同的SyntheticEvent
  • React使用对象池来管理合成事件对象的创建和销毁,这样减少了垃圾的生成和新对象内存的分配,大大提高了性能

可以穿透边界的event(未经手动dispatch)

定义一个web component组件user-card

customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<p>
      <button>Click me</button>
    </p>`;
    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

原生JS使用示例

<user-card onclick="alert("triggered")"></user-card> 

<user-card></user-card>   
document.onclick = e => alert("Outer target: " + e.target.tagName)  

React使用示例

<user-card onClick={e=>{handleClick(e)}></user-card>  

由于click事件穿透了边界并冒泡到了document节点,react也可以正确分发该类型事件,所以结果是两个示例中onclick方法均被触发

不能穿透的边界的事件(经过手动dispatch)

可以分为浏览器原生事件和自定义事件两种情况讨论 先定义一个web component组件my-modal,包含了一个button,一个input以及一个icon,dispatch三个事件 'change', 'close', 'remove'(前两个为浏览器原生事件,最后一个是自定义事件)

    customElements.define('my-modal', class extends HTMLElement {
        connectedCallback() {
            this.attachShadow({ mode: 'open' });
            this.shadowRoot.innerHTML =
                `<div>
                   <button>close modal</button>
                   <input type="text"/>
                   <span>remove</span>
                 </div>  
            `;
            const closeEvent = new Event("close", { "bubbles": true, "composed": true });
            this.shadowRoot.querySelector('button').onclick =
                e => {
                    this.shadowRoot.firstElementChild.dispatchEvent(closeEvent);
                }
            this.shadowRoot.querySelector('input').onchange =
                e => {
                    const changeEvent = new CustomEvent("change", { "bubbles": true, "composed": true ,detail:e.target.value});
                    this.shadowRoot.firstElementChild.dispatchEvent(changeEvent);
                }
            const removeEvent = new CustomEvent("remove", { "bubbles": true, "composed": true });
            this.shadowRoot.querySelector('span').onclick =
                e => {
                    this.shadowRoot.firstElementChild.dispatchEvent(removeEvent);
                }
        }
    });  

原生JS使用示例:

<html>
    <body>
        <my-modal onclose="alert('close')"></my-modal>
    </body>
    <script>
        document.querySelector('my-modal').addEventListener('remove',()=>{alert('remove')})
        document.querySelector('my-modal').onchange = (e)=>{alert(e.detail)}
    </script>  
</html>

React使用示例:

<my-modal onClose={()=>alert('close')}></my-modal>  
document.querySelector('my-modal').addEventListener('remove',()=>{alert('remove')})
document.querySelector('my-modal').onchange = (e)=>{alert(e.detail)}

React示例中由于close是原生事件,所以onClose合成事件能正常工作,事实上,大部分浏览器原生事件是可以被React正确分发的,但change事件例外(一部分是安全方面的原因),只能选择在dom节点上添加onchange事件。
PS. Angular的事件是挂在真实dom节点上,其模板引擎兼容html语法,并且为自定义事件自动绑定监听事件,因此对Web Component及其友好
Angular示例:

<my-modal (close)="()=>alert('close')" (change)="()=>alert('change')" (remove)="()=>alert('remove')" ></my-modal>   

事件使用总结

从使用者的角度出发,我们总结出以下web component事件交互使用规则:

  • 原生JS
    • 若xxx事件是浏览器原生事件,则添加onxxx属性到html组件元素或在dom节点添加onxxx监听事件均可
    • 若xxx事件是自定义事件,则需要在组件dom节点上通过addEventListener添加监听事件
  • React
    • 若xxx事件是浏览器原生事件,大部分情况下添加onXXX到组件的属性即可,change事件例外,需要在组件dom节点上通过addEventListener添加change监听事件
    • 若xxx事件是自定义事件,则需要在组件dom节点上通过addEventListener添加监听事件