Web Components 使用跟 Vue 有点相似,但是相对于 Vue 来说没那么方便强大,在使用过程中还是有许多限制的,
虽然 Web Components 的自定义组件使用看似简单直接,但在实际开发中还是需要注意脚本加载顺序、组件通信和属性监听等细节问题,以确保组件能够正确无误地运行。
构成
Web Components 由三个主要部分构成:
- 自定义元素(Custom Element): 这是一组 JavaScript API,使你能够定义自定义元素及其行为,以便根据需要在用户界面中使用它们
- 影子 DOM(Shadow DOM): 这也是一组 JavaScript API,用于将一个封闭的“影子”DOM 树附加到元素上(与主文档 DOM 分开渲染),并管理其相关功能。这样,你可以确保元素的功能是私有的,因此它们可以被脚本化和样式化,而不会与文档的其他部分产生冲突
- HTML 模板(HTML Template): template 和 slot 元素让你能够创建不会立即显示在渲染页面中的标记模板。然后,这些模板可以作为自定义元素结构的基础,被反复使用
自定义组件
创建一个基础的自定义组件包含以下步骤:
- 继承:自定义元素需要通过继承 HTMLElement 类(或其子类)来创建
- 创建模板:使用
document.createElement('template')
来创建一个新的模板元素 - 获取影子 DOM:通过元素的 attachShadow 方法获取影子 DOM,为元素提供封装的 DOM 结构
- 添加内容:将模板内容通过 appendChild 方法添加到影子 DOM 中。使用 cloneNode 方法克隆节点,避免对原始模板的污染
- 定义组件:使用
customElements.define('component-tag-name', ComponentClassName)
来定义组件,使其可在页面中使用
至此,一个最基础的自定义组件创建完成。
class ExtendP extends HTMLElement {
constructor() {
super()
// 创建模板
const template = document.createElement('template')
template.innerHTML = `
<style>
article {
width: 20%;
margin: 20px auto;
border: 1px solid gray;
}
header {
background-color: #f1f1f1;
color: #fff;
border: 1px solid lightblue;
}
</style>
<article>
<header>
<slot name="title">博客标题</slot>
</header>
<section>
<slot name="content">博客内容内容...</slot>
</section>
</article>
`
// 获取影子 DOM
const iRoot = this.attachShadow({ mode: 'open' })
// 将模板添加到影子 Dom 中
iRoot.appendChild(template.content.cloneNode(true))
}
// 要监听属性的变化,必须通过 static get observedAttributes 静态方法声明哪些属性是被监听的。
static get observedAttributes() {
return ['color', 'size']
}
// 之后,就可以在 attributeChangedCallback 回调函数中捕获属性变化。
attributeChangedCallback(name, oldValue, newValue) {
console.log(`属性 ${name} 已从 ${oldValue} 变更到 ${newValue}`)
}
get size() {
return this.getAttribute('size')
}
sayHello() {
console.log('hello')
}
// 仅有的三个生命周期钩子
connectedCallback() {
console.log('自定义组件添加至页面。')
this.ready() // 父组件给自定义组件绑定的方法,后面会提到
}
disconnectedCallback() {
console.log('自定义组件从页面中移除。')
}
adoptedCallback() {
console.log('自定义组件被移动到新的文档中。')
}
}
customElements.define('extend-p', ExtendP)
使用
当引入自定义组件时,建议使用 defer 属性,这样可以确保脚本在文档解析完成后再执行,避免潜在的执行顺序问题。
自定义组件的标签使用方式与 Vue 非常相似,即使用组件在 define 时指定的名称,并通过 slot="slot-name"来指定插槽的名称。
在 Vue 项目中,可以通过简单地导入组件的 JavaScript 文件(例如 import "./components/WebComponent/WebComponent.js";
)来使用这些自定义元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>test</title>
<script src="./WebComponent.js" defer></script>
</head>
<body>
<extend-p id="extendP" size="100">
<div slot="title">This is title.</div>
</extend-p>
</body>
<script>
const extendP = document.querySelector('#extendP')
// 父组件向自定义组件暴露某些方法
extendP.ready = () => {
extendP.sayHello()
extendP.setAttribute('size', '200')
}
// 调用自定义组件的方法
window.onload = () => {
extendP.sayHello()
extendP.setAttribute('color', 'b')
}
</script>
</html>
需要注意:
- 通常情况下,自定义组件内部和父组件都会包含一些业务逻辑,这可能需要父组件 向子组件暴露某些方法。例如,在自定义组件被加入到页面(即在 connectedCallback 被调用)之后,可能需要通知父组件,这时可以通过调用在父组件中定义的
this.ready()
方法。为了避免 connectedCallback 在 ready 方法定义之前执行而导致错误,推荐在引入自定义组件的脚本标签中加上 defer。 - 若需调用自定义组件的方法,必须确保自定义组件已经完全创建好。因此,在示例中通过定义一个 ready 方法,并在 connectedCallback 调用之后执行它,或者等待页面的 onload 事件触发后执行。这意味着定义和调用之间的执行顺序需要特别注意,以避免由于执行顺序引起的问题。
- 监听属性变化时,应使用 setAttribute 方法来修改属性值,因为直接修改属性(如
extendP.attributes['size']=200
)不会触发属性监听回调。 - 关于插槽(slot)传值的问题,目前似乎没有直接的方法来实现