将组件化引入 Web:Web Components 概述





5.00/5 (2投票s)
微软 Edge 团队分享了关于 Web Components 的见解:如何立即使用它们以及对下一代 Web Components 的期望。
Microsoft Edge 开发者建议箱中投票最多的五项功能中有四项属于 Web Components 系列功能——Shadow DOM、Template、Custom Elements 和 HTML Imports。在这篇文章中,我们将讨论 Web Components 并给出我们的观点,为那些可能不太熟悉它们的人提供一些背景知识,并对它们未来可能的发展方向进行一些猜测。要做到公正需要一些篇幅,所以请坐下来,倒杯咖啡(或不含咖啡因的饮料),然后继续阅读。
组件化:一种在 Web 上焕然一新的旧设计实践
Web 应用程序现在与任何其他软件应用程序一样复杂,并且通常需要许多人协调才能生产出发布的产品。为了提高效率,找到正确的方法来划分开发工作,同时最大限度地减少人员和系统之间的重叠是至关重要的。组件化(通常而言)就是这样做的。任何组件系统都应该通过提供隔离来减少整体复杂性,隔离是一种自然屏障,可以将一个系统的复杂性隐藏起来,不让另一个系统知晓。良好的隔离性还可以使重用性和可维护性变得更容易。
最初,Web 应用程序的复杂性主要通过在服务器上将应用程序隔离成单独的页面来管理,每个页面都要求用户在浏览器中从一个页面导航到另一个页面。随着 AJAX 和相关功能的引入,开发人员不再需要在 Web 应用程序的不同页面之间“导航”。对于一些常见场景,如阅读电子邮件或新闻,期望已经改变。例如,登录电子邮件后,您可能会从单个 URL“运行电子邮件应用程序”并全天停留在该页面上(即单页应用程序)。客户端 Web 应用程序逻辑可能要复杂得多,甚至可能与服务器端相媲美。解决这种复杂性的一个可能方法是在单个 Web 页面或文档中进一步组件化和隔离逻辑。
Web Components 的目标是通过隔离相关的 HTML、CSS 和 JavaScript 组来降低复杂性,以在单个页面的上下文中执行通用功能。
如何组件化?
因为 Web 组件必须将 HTML、CSS 和 JavaScript 整合在一起,所以每种技术支持的现有隔离模型都有助于在整个 Web 组件中保留重要的场景。这些独立的隔离模型包括(在以下段落中会更详细地描述):
- CSS 样式隔离
- JavaScript 和作用域(闭包)
- 全局对象隔离
- 元素封装(iframe)
CSS 样式隔离
目前在平台上还没有很好的方法来原生组件化 CSS(尽管像 Sass 这样的工具肯定会有所帮助)。组件模型必须支持一种将一组 CSS 与另一组 CSS 隔离开来的方式,以便一个组件中定义的规则不会干扰另一个组件。此外,组件样式应该只应用于组件的必要部分,而不应用于其他任何部分。说起来容易做起来难!
在样式表中,CSS 样式使用选择器应用于文档。选择器总是被认为对整个文档具有潜在的适用性,因此它们的范围本质上是全局的。当一个项目的许多贡献者将他们的 CSS 文件汇集在一起时,这种全局范围会导致实际的冲突。重叠和重复的选择器具有既定的优先级(例如,层叠、特异性和源顺序)来解决冲突,但产生的行为可能不是开发人员所期望的。解决这个问题有许多潜在的解决方案。一个简单的解决方案是将参与组件的元素和相关样式从主文档移动到不同的文档(一个影子文档),这样它们就不再是选择器匹配的候选项。这带来了一个次要问题:既然已经建立了边界,如何跨边界设置样式?这显然可以在 JavaScript 中命令式地完成,但似乎依赖 JavaScript 来调解跨边界的样式对于 CSS 中的一个空白来说是很尴尬的。
为了有效地跨组件边界传输样式,并保护组件的结构(例如,允许自由更改结构而不会破坏样式),有两种普遍认同的通用方法:“部分”样式(使用自定义伪元素)和自定义属性(以前称为 CSS “变量”)。曾有一段时间,功能强大的跨边界选择器组合器 '>>>' 也被考虑过(在CSS Scoping 中指定),但现在普遍认为这是一个糟糕的主意,因为它太容易破坏组件隔离。
部分样式允许组件作者创建自定义伪元素进行样式设置,从而仅将其内部结构的一部分暴露给外部世界。这类似于浏览器用来暴露其原生控件的“部分”的模型。为了完成此场景,作者可能需要某种方式来限制可以应用于伪元素的样式集。对基于伪元素的“部分模型”的进一步探索可能会产生有用的样式原语,尽管细节需要理清。部分模型中的进一步工作还应合理化浏览器内置原生控件样式(这是一个急需关注的领域)。
自定义属性允许作者描述他们希望在样式表中重用的样式值(定义为以双破折号为前缀的自定义属性名称)。自定义属性通过文档的子树继承,允许选择器覆盖给定子树的自定义属性值,而不影响其他子树。自定义属性名称也能够跨组件边界继承,为组件提供了一种优雅的样式机制,避免了暴露组件的结构性质。自定义属性已被各种 Google 组件框架评估,并据称可以满足大多数样式需求。
到目前为止所考虑的所有样式方法中,未来的“部分”模型和当前的自定义属性规范似乎具有最积极的势头。我们认为自定义属性是 Web 组件规范家族中必不可少的新成员。
其他 CSS 样式隔离方法
为求完整性,CSS 的作用域和隔离并不像上面假设的那么黑白分明。事实上,一些过去和当前的提案提供了作用域和隔离优势,这些优势对 Web 组件具有不同的适用性。
CSS 为特定场景提供了一些有限形式的选择器隔离。例如,@media 规则将一组选择器组合在一起,并在满足媒体条件(如视口的大小/尺寸或媒体类型,例如打印)时有条件地应用它们;@page 规则定义了一些仅适用于打印条件(分页媒体)的样式;@supports 规则将选择器集合起来,仅在实现支持特定 CSS 功能时应用(CSS 功能检测的新形式);@document 的提案将选择器组合在一起,仅在加载样式表的文档与规则匹配时应用。
CSS Scoping 功能(最初作为 Web Components 工作的一部分形成)是一个关于限制 CSS 选择器在单个 HTML 文档中适用性的提案。它定义了一个新的 @scope 规则,该规则允许选择器识别作用域根,然后导致 @scope 规则中包含的所有选择器的评估仅对该根具有子树范围的适用性(而不是文档范围的适用性)。该规范允许 HTML 声明性地定义作用域根(例如,提议的 <style scoped> 属性,目前只有 Firefox 实现了该功能;该功能以前在 Chrome 中作为可选实验可用,但此后已被完全移除)。该功能的一些方面(即Selectors L4中定义的 :scope)也旨在用于 DOM 规范的新查询 API 中的相对选择器评估。
需要注意的是,@scope 只建立一个单向隔离边界:@scope 内的选择器被限制在作用域内,而任何其他选择器(@scope 外部)仍然可以随意选择 @scope 内部(尽管它们可能会被层叠以不同的顺序排列)。这是一个不幸的设计,因为它不为不在 @scope 子集中的任何选择器提供作用域/隔离——所有 CSS 仍然必须“和睦相处”,以避免在另一个 @scope 规则中意外样式。参见Tab 的 @in-shadow-of 草图,它更符合保护组件隔离的模型。
另一种提议的作用域形式是 CSS Containment。Containment 作用域更多地关注“布局”隔离,而非样式/选择器隔离。通过“contain”属性,某些具有自然继承性(在文档中从父元素到子元素的适用性,例如计数器)的 CSS 功能的行为将被阻止。主要用例是让开发人员表明某些元素具有强大的包含承诺,这样应用于该元素及其子树的布局永远不会影响文档的另一部分的布局。这些包含承诺(通过使用“contain”属性强制执行)允许浏览器优化布局和渲染,以便包含子树中的“脏”布局仅需要更新该子树的布局,而不是整个文档。
随着浏览器供应商对 Web Components 技术的实现日趋成熟并得到越来越多的公众使用,可能会出现额外的样式模式和问题;我们应该预期会因此看到对各种 CSS 提案的进一步投资和进展,以改进 Web Components 样式。
JavaScript 和作用域
所有包含在网页中的 JavaScript 都可以访问相同的共享全局对象。与任何编程语言一样,JavaScript 具有作用域,为函数的代码提供了一定程度的“隐私”。这些词法作用域用于将变量和函数与全局环境的其余部分隔离。今天流行的 JavaScript“模块模式”(它使用词法作用域)是为了满足多个 JavaScript 框架在单个全局环境中“共存”而演变出来的,它们不会相互“践踏”(取决于加载顺序)。
JavaScript 中的词法作用域是一个单向隔离边界——作用域内的代码可以访问作用域的内容以及任何祖先作用域,直到全局作用域,而作用域外的代码无法访问作用域的内容。重要的原则是单向隔离有利于作用域内的代码,保护它。词法作用域中的代码可以选择保护/隐藏其代码,使其不被环境的其余部分访问(或不)。
JavaScript 的词法作用域对 Web Components 的贡献是要求有一种“关闭”组件的方式,以便其内容可以合理地私有化。
全局对象隔离
某些代码可能不希望如上所述共享对全局环境的访问。例如,某些 JavaScript 代码可能不被应用程序开发人员信任——但它提供了至关重要的价值。广告和广告框架就是这样的例子。为了 JavaScript 的安全保障,需要在一个独立的、干净的脚本环境(一个拥有自己独特全局对象的环境)中运行不受信任的代码。开发人员也可能更喜欢一个全新的全局对象,可以在其中编写代码而无需担心其他脚本。为了今天做到这一点(不求助于 iframe 元素),开发人员可以使用 Worker。Worker 的缺点是它们不提供对元素的访问,因此也无法访问 UI。
在设计支持全局对象隔离的组件时,有许多考虑因素——尤其是当这种隔离将启用安全边界时(下面会详细介绍)。隔离组件预计要到初始的 Web Components 规范集确定下来之后才能完全开发完成(即,“留待 v2”)。但是,现在花一些时间展望隔离组件可能是什么样子,将有助于为当前正在进行的一些工作提供信息。几个提案已被提出,值得深入研究。
全局对象隔离填补了 Web Components 的一个重要缺失场景。在此期间,我们必须依靠当今 Web 上最成功、最广泛部署的组件化形式:iframe 元素。
元素封装(iframe)
iframe 元素及其相关兄弟:object 元素、frameset 和命令式 window.open() API 已经提供了托管隔离元素子树的能力。与旨在在单个文档中运行的组件不同,iframe 将整个 HTML 文档连接在一起;就像两个独立的 Web 应用程序被共同放置,一个在另一个内部。每个都有一个唯一的文档 URL、全局脚本环境和 CSS 作用域;每个文档都完全相互隔离。
iframe 是当今 Web 上最成功(也是唯一广泛部署)的组件化形式。iframe 允许不同的 Web 应用程序进行协作。例如,许多网站使用 iframe 作为组件形式,以实现从广告到联合身份登录的所有功能。iframe 有一系列挑战以及解决这些挑战的方法
一个 HTML 文档中的 JavaScript 代码可能会突破另一个文档的隔离边界(例如,通过 iframe 的 contentWindow 属性)。这种突破 iframe 隔离边界的能力可能是一个必需的功能,但当 iframe 中的内容包含不打算共享的敏感信息时,这也会带来安全风险。今天,通过同源策略来缓解不必要的突破:同源的文档 URL 默认允许突破,而跨源文档之间只有有限的通信能力。
仅凭突破并不是唯一的安全风险。<iframe sandbox> 属性的使用对跨源 iframe 提供了进一步的限制,以保护宿主免受不必要的脚本、弹出窗口、导航以及框架中可用的其他功能的影响。
框架文档外部的 CSS 样式无法应用于其内部文档。这种设计保留了隔离原则。然而,当 iframe 作为组件(在同源内部)使用时,样式隔离在集成中造成了一个显著的缝隙。HTML 通过针对同源 iframe 的提议的 <iframe seamless> 属性解决了这个问题。seamless 属性的使用消除了框架内容的样式隔离;无缝框架文档会复制其宿主文档的样式,并像没有其宿主框架元素的限制一样进行渲染。
有了良好的安全策略和无缝框架功能,将 iframe 作为组件模型似乎是一个非常有吸引力的解决方案。然而,Web 组件模型的一些期望特性仍然缺乏
深度集成。iframe 限制(并且大部分完全阻止)了宿主和框架文档之间的集成和交互模型。例如,相对于宿主,焦点和选择模型是独立的,事件传播被隔离到其中一个文档。对于想要更紧密集成的组件,在宿主文档中没有“代理”来促进跨越边界的情况下,支持这些行为是不可能的。
全局对象泛滥。页面上创建的每个 iframe 实例都会创建一个唯一的全局对象。全局对象及其相关完整的类型系统创建成本不菲,最终会消耗大量内存和浏览器中的额外开销。页面上使用的相同组件的多个副本可能不需要彼此完全隔离,事实上共享一个全局对象更可取,尤其是在需要通用共享状态时。
宿主内容模型分发。Iframe 目前不允许在框架文档中重用宿主元素的内容模型。(简单来说:元素的内容模型是其支持的元素和文本子树。)例如,一个 select 元素有一个包含 option 元素的内容模型。一个实现为 Web 组件的 select 元素也希望以同样的方式消费这些子元素。
选择性样式。无缝 iframe 不适用于跨源文档。如果允许这样做,会存在微妙的安全风险。主要问题是“无缝”由宿主控制,而不是框架文档(框架文档在相关攻击中通常是受害者)。对于组件而言,二进制的“无缝”功能可能过于极端;组件可能希望更具选择性地决定宿主中的哪些样式应适用于其内容(而不是自动继承所有样式,即无缝的工作方式)。一般来说,样式化什么的问题应该属于组件。
API 暴露。许多 Web Components 的场景涉及创建具有自己的暴露 API 表面、渲染语义和生命周期管理的完整新“自定义”元素。使用 iframe 将开发人员限制在以 iframe 的 API 表面为基准进行工作,并附带所有相关假设。例如,元素的标识及其生命周期语义无法更改。
并非首次尝试
值得注意的是,过去曾提出并实施了几种技术,试图改进 HTML 的 iframe 和相关的封装功能。然而,这些技术在今天的公共网络上都没有以任何有意义的方式存活下来
HTML Components (1998) 由微软在 IE5.5 中提出并实现(在 IE10 中废弃)。它使用声明式模型将事件和 API 附加到宿主元素(考虑到隔离),并将组件解析为“viewlink”(即“Shadow DOM”)。支持两种风格的组件,一种永久附加到元素,另一种通过 CSS“行为”属性动态绑定。
XBL(2001)及其后继者XBL2(2007)由 Mozilla 提出,作为其 XUL 用户界面语言的伴侣。XBL 是一种声明性绑定语言,具有两种绑定风格(类似于微软的 HTML Components),支持宿主内容模型分发和内容生成的额外功能。
今天的 Web Components
在两次尝试启动失败之后,又到了新一轮组件提案的时候,这次由 Google 牵头。以 XBL 中描述的概念为起点,这个庞大的组件系统被分解为一系列组件构建块。这些构建块允许 Web 开发人员在 Web Components 的完整愿景完全实现之前,尝试一些有用的独立功能。正是这种组件化和独立有用功能的开发,促成了它的成功。几乎每个人都能找到 Web Components 中对其应用程序有用的部分!
这种新一代的 Web Components 旨在满足一系列定义的用例,并通过解释现有内置元素如何在今天的 Web 平台上工作来实现这一点。理论上,Web Components 允许开发人员以与原生元素相同的保真度和特性来原型化新型 HTML 元素(实践中,HTML 中的可访问性行为目前尤其难以匹配)。
很明显,交付所有 Web Components 用例所需的全部技术,不会在一开始就在所有浏览器中提供。实现者正在共同努力,商定核心技术集,其细节可以一致地实现,然后再继续处理其他用例。
“第一代”Web Components 技术包括:
- 自定义元素:自定义元素定义了 HTML 解析器的扩展点,使其能够识别新的“自定义元素”名称并自动为其提供 JavaScript 支持的对象模型。自定义元素不启用组件边界,而是为浏览器提供了将 API 和行为附加到作者定义的元素的方法。不支持的浏览器可以使用变动观察者/事件和原型调整以不同程度的精确度填充自定义元素。掌握正确的时机并理解其含义是我们即将召开的会议的主题之一。
- “is”属性。在自定义元素规范中隐藏着另一个重要的特性——指示内置元素应被赋予自定义元素名称和 API 功能的能力。在正常情况下,自定义元素从一个通用元素开始;通过“is”属性,可以使用原生元素代替(例如,<input is="custom-input">)。虽然此功能是继承特定 HTML 元素内置的默认渲染、可访问性、解析等所有优点的绝佳方式,但其语法被认为有些笨拙,并且有人怀疑可访问性原语加上原生控件样式是否是更好的长期标准化途径。
- Shadow DOM:提供了一个命令式 API,用于创建可以(仅一次)连接到宿主元素的独立元素树。这些“影子”子元素在渲染文档时会替换“真实”子元素。Shadow DOM 还通过使用新的槽位元素(最近提出)、事件目标修正以及封闭/开放操作模式(也新近采用)来重用宿主元素的内容模型。这个相对简单的想法对从焦点模型和选择到组合和分发(对于 Shadow DOM 中的 Shadow DOM)的所有方面都产生了惊人的巨大副作用。
- CSS Scoping 定义了与 Shadow DOM 样式相关的各种伪元素,包括 :host、::content(即将变为 ::slot??)和以前的 '>>>'(Shadow DOM 穿透组合器),后者现已正式废弃。
- template 元素:为了完整性而包含,这个早期的 Web Components 功能现在已成为 HTML5 推荐标准的一部分。template 元素引入了惰性概念(template 的子元素不会触发下载或响应用户输入等),并且是 HTML 中声明性创建分离元素子树的第一种方式。Template 可用于多种用途,从模板盖章和数据绑定到传递 Shadow DOM 的内容。
- HTML Imports:定义了声明性语法,用于将 HTML“导入”(请求、下载和解析)到文档中。导入(使用 link 元素的 rel="import")在宿主页面的上下文中执行导入文档的脚本(因此可以访问相同的全局对象和状态)。Web Components 的 HTML、JavaScript 和 CSS 部分可以使用单个导入方便地部署。
- 自定义属性:如上文所述,在组件外部描述的、可在组件内部使用的自定义属性,是当今组件样式的一种简单而有用的模型。鉴于此,我们将自定义属性作为第一代 Web Components 技术的一部分。
Web Components:下一代
正如本文开头所指出的,构建 Web Components 的功能是一段旅程。许多旨在扩展和填补当前功能空白的想法已经开始流传(这并非完整索引):
- 声明式 Shadow DOM。在考虑如何以序列化形式重新传递组件时,声明式 Shadow DOM 变得很重要。如果没有声明式形式,像 innerHTML 或 XMLSerializer 这样的技术就无法构建包含任何 Shadow 内容的 DOM 字符串表示。因此,Shadow DOM 在没有脚本帮助的情况下是无法往返的。Mozilla 的 Anne 提出了一个 <shadowdom> 元素作为初步提案。类似地,template 元素已经是一种构建 Shadow 标记形式的声明式方法,并且浏览器中的序列化技术已经进行了调整,以考虑到这一点并相应地序列化 template 的“Shadow”内容。
- 完全隔离的组件。三家浏览器供应商在这个领域提出了三个不同的提案。这三个提案已经相当一致,这从达成共识的角度来看是个好消息。如前所述,隔离组件将使用一个新的全局对象,并且可能从跨源导入。它们还将有一个合理的模型来在其隔离边界上公开 API 和相关行为。
- 可访问性原语。许多可访问性社区的人喜欢“is”(来自自定义元素)这个概念,作为扩展现有原生元素的一种方式,因为原生元素携带着 JavaScript 开发人员通常无法获得的可访问性行为。理想情况下,一个通用 Web 组件(不使用“is”)可以在可访问性方面与原生元素紧密集成,包括表单提交和焦点功能等;这些扩展点目前尚不可行,但应该进行探索和定义。
- 统一原生控件样式。 浏览器之间缺乏一致的控件样式一直是一个互操作性问题区域,阻碍了简单的“主题导向”扩展的广泛部署。这导致开发人员经常将 Shadow DOM 视为替代解决方案(尽管用与原生控件相同的行为保真度来模拟 Shadow DOM 可能具有挑战性)。一些控件样式理念已经在 CSS 中酝酿了一段时间,但由于缺乏动力,这些理念的发展速度并不快。
- CSS Parts 样式。 虽然 CSS 自定义属性今天可以为 Web Components 提供广泛的样式选项,但在某些情况下,暴露组件的一些封装框以进行样式化更直接或更合适。如果这也能合理化现有浏览器使用的暴露原生控件部分样式的方法,那尤其有用。
- 解析器定制。与自定义元素一起使用时,自定义元素解析只能使用标准开/闭标签。为了让 Web 组件利用,允许解析器进行额外的定制可能是可取的。
最后,虽然不正式视为 Web Components 的一部分,但受人尊敬的 iframe 也不应被忽视。正如所讨论的,iframe 仍然是 Web 平台构建类似组件的事物的非常有用的功能。了解并可能改进 iframe 的“组件”故事将是有益的。例如,理解并解决 <iframe seamless> 未解决的问题似乎是一个很好的起点。
Web Components 是 Web 迈出的变革性一步。我们很高兴能继续支持并为这一旅程做出贡献。明天,我们将分享更多关于我们的路线图和实施计划。在此期间,请在 Twitter @MSEdgeDev 或下面的评论中分享您的反馈。
更多 Web 开发实践
本文是微软技术布道师和工程师关于实用 JavaScript 学习、开源项目和互操作性最佳实践的 Web 开发系列文章的一部分,其中包括 Microsoft Edge 浏览器和新的 EdgeHTML 渲染引擎。
我们鼓励您使用 dev.modern.IE 上的免费工具,在包括 Microsoft Edge(Windows 10 的默认浏览器)在内的各种浏览器和设备上进行测试。
- 扫描您的网站是否存在过时库、布局问题和可访问性问题
- 将虚拟机用于 Mac、Linux 和 Windows
- 在您自己的设备上远程测试 Microsoft Edge
- GitHub 上的编程实验室:跨浏览器测试和最佳实践
深入了解 Microsoft Edge 和 Web 平台的技术学习
- Microsoft Edge Web Summit 2015(新浏览器、新支持的 Web 平台标准以及 JavaScript 社区特邀嘉宾的期待)
- 哇,我可以在 Mac 和 Linux 上测试 Edge 和 IE!(来自 Rey Bango)
- 在不破坏 Web 的情况下推进 JavaScript(来自 Christian Heilmann)
- 让 Web 正常工作的 Edge 渲染引擎(来自 Jacob Rossi)
- 使用 WebGL 释放 3D 渲染(来自 David Catuhe,包括 vorlon.JS 和 babylonJS 项目)
- 托管 Web 应用程序和 Web 平台创新(来自 Kevin Hill 和 Kiril Seksenov,包括 manifold.JS 项目)
更多免费的跨平台工具和网络平台资源
关于作者
Travis Leithead 和 Arron Eicholz 是 Microsoft Edge 团队的项目经理。您可以在 Twitter 上分别关注 Travis @TravisLeithead 和 Arron @MrCSS。