以下是一个简单的 web component
<demo-component></demo-component>
<template id='demo-component'>
<style></style>
<div>
Web Component
</div>
</template>
<script>
window.customElements.define('demo-component', class extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'closed'})
const target = document.getElementById('demo-component')
const content = target.content.cloneNode(true)
shadow.appendChild(content)
}
})
</script>
但是通常在实际场景中,作为 component 肯定是要可抽象复用,由此带来的一个场景是我们在复用 components 的同时,也要通过一个接口(例如 React props)传入数据来显示不同数据。官方也提供了两种不同的方式 attribute & slot
以下是通过 attribute 实现,引用 component 时通过设置 attribute 方式传递数据,在实例化类中通过 this.getAttributes 获取设置的 attribute,带来的问题是会将数据都放在标签属性上。如果是数据是一个列表,则需要 decodeURI/encodeURI,过长的 attribute 对页面也不太友好
<demo-component demo-attr='Web Component'></demo-component>
<template id='demo-component'>
<style></style>
<div id='demo-attr'></div>
</template>
<script>
window.customElements.define('demo-component', class extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'closed'})
const target = document.getElementById('demo-component')
const content = target.content.cloneNode(true)
this.setAttr(content)
shadow.appendChild(content)
}
setAttr(content) {
const attr = this.getAttributes('demo-attr')
content.querySelector(`#demo-attr`).innerText = attr
}
})
</script>
另一种方式是通过 slot 插槽来控制 component 内部;但是这种方式引出的问题更多
最重要的一点是让 shadow DOM “失效”了,在外部能直接对传入的 slot 进行操作(当然也可能是官方故意为之),这样就失去了使用web component 的初衷;
其次 web component 专属的 ::slotted 选择器的权重比通配符(*)还要低,在外部设置的样式能直接更改 slot 自身的样式;
此外如果slot还存在子元素,子元素的样式没办法通过 ::slotted 选择器来控制(https://github.com/WICG/webcomponents/issues/594)
<style>
* {
color: #38f;
}
</style>
<demo-component>
<p slot='slot1'>
Web Component
<span>SLOT</span>
</p>
</demo-component>
<template id='demo-component'>
<style>
::slotted(p[slot=slot1]) {
color: #f40;
}
</style>
<div>
<slot name='slot1'></slot>
</div>
</template>
<script>
window.customElements.define('demo-component', class extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'closed'})
const target = document.getElementById('demo-component')
const content = target.content.cloneNode(true)
shadow.appendChild(content)
}
})
</script>
基于以上原因,实现 slotpro
<demo-component>
<div slotpro='slotpro-demo'>
<p>
Web Component
<span>SLOTPRO</span>
</p>
</div>
</demo-component>
<template id='demo-component'>
<style></style>
<div>
<slotpro name='slotpro-demo'></slotpro>
</div>
</template>
<script>
window.customElements.define('demo-component', class extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'closed'})
const target = document.getElementById('demo-component')
const content = target.content.cloneNode(true)
this.init(content)
shadow.appendChild(content)
}
init(content) {
[...content.querySelectorAll('slotpro')].forEach(item => {
const name = item.getAttribute('name')
const slotValue = [...this.querySelectorAll(`*[slotpro=${name}]`)]
item.outerHTMl = slotValue.reduce((dom, next) => {
dom.insertAdjacentElement('beforeend', next)
return dom
}, document.createElement('div')).innerHTML
})
}
})
</script>