我们都有自己不想从事的项目。代码变得难以管理,范围不断发展,快速修复应用在其他修复之上,并且结构在意大利面条式代码的重压下崩溃了。编码可能是一件很麻烦的事情。
项目受益于使用具有单一职责的简单、独立的模块。模块化代码被封装,因此无需担心实现。只要您知道在给定一组输入时模块将输出什么,您就不一定需要了解它是如何实现该目标的。
将模块化概念应用于单一编程语言很简单,但Web开发需要多种技术组合。浏览器解析HTML 、 CSS和JavaScript以呈现页面的内容、样式和功能。
它们并不总是很容易混合,因为:
- 相关代码可以拆分为三个或更多文件,并且
- 全局样式和JavaScript对象可能会以意想不到的方式相互干扰。
除了这些问题之外,语言运行时、框架、数据库和服务器上使用的其他依赖项也会遇到这些问题。
什么是Web组件?
Web组件是一种创建可在任何页面上重用的封装的、单一职责的代码块的方法。
考虑HTML<video>
标签。给定URL ,查看者可以使用诸如播放、暂停、后退、前进和调整音量等控件。
虽然您可以使用各种属性和JavaScript API调用进行修改,但仍提供了样式和功能。任意数量<video>
元素可以放在其他标签内,它们不会发生冲突。
如果您需要自己的自定义功能怎么办?例如,显示页面上字数的元素?还没有HTML<wordcount>
标签。
React和Vue.js等框架允许开发人员创建Web组件,其中的内容、样式和功能可以在单个JavaScript文件中定义。这些解决了许多复杂的编程问题,但请记住:
- 您必须学习如何使用该框架并随着它的发展更新您的代码。
- 为一个框架编写的组件很少与另一个框架兼容。
- 框架的流行度起起落落。您将变得依赖于开发团队和用户的奇思妙想和优先事项。
- 标准Web组件可以添加浏览器功能,这在单独的JavaScript中是很难实现的(例如Shadow DOM)。
幸运的是,库和框架中引入的流行概念通常会进入Web标准。这花了一些时间,但Web组件已经到来。
Web组件简史
在许多特定于供应商的错误开始之后, Alex Russell在2011年的Fronteers Conference上首次引入了标准Web组件的概念。谷歌的Polymer库(一个基于当前提案的polyfill)在两年后问世,但早期的实现直到2016年才出现在Chrome和Safari中。
浏览器供应商花时间协商细节,但Web组件于2018年和20210年分别被添加到Firefox和Edge(当微软切换到Chromium引擎时)。
可以理解的是,很少有开发人员愿意或能够采用Web组件,但我们最终通过稳定的API达到了良好的浏览器支持水平。并非一切都是完美的,但它们是基于框架的组件的日益可行的替代方案。
即使您现在还不愿意放弃您最喜欢的,Web组件与每个框架都兼容,并且API将在未来几年内得到支持。
每个人都可以查看预先构建的Web组件库:
- WebComponents.org
- The Component Gallery
- generic-components
- web-components-examples
- awesome-standalones
- accessible_components
- Kickstand UI
……但是编写自己的代码更有趣!
本教程完整介绍了在没有JavaScript框架的情况下编写的Web组件。您将了解它们是什么以及如何使它们适应您的Web项目。您将需要一些HTML5 、CSS和JavaScript 的知识。
Web组件入门
Web组件是自定义HTML元素,例如<hello-world></hello-world>
。该名称必须包含一个破折号,以免与HTML规范中正式支持的元素发生冲突。
您必须定义一个ES2015类来控制元素。它可以命名为任何名称,但HelloWorld是常见的做法。它必须扩展HTMLElement接口,它表示每个HTML元素的默认属性和方法。
注意: Firefox允许您扩展特定的HTML元素,例如HTMLParagraphElement、HTMLImageElement或HTMLButtonElement。这在其他浏览器中不受支持,并且不允许您创建Shadow DOM。
为了做任何有用的事情,该类需要一个名为connectedCallback()的方法,该方法在元素添加到文档时调用:
class HelloWorld extends HTMLElement { // connect component connectedCallback() { this.textContent = 'Hello World!'; } }
在此示例中,元素的文本设置为“Hello World”。必须使用CustomElementRegistry注册该类才能将其定义为特定元素的处理程序:
customElements.define( 'hello-world', HelloWorld );
现在,当您的JavaScript加载时,浏览器会将<hello-world>
元素与您的HelloWorld <script type="module" src="./helloworld.js"></script>
)。
您现在有一个自定义元素!
这个组件可以像任何其他元素一样在 CSS 中设置样式:
hello-world { font-weight: bold; color: red; }
添加属性
该组件无益,因为无论如何都会输出相同的文本。像任何其他元素一样,我们可以添加HTML属性:
<hello-world name="Craig"></hello-world>
这可能会覆盖文本,因此“Hello Craig!” 被陈列。为此,您可以向HelloWorld类添加一个constructor()函数,该函数在创建每个对象时运行。它必须:
- 调用super()方法来初始化父 HTMLElement,以及
- 进行其他初始化。在这种情况下,我们将定义一个name属性,该属性设置为默认值“World”:
class HelloWorld extends HTMLElement { constructor() { super(); this.name = 'World'; } // more code...
您的组件只关心name属性。静态observedAttributes()属性应返回一组要观察的属性:
// component attributes static get observedAttributes() { return ['name']; }
当在HTML中定义属性或使用JavaScript更改属性时,将调用attributeChangedCallback()方法。它传递了属性名称、旧值和新值:
// attribute change attributeChangedCallback(property, oldValue, newValue) { if (oldValue === newValue) return; this[ property ] = newValue; }
在此示例中,只会更新name属性,但您可以根据需要添加其他属性。最后,您需要调整connectedCallback()方法中的消息:
// connect component connectedCallback() { this.textContent = `Hello ${ this.name }!`; }
生命周期方法
在Web组件状态的整个生命周期中,浏览器会自动调用六个方法。此处提供了完整列表,尽管您已经在上面的示例中看到了前四个:
constructor()
它在组件第一次初始化时被调用。它必须调用super()并且可以设置任何默认值或执行其他预渲染过程。
静态observedAttributes()
返回浏览器将观察到的一组属性。
attributeChangedCallback(propertyName, oldValue, newValue)
每当观察到的属性更改时调用。那些在HTML中定义的会被立即传递,但JavaScript可以修改它们:
document.querySelector('hello-world').setAttribute('name', 'Everyone');
发生这种情况时,该方法可能需要触发重新渲染。
connectedCallback()
当Web组件附加到文档对象模型时,将调用此函数。它应该运行任何需要的渲染。
disconnectedCallback()
当从文档对象模型中删除Web组件时调用它。如果您需要清理,例如删除存储的状态或中止Ajax请求,这可能很有用。
adoptedCallback()
当Web组件从一个文档移动到另一个文档时调用此函数。
Web组件如何与其他元素交互
Web组件提供了一些您在JavaScript框架中找不到的独特功能。
Shadow DOM
虽然我们在上面构建的Web组件可以工作,但它不能免受外部干扰,CSS或JavaScript可以对其进行修改。同样,您为组件定义的样式可能会泄漏并影响其他组件。
Shadow DOM通过将一个单独的DOM附加到Web组件来解决这个封装问题:
const shadow = this.attachShadow({ mode: 'closed' });
模式可以是:
- “open” ——外层页面的JavaScript可以访问Shadow DOM(使用Element.shadowRoot ),或者
- “closed” ——Shadow DOM只能在Web组件中访问。
Shadow DOM可以像任何其他DOM元素一样进行操作:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style> p { text-align: center; font-weight: normal; padding: 1em; margin: 0 0 2em 0; background-color: #eee; border: 1px solid #666; } </style> <p>Hello ${ this.name }!</p>`; }
<p>
元素中呈现“Hello”文本并为其设置样式。它不能被组件外的JavaScript或CSS修改,尽管字体和颜色等一些样式是从页面继承的,因为它们没有明确定义。
限定在此Web组件范围内的样式不能影响页面上的其他段落,甚至其他<hello-world>
组件。
请注意,CSS :host
选择器可以从Web组件中设置外部<hello-world>
元素的样式:
:host { transform: rotate(180deg); }
您还可以设置元素使用特定类时应用的样式,例如<hello-world class="rotate90">
:
:host(.rotate90) { transform: rotate(90deg); }
HTML模板
对于更复杂的Web组件,在脚本中定义HTML可能变得不切实际。模板允许您在页面中定义Web组件可以使用的HTML块。这有几个好处:
- 您可以调整HTML代码,而无需在JavaScript中重写字符串。
- 无需为每种类型创建单独的JavaScript类,即可自定义组件。
- 在HTML中定义HTML更容易——并且可以在组件呈现之前在服务器或客户端上对其进行修改。
模板是在<template>
标记中定义的,分配一个ID很实用,这样您就可以在组件类中引用它。此示例使用三个段落显示“Hello”消息:
<template id="hello-world"> <style> p { text-align: center; font-weight: normal; padding: 0.5em; margin: 1px 0; background-color: #eee; border: 1px solid #666; } </style> <p class="hw-text"></p> <p class="hw-text"></p> <p class="hw-text"></p> </template>
Web组件类可以访问此模板、获取其内容并克隆元素以确保您在任何使用它的地方创建唯一的DOM片段:
const template = document.getElementById('hello-world').content.cloneNode(true);
DOM可以直接修改并添加到Shadow DOM中:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }), template = document.getElementById('hello-world').content.cloneNode(true), hwMsg = `Hello ${ this.name }`; Array.from( template.querySelectorAll('.hw-text') ) .forEach( n => n.textContent = hwMsg ); shadow.append( template ); }
模板插槽Template Slots
插槽允许您自定义模板。假设您想使用Web组件<hello-world>
,但将消息放在Shadow DOM中的<h1>标题中。你可以写这样的代码:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> </hello-world>
(注意slot属性。)
您可以选择添加其他元素,例如另一个段落:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
现在可以在您的模板中实现插槽:
<template id="hello-world"> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template>
将在名为“msgtext”的 <slot>
位置插入设置为“msgtext”(即<h1>
)的元素slot属性。 <p>
没有指定插槽名称,但在下一个可用的未命名<slot>
中使用。实际上,模板变为:
<template id="hello-world"> <slot name="msgtext" class="hw-text"> <h1 slot="msgtext">Hello Default!</h1> </slot> <slot> <p>This text will become part of the component.</p> </slot> </template>
事实并非如此简单。Shadow DOM中的<slot>
元素指向插入的元素。只能通过定位<slot>
然后使用.assignedNodes() 方法返回内部子级数组来访问它们。更新的connectedCallback()方法:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }), hwMsg = `Hello ${ this.name }`; // append shadow DOM shadow.append( document.getElementById('hello-world').content.cloneNode(true) ); // find all slots with a hw-text class Array.from( shadow.querySelectorAll('slot.hw-text') ) // update first assignedNode in slot .forEach( n => n.assignedNodes()[0].textContent = hwMsg ); }
此外,您不能直接设置插入元素的样式,尽管您可以定位Web组件中的特定插槽:
<template id="hello-world"> <style> slot[name="msgtext"] { color: green; } </style> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template>
模板插槽有点不寻常,但一个好处是,如果JavaScript无法运行,您的内容将被显示。此代码显示了仅在Web组件类成功执行时才替换的默认标题和段落:
<hello-world name="Craig"> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
因此,您可以实现某种形式的渐进增强——即使它只是一条“You need JavaScript”的消息!
声明式Shadow DOM
上面的例子使用JavaScript构建了一个Shadow DOM。这仍然是唯一的选择,但正在为Chrome开发一个实验性的声明性Shadow DOM。这允许服务器端渲染并避免任何布局变化或无样式内容的闪烁。
HTML解析器检测到以下代码,它创建一个与您在上一节中创建的Shadow DOM相同的Shadow DOM(您需要根据需要更新消息):
<hello-world name="Craig"> <template shadowroot="closed"> <slot name="msgtext" class="hw-text"></slot> <slot></slot> </template> <h1 slot="msgtext">Hello Default!</h1> <p>This text will become part of the component.</p> </hello-world>
该功能在任何浏览器中均不可用,并且不能保证它会到达Firefox或Safari。您可以找到有关声明式Shadow DOM的更多信息,polyfill很简单,但请注意实现可能会发生变化。
Shadow DOM事件
您的Web组件可以像在页面DOM中一样将事件附加到Shadow DOM中的任何元素,例如侦听所有内部子级上的单击事件:
shadow.addEventListener('click', e => { // do something });
除非您stopPropagation ,否则该事件将冒泡到页面DOM中,但该事件将被重定向。因此,它似乎来自您的自定义元素,而不是其中的元素。
在其他框架中使用Web组件
您创建的任何Web组件都可以在所有JavaScript框架中工作。他们都不知道或关心HTML元素——您的<hello-world>
组件将被视为与<div>
相同,并被放置到DOM中,类将在其中激活。
custom-elements-everywhere.com提供了框架和Web组件注释的列表。尽管在React.js有一些问题,但大多数都是完全兼容的。可以在JSX中使用<hello-world>
:
import React from 'react'; import ReactDOM from 'react-dom'; import from './hello-world.js'; function MyPage() { return ( <> <hello-world name="Craig"></hello-world> </> ); } ReactDOM.render(<MyPage />, document.getElementById('root'));
…但:
- React只能将原始数据类型传递给HTML属性(而不是数组或对象)
- React无法侦听Web组件事件,因此您必须手动附加自己的处理程序。
关于Web组件批评和问题
Web组件有了显着改进,但有些方面可能难以管理。
样式和难点
样式化Web组件会带来一些挑战,尤其是当您想覆盖范围样式时。有很多解决方案:
- 避免使用Shadow DOM。您可以将内容直接附加到您的自定义元素,尽管任何其他JavaScript都可能意外或恶意更改它。
- 使用
:host
类。正如我们在上面看到的,当类应用于自定义元素时,作用域CSS可以应用特定的样式。 - 查看CSS自定义属性(变量)。自定义属性级联到Web组件中,因此,如果您的元素使用
var(--my-color)
,您可以在外部容器(例如:root
--my-color
,它将被使用。自定义属性级联到Web组件中,因此,如果您的元素使用var(--my-color)
,您可以在外部容器(例如::root
)中设置--my-color
,然后使用它。 - 利用阴影部件。新的::part() 选择器选择器可以设置具有part属性的内部组件的样式,即
<hello-world>
组件内部的<h1 part="heading">
可以使用选择器hello-world::part(heading)
设置样式。 - 传入一串样式。您可以将它们作为属性传递给
<style>
块。
没有一个是理想的,您需要计划其他用户如何仔细定制您的Web组件。
忽略输入
Shadow DOM中的任何<input>
、 <textarea>
或<select>
字段都不会在包含表单中自动关联。早期的Web组件采用者会将隐藏字段添加到页面DOM中,或者使用FormData接口来更新值。两者都不是特别实用的,都会破坏Web组件的封装。
新的ElementInternals接口允许Web组件连接到表单中,以便定义自定义值和有效性。它是用Chrome实现的,但是其他浏览器也可以使用polyfill。
为了演示,您将创建一个基本的<input-age name="your-age"></input-age>
组件。该类必须将静态formAssociated值设置为true,并且可以选择在外部窗体关联时调用formAssociatedCallback() 方法:
// <input-age> web component class InputAge extends HTMLElement { static formAssociated = true; formAssociatedCallback(form) { console.log('form associated:', form.id); }
构造函数现在必须运行attachInternals()方法,该方法允许组件与表单和其他想要检查值或验证的JavaScript代码进行通信:
constructor() { super(); this.internals = this.attachInternals(); this.setValue(''); } // set form value setValue(v) { this.value = v; this.internals.setFormValue(v); }
ElementInternal的setFormValue()方法在此处为使用空字符串初始化的父窗体设置元素的值(也可以传递具有多个名称/值对的FormData对象)。其他属性和方法包括:
- form:父表单
- labels:标记组件的元素数组
- 约束验证API选项,例如willValidate、checkValidity和validationMessage
connectedCallback()方法像以前一样创建Shadow DOM,但还必须监视字段的更改,以便可以运行setFormValue():
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style>input { width: 4em; }</style> <input type="number" placeholder="age" min="18" max="120" />`; // monitor input values shadow.querySelector('input').addEventListener('input', e => { this.setValue(e.target.value); }); }
您现在可以使用此Web组件创建一个HTML表单,其作用与其他表单字段类似:
<form id="myform"> <input type="text" name="your-name" placeholder="name" /> <input-age name="your-age"></input-age> <button>submit</button> </form>
它有效,但不可否认,它感觉有点令人费解。在CodePen演示中查看有关更多信息,请参阅有关功能更强大的表单控件的文章。
小结
在JavaScript框架的地位和能力不断提高的时候,Web组件很难获得一致和采用。如果您来自React、Vue或Angular,Web组件可能会显得复杂而笨拙,尤其是当您缺少数据绑定和状态管理等功能时。
有很多问题需要解决,但Web组件的未来是光明的。它们是框架无关的、轻量级的、快速的,并且可以实现单独使用JavaScript无法实现的功能。
十年前,很少有人会在没有jQuery的情况下解决网站问题,但浏览器供应商采用了优秀的部分,并添加了本地替代方案(如querySelector)。JavaScript框架也是如此,Web组件是第一个尝试性的步骤。
评论留言