Skip to main content

再谈 Web Components 使用

Web Components 使用跟 Vue 有点相似,但是相对于 Vue 来说没那么方便强大,在使用过程中还是有许多限制的,

虽然 Web Components 的自定义组件使用看似简单直接,但在实际开发中还是需要注意脚本加载顺序、组件通信和属性监听等细节问题,以确保组件能够正确无误地运行。

构成

Web Components 由三个主要部分构成:

  • 自定义元素(Custom Element): 这是一组 JavaScript API,使你能够定义自定义元素及其行为,以便根据需要在用户界面中使用它们
  • 影子 DOM(Shadow DOM): 这也是一组 JavaScript API,用于将一个封闭的“影子”DOM 树附加到元素上(与主文档 DOM 分开渲染),并管理其相关功能。这样,你可以确保元素的功能是私有的,因此它们可以被脚本化和样式化,而不会与文档的其他部分产生冲突
  • HTML 模板(HTML Template): template 和 slot 元素让你能够创建不会立即显示在渲染页面中的标记模板。然后,这些模板可以作为自定义元素结构的基础,被反复使用

自定义组件

创建一个基础的自定义组件包含以下步骤:

  1. 继承:自定义元素需要通过继承 HTMLElement 类(或其子类)来创建
  2. 创建模板:使用 document.createElement('template') 来创建一个新的模板元素
  3. 获取影子 DOM:通过元素的 attachShadow 方法获取影子 DOM,为元素提供封装的 DOM 结构
  4. 添加内容:将模板内容通过 appendChild 方法添加到影子 DOM 中。使用 cloneNode 方法克隆节点,避免对原始模板的污染
  5. 定义组件:使用 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)传值的问题,目前似乎没有直接的方法来实现