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添加监听事件