Web Components
Web Components区别于现在各种框架的组件,基于原生的API实现,最终展示出来的组件的不是"组合的HTML元素"而是我们定义的一个全新的"HTML元素"。
三个核心的技术
- Custom elements (自定义元素):一组JavaScript API,允许您定义custom elements及其行为。
- Shadow DOM(影子DOM):一组JavaScript API,用于封装"影子"DOM树附加到元素(与文档DOM分开呈现)并控制其关联的功能,通过这种方式可以保存元素功能私有,这样它们就可以脚本化和样式化,而不用担心与文档的其他部分发生冲突。(隐藏内部实现和结构)。
- HTML templates(HTML模板):
<template>和<slot>元素是你可以编写不呈现页面中显示的标记模板。然后它们可以作为自定义自定义结构的基础被多次被多次重用。(其实就是html和)
构建第一个 Web Components
- 定义一个新组件类,继承自
HTMLElement
class MyComponent extends HTMLElement {
constructor() {
super();
}
}类似的,每一个DOM元素有一个自己的类HTMLDivElement、HTMLSpanElement、HTMLParagraphElement...,每一个元素DOM都是它们的一个实例。我们操作DOM的方法都是在他们的类上定义的,所以类似的,我们可以给自定义DOM定义它们独有的属性和方法。
- 注册DOM元素 将我们新建的这个DOM类注册一个DOM名称,这样在HTML文档中就能识别对应的元素到我们定义的类上。
window.customElements.define('my-component', MyComponent);需要注意的是,这里的命名必须带全小写字母并且有-链接。 3. 将新元素加入到html中 使用我们注册的名称
<my-component></my-component>这时候可以查看DOM结构中已经出现了这个元素标签,但是因为我们没写内容,现在还是空白。
- 为新元素增加内容 这里我们创建一个带默认内容的button。
my-component和其余内置DOM同样是继承自HTMLElement,所以也能使用DOM的方法和属性,而在constructor中this指向这个元素的实例也就是这个DOM。所以我们可以这样:
constructor() {
super();
this.append('按钮')
// 等同于
// let textNode = document.createTextNode('text');
// this.appendChild(textNode)
}我们也可以向内部追加其他DOM元素
constructor() {
super();
this.append('按钮')
// 等同于
// let textNode = document.createTextNode('text');
// this.appendChild(textNode)
let divDom = document.createElement('div');
divDom.innerHTML = "div"
this.setAttribute('class','testclassname')
this.style.color = 'red'
this.append(divDom)
}所以我们可以使用dom的方法操作这个新元素,为添加结构和属性。
- 为元素添加事件
使用js正常监听事件就可以: 内部监听:
constructor() {
super();
this.append('按钮')
// 等同于
// let textNode = document.createTextNode('text');
// this.appendChild(textNode)
let divDom = document.createElement('div');
divDom.innerHTML = "div"
this.append(divDom)
this.setAttribute('class','testclassname')
this.style.color = 'red'
divDom.onclick = function () {
alert('div click')
}
this.onclick = function () {
alert('this click')
}
}外部监听:
<my-component id="my"></my-component>
document.getElementById('my').onclick=function(){
console.log('1')
}
document.querySelector('#my div').onclick=function(){
console.log('2')
}需要注意的是直接onclick只能绑定一个事件函数,所以内部的绑定的事件函数被替换掉了。如果需要绑定多个可以使用document.addEventListener。
- 隔离内部实现和外部实现
当我们给上面的代码加上了:
<style>
#my>div{
color: #000;
}
</style>和
document.querySelector('#my div').onclick=function(){
console.log('2')
}发现我们可以自由的操作新元素的内部实现,我们更希望是得到一个类似原生元素的标签。如果我们需要改变div的一些属性和结构,我们希望是通过div而不是直接被访问到内部的构造。 所以我们通过this.attachShadow({root:'open'})创建一个节点的root来包括内部实现的结构,隔断内外:
constructor() {
super();
var shadow = this.attachShadow({mode: 'open'});
shadow.append('按钮')
// 等同于
// let textNode = document.createTextNode('text');
// this.appendChild(textNode)
let divDom = document.createElement('div');
divDom.innerHTML = "div"
divDom.onclick = function () {
alert('div click')
}
shadow.append(divDom)
this.setAttribute('class','testclassname')
this.style.color = 'red'
this.onclick = function () {
alert('this click')
}
}此时你会发现: document.querySelector('#my div') 已经无法获取到这个元素,而样式设置也失效了。 此时DOM结构如下:
<my-component id="my" class="testclassname" style="color: red;">
#shadow-root(open)
按钮
<div>div</div>
</my-component>这种情况如果我们需要访问内部节点element.shadowRoot,
document.getElementById('my').shadowRoot.querySelector('div')如果需要禁止外部访问,设置this.attachShadow({mode: 'close'}),此时element.shadowRoot是null。
- 使用
template的html
<template id="MyComponentTemplate">
<style>
div{
color: green;
}
</style>
<div>
<span>2121</span>
haha
</div>
</template>和
var content = document.getElementById('MyComponentTemplate').content.cloneNode(true);
shadow.append(content)- 使用插槽
定义:
<template id="MyComponentTemplate">
<style>
div {
color: green;
}
</style>
<i class="desc">
<slot name="my-text">default slot</slot>
</i>
<div>
<span>2121</span>
haha
</div>
<slot></slot>
</template>使用
<my-component id="my">
<span slot="my-text">slot</span>
<div>qaq</div>
</my-component>没有声明的slot属性的内容(
<slot></slot>的位置。 如果没用使用attachShadow,标签内容- 向新元素内部传递参数
<my-component id="my" value="hahaha">
<span slot="my-text">slot</span>
<div>qaq</div>
</my-component>然后通过:
var value = this.getAttribute('value')
console.log(value)获取参数。
Web Components with React
React 和
Web Components为了解决不同问题而生.Web Components 为了可复用提供了强大的封装,而React则提供了声明式的解决方案。使DOM与数据保持同步。两者旨在互补。作为开发人员,可以自由选择Web Components中使用React,或者在React中使用Web Components,或者两者共存。
大多数开发在使用React的时候,不使用Web Components,但你可能会需要使用,尤其是在使用 Web Components编写的第三方UI组件时。
在React中使用Web Components
class HelloMessge extends React.Components{
render(){
return (
<div> Hello ,,<x-search> { this.props.name } <x-search /> </div>
)
}
}Web Components 通常暴露的是命令式 API。例如,Web Components 的组件 video 可能会公开 play() 和 pause() 方法。要访问 Web Components 的命令式 API,你需要使用 ref 直接与 DOM 节点进行交互。如果你使用的是第三方 Web Components,那么最好的解决方案是编写 React 组件包装该 Web Components。Web Components 触发的事件可能无法通过 React 渲染树正确的传递。 你需要在 React 组件中手动添加事件处理器来处理这些事件。
常见的误区是在 Web Components 中使用的是 class 而非 className。
function BrickFlipbox() {
return (
<brick-flipbox class="demo">
<div>front</div>
<div>back</div>
</brick-flipbox>
);
}在Web Components中使用React
class XSearch extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement('span');
this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
customElements.define('x-search', XSearch);注意:如果使用 Babel 来转换 class,此代码将不会起作用。请查阅该 issue 了解相关讨论。 在加载 Web Components 前请引入 custom-elements-es5-adapter 来解决该 issue。
