使用 Vanilla JavaScript 构建 Web 组件

2025-05-24

使用 Vanilla JavaScript 构建 Web 组件

2015年,我正在学习我的第一个前端框架——AngularJS。我当时的想法是,用自定义功能构建自己的HTML标签。当然,实际情况并非如此,但它确实降低了学习难度。

现在,您真的可以使用 Web Components 构建自己的 HTML 标签了!它们目前仍处于实验阶段——Chrome 和 Opera 浏览器支持,FireFox 浏览器支持,但 Safari 和 Edge 浏览器尚未实现。一旦它们全面推出,它们将成为更强大的工具,让您使用纯 JavaScript 构建可复用组件——无需任何库或框架!

学习过程

我费了好大劲才找到用 Vanilla JS 编写的 Web 组件的文章和示例。Polymer 上有很多示例和文章,它是一个用于编写 Web 组件的框架,包含针对尚不支持 Web 组件的浏览器的 polyfill。这个框架听起来很棒,我以后可能会尝试使用它,但在这个特定的项目中,我只想使用 Vanilla JavaScript。

我最终主要使用了MDN上关于 Shadow DOM 的文档来构建我的项目。我还浏览了 CodePen 和 WebComponents 网站,但都没有找到太多与我想要构建的内容类似的内容。

我也非常喜欢Joseph Moore 的那篇关于 Web 组件的文章,这篇文章是在我做这个项目的时候发表的!它介绍了使用 Web 组件的一些好处:它们可以与所有框架兼容,而且由于只使用原生 JavaScript,所以易于实现和理解。

最终项目

在我的很多项目中,我都使用类似的设计方案,既是为了打造个人品牌,也是为了避免重复设计!具体来说,我使用的标题每个字母都有不同的颜色,并且带有下落动画。我的个人网站alispit.tel就是一个很好的例子!我的简历、会议幻灯片中也用到了这段文字,而且我计划在不久的将来将它用在其他网站上!唯一的问题是,CSS 不允许你定位单个字符——除了第一个字符。因此,每个字母都必须用 包裹起来span。这样写起来会很麻烦,所以我决定这是使用 Web 组件的最佳时机!

彩虹字母的显示

由于我很难找到有关人们编写 Web 组件的文章,因此我将在这里深入研究代码。

首先,使 Web 组件呈现的 HTML 代码如下所示:

  <rainbow-text text="hello world" font-size="100"></rainbow-text>
Enter fullscreen mode Exit fullscreen mode

Web 组件被调用rainbow-text,它有两个属性:文本(组件将渲染文本)和字体大小。您也可以使用slotstemplates来插入内容;但是,在我的用例中,它们会增加额外的开销。我希望输入文本,然后输出一系列 HTML 元素,每个文本之间用一个字符分隔,因此最简单的方法是通过属性传入文本——尤其是在使用 Shadow DOM 的情况下。

那么,什么是 Shadow DOM?它其实并不新鲜,也并非 Web 组件所独有。Shadow DOM 引入了一个具有独立作用域的 DOM 元素子树。它还允许我们隐藏子元素。例如,一个video元素实际上是 HTML 元素的集合;然而,当我们创建一个元素并检查它时,我们只能看到它的video标签!对我来说,Shadow DOM 最酷的地方在于它的样式是有作用域的!如果我在文档中添加一个样式,例如修改所有div元素,那么该样式不会影响 Shadow DOM 内的任何元素。反过来,Shadow DOM 内的样式也不会影响外部文档 DOM 上的元素。这是我最喜欢的 Vue 功能之一,所以我非常兴奋能够在没有框架的情况下实现类似的功能!

现在让我们开始编写实现自定义元素的 JavaScript 代码。首先,你需要编写一个 JavaScript 类来扩展内置HTMLElement类。我使用了 ES6 类,但你也可以根据需要使用 JavaScript 的旧版面向对象语法。我非常喜欢使用 ES6 类,尤其是在我之前在 React 中就已经非常习惯使用它们的情况下!它的语法既熟悉又简单。

我做的第一件事就是编写connectedCallback生命周期方法。它会在元素渲染时自动调用——类似于componentDidMountReact 中的生命周期方法。你也可以使用constructor类似于任何其他 ES6 类的方法;不过,我其实并不需要它,因为我没有设置任何默认值之类的。

在 中connectedCallback,我首先通过调用 实例化了元素的影子 DOM this.createShadowRoot()。现在,rainbow-text元素是其自身影子 DOM 的根,因此它的子元素将被隐藏,并拥有自己的样式和外部 JavaScript 修改范围。然后,我根据传入的 HTML 属性在类中设置属性。在类中,您可以将其视为this引用rainbow-text元素。无需运行document.querySelector('rainbow-text').getAttribute('text'),只需运行 即可从元素中this.getAttribute('text')获取属性。text

class RainbowText extends HTMLElement {
  connectedCallback () {
    this.createShadowRoot()
    this.text = this.getAttribute('text')
    this.size = this.getAttribute('font-size')
    this.render()
  }
Enter fullscreen mode Exit fullscreen mode

render是我编写的一种方法,在 中调用connectedCallback。如果disconnectedCallbackattributeChangedCallback生命周期方法对您的代码有帮助,您也可以使用它们!我将它们分开只是为了遵守 Sandi Metz 的规则,我非常严格地遵守这些规则!此方法与普通的原生 DOM 操作不同之处在于,我将创建的元素附加到 ,shadowRoot而不是document或 直接附加到元素!这只是将元素附加到文档的影子 DOM,而不是根 DOM。

  render () {
    const div = document.createElement('div')
    div.classList.add('header')
    this.shadowRoot.appendChild(div)
    this.addSpans(div)
    this.addStyle()
  }
Enter fullscreen mode Exit fullscreen mode

然后我将每个字母的单独跨度添加到 DOM,这本质上与原始 JavaScript 代码相同:

  addSpanEventListeners (span) {
    span.addEventListener('mouseover', () => { span.classList.add('hovered') })
    span.addEventListener('animationend', () => { span.classList.remove('hovered') })
  }

  createSpan (letter) {
    const span = document.createElement('span')
    span.classList.add('letter')
    span.innerHTML = letter
    this.addSpanEventListeners(span)
    return span
  }

  addSpans (div) {
    [...this.text].forEach(letter => {
      let span = this.createSpan(letter)
      div.appendChild(span)
    })
  }
Enter fullscreen mode Exit fullscreen mode

最后,我将样式添加到影子 DOM:

  addStyle () {
    const styleTag = document.createElement('style')
    styleTag.textContent = getStyle(this.size)
    this.shadowRoot.appendChild(styleTag)
  }
Enter fullscreen mode Exit fullscreen mode

此方法向影子 DOM 添加一个style标签,以修改其中的元素。我使用了一个函数,将标题的字体大小插入到包含所有 CSS 的模板字面量中。

编写组件后,我必须注册我的新元素:

try {
  customElements.define('rainbow-text', RainbowText)
} catch (err) {
  const h3 = document.createElement('h3')
  h3.innerHTML = "This site uses webcomponents which don't work in all browsers! Try this site in a browser that supports them!"
  document.body.appendChild(h3)
}
Enter fullscreen mode Exit fullscreen mode

我还为非 WebComponent 友好浏览器的用户添加了警告!

该元素最终在控制台中显示如下:

后续步骤

我很喜欢使用 Web 组件!能够无需框架创建可复用组件的想法真是太棒了。我构建的这个组件对我来说非常实用,因为我经常使用那个多色名称。我只会script在其他文档中添加它。不过,我不会将我的个人网站转换为使用该组件,因为我希望它能够跨浏览器支持。此外,它还没有一个清晰的状态或数据管理系统,考虑到 Web 组件的目标,这很合理;然而,这也使得其他前端框架仍然必不可少。出于这些原因,我想我会继续使用前端框架;不过,一旦它们获得全面支持,使用起来将会非常棒!

完整代码
示例使用- (不使用 webcomponents)

我的“学习新事物”系列的一部分

文章来源:https://dev.to/aspittel/building-web-components-with-vanilla-javascript--jho
PREV
CSS:从零到精通媒体查询
NEXT
打造出色的投资组合