使用 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>
Web 组件被调用rainbow-text
,它有两个属性:文本(组件将渲染文本)和字体大小。您也可以使用slots
和templates
来插入内容;但是,在我的用例中,它们会增加额外的开销。我希望输入文本,然后输出一系列 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
生命周期方法。它会在元素渲染时自动调用——类似于componentDidMount
React 中的生命周期方法。你也可以使用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()
}
render
是我编写的一种方法,在 中调用connectedCallback
。如果disconnectedCallback
和attributeChangedCallback
生命周期方法对您的代码有帮助,您也可以使用它们!我将它们分开只是为了遵守 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()
}
然后我将每个字母的单独跨度添加到 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)
})
}
最后,我将样式添加到影子 DOM:
addStyle () {
const styleTag = document.createElement('style')
styleTag.textContent = getStyle(this.size)
this.shadowRoot.appendChild(styleTag)
}
此方法向影子 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)
}
我还为非 WebComponent 友好浏览器的用户添加了警告!
后续步骤
我很喜欢使用 Web 组件!能够无需框架创建可复用组件的想法真是太棒了。我构建的这个组件对我来说非常实用,因为我经常使用那个多色名称。我只会script
在其他文档中添加它。不过,我不会将我的个人网站转换为使用该组件,因为我希望它能够跨浏览器支持。此外,它还没有一个清晰的状态或数据管理系统,考虑到 Web 组件的目标,这很合理;然而,这也使得其他前端框架仍然必不可少。出于这些原因,我想我会继续使用前端框架;不过,一旦它们获得全面支持,使用起来将会非常棒!
我的“学习新事物”系列的一部分
文章来源:https://dev.to/aspittel/building-web-components-with-vanilla-javascript--jho