一起构建 Web 组件!第二部分:Gluonjs 的 Polyfill

2025-06-08

开始构建 Web 组件!第二部分:Polyfill

Gluonjs

如今,基于组件的 UI 风靡一时。你知道 Web 有自己的原生组件模块,无需使用任何库吗?真的!你可以编写、发布和复用单文件组件,这些组件可以在任何优秀的浏览器和框架(如果你喜欢的话)上运行。

在上一篇文章,我们了解了允许我们编写 Web 组件的四种 Web 标准:<template>自定义元素、影子 DOM 和 JavaScript 模块。

今天,我们将学习一些有关webcomponentsjs polyfill 的知识,它让我们可以编写基于 Web 组件的应用程序,这些应用程序可以在不支持该规范的浏览器上运行。

概述

Web 组件真的很棒。如果你是我最喜欢的那种书呆子,跨浏览器、可复用、可互操作的组件承诺绝对会让你兴奋不已。基于 Web 组件的库和应用将迅速普及,这毋庸置疑,因为自 2018 年 10 月下旬起,最新版本的 Chrome、Firefox 和 Safari 都将原生支持 Web 组件。就连微软也开始在 Edge 浏览器中实现这些组件了。太棒了!

但在这个圈子里混迹多年的 Web 开发者都知道,事情并不总是那么简单。有时候,感觉 Web 平台的功能越酷(我说的就是你,scroll-snap!),获得广泛支持的可能性就越小。

不过朋友们,别担心!现在您就可以深入探索 Web Components 的世界,不用担心会把老旧浏览器的用户甩在身后。Google Web Components 团队的优秀成员在创建webcomponentsjs polyfills时就已经考虑到了您的需求,这些 polyfills 可以让您的应用适配 IE11,我相信这正是您每天早上醒来的动力。这些 polyfills 也适用于旧版 Chrome、Firefox 和 Microsoft Edge,直到它们正式发布。 醒来并实施用户之声板上最受欢迎的两张票完成其实施。

所以别傻坐着,继续阅读!我们将一起学习如何加载 polyfill,如何编写正确利用 polyfill 的自定义元素,以及如何避免 polyfill 的已知问题和陷阱。

加载 Polyfill

对于大多数用户来说,最简单的做法是在加载任何组件文件之前,将webcomponents-loader.js脚本添加到页面的 中head。该脚本会检查用户浏览器的UA字符串,并仅加载所需的 polyfill 或 polyfill 集合。

<head>
  <!-- Load the polyfills first -->
  <script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
  <!-- Then afterwards, load components -->
  <script type="module" src="./superlative-input.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

您可以像我们上面所做的那样通过 CDN 加载脚本,也可以通过安装到项目中将它们与应用程序的其余代码捆绑在一起:

npm install --save @webcomponents/webcomponentsjs
Enter fullscreen mode Exit fullscreen mode
<head>
  <!-- ... -->
  <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>;
</head>

<body>
  <script type="module">
    import './superlative-input.js'
    const template = html`<superlative-input label="🥙"></superlative-input>`;
    // ...
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode
顺便说一句,我刚刚发现有一个沙拉三明治表情符号🥙,我认为从技术上讲,它让世界更接近完美了一步。

高级加载场景

如果你确切地知道你需要什么,你也可以单独加载特定的 polyfill:

<!-- Load all polyfills, including template, Promise, etc. -->
<!-- Useful when supporting IE11 -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>

<!-- Load only the Shadow-DOM and Custom Elements polyfills -->
<!-- Useful to support Firefox <63 -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/entrypoints/webcomponents-sd-ce-index.js"></script>

<!-- Load only the Shadow-DOM polyfills -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/entrypoints/webcomponents-sd-index.js"></script>

<!-- Load only the Custom Elements polyfills -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/entrypoints/webcomponents-ce-index.js"></script>
Enter fullscreen mode Exit fullscreen mode

您可以选择硬着头皮sd-ce在所有情况下都加载 bundle 或 polyfill,这样可以节省用户与服务器的往返次数。在生产环境中,减少请求数量至关重要,这是一种常见的选择。在大多数简单情况下,您可能只想使用webcomponents-loader.js脚本。

完整包会给关键加载路径增加94kb ,而加载器仅增加5kb。您应该在少数使用旧版浏览器的用户的需求与大多数使用常用浏览器的用户的便利性之间取得平衡。

异步加载

大多数情况下,您需要同步加载webcomponents-loader.js位于 顶部的脚本head。但有时您需要异步加载。例如:如果您的应用实现了静态应用外壳,以便给用户带来性能方面的视觉冲击,您会希望静态 HTML 和 CSS 能够尽快加载,这意味着要消除阻塞渲染的资源。在这种情况下,您需要使用window.WebComponents.waitFor方法来确保组件在 polyfill 之后加载。以下是 无偿解除README中稍作修改的示例webcomponentsjs

<!-- Note that because of the "defer" attr, "loader" will load these async -->
<script defer src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<!-- Note that all modules are deferred -->
<script type="module">
  WebComponents.waitFor(() =>
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components API's.
    // The standard pattern is to load element definitions that call
    // `customElements.define` here.
    // Note: returning the import's promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    Promise.all([
      import('./my-element.js'),
      import('/node_modules/bob-elements/bobs-input.js'),
      import('https://unpkg.com/@power-elements/lazy-image/lazy-image.js?module'),
    ])
  );
</script>

<!-- Use the custom elements -->
<my-element>
  <bobs-input label="Paste image url" onchange="e => lazy.src = e.target.value"></bobs-input>
  <lazy-image id="lazy"></lazy-image>
</my-element>
Enter fullscreen mode Exit fullscreen mode

或者更典型的静态应用程序外壳模式的示例:

<head>
  <script defer src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
  <style>
    /* critical static-app-shell styles here */
  </style>
</head>
<body>
  <script type="module">
    // app-shell.js in turn imports its own dependencies
    WebComponents.waitFor(() => import('./app-shell.js'))
  </script>
  <app-shell loading>
    <header id="static-header">
      <span id="static-hamburger"></span>
      <span id="static-user"></span>
    </header>
    <main>
      <div id="static-spinner"></div>
    </main>
    <footer id="static-footer"></footer>
  </app-shell>
</body>
Enter fullscreen mode Exit fullscreen mode

编写与 Polyfill 兼容的自定义元素

如果您使用PolymerLitElement混合组件库(以及其他一些库)来编写组件(我们将在后续文章中介绍),您的组件将能够直接使用 polyfill。这些库是专门 使用polyfill 而编写的。您的工作完成了。喝杯啤酒吧。

但是如果您在编写组件时不使用库(首先,这对您有好处),您需要完成一些工作以确保您的组件能够为尽可能多的用户正确呈现。

眼尖的读者可能已经注意到,我们在上一篇文章中使用的一个示例中穿插着几行棘手的 JavaScript 代码:

const template = document.createElement('template')
template.innerHTML = /*...*/

// Let's give the polyfill a leg-up
window.ShadyCSS &&
window.ShadyCSS.prepareTemplate(template, 'awesome-button')

customElements.define('awesome-button', class AwesomeButton extends HTMLElement {
  constructor() {
    super()
    this.onclick = () => report('Clicked on Shadow DOM')
  }

  connectedCallback() {
    // Let's give the polyfill a leg-up
    window.ShadyCSS && window.ShadyCSS.styleElement(this)
    if (!this.shadowRoot) {
      this.attachShadow({mode: 'open'});
      this.shadowRoot.appendChild(template.content.cloneNode(true))
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

看到那个ShadyCSS引用了吗?这是 polyfill 的一部分,它模拟了不支持 shadow DOM 的浏览器中的样式作用域。为了确保你的样式作用域正确,需要遵循以下几条规则:

ShadyCSS 规则:

  1. <style>样式应该在元素的直接子元素中定义<template>
  2. <style>标签应该是该模板中唯一的标签。
  3. 在元素附加之前,将其模板与其标签名称关联起来ShadyCSS.prepareTemplate(templateElement, tagName)
  4. 在自定义元素附加到文档之后,但在创建影子根之前,调用ShadyCSS.styleElement自定义元素来计算其样式。

prepareTemplate将样式标签中的规则解析为抽象语法树,然后将生成的父选择器添加到其中以模拟范围。

button {/*...*/}
Enter fullscreen mode Exit fullscreen mode

变成...

.style-scope .awesome-button button {/*..*/}
Enter fullscreen mode Exit fullscreen mode

styleElement将范围类应用于您的元素及其“可疑”子元素。

<awesome-button>
  #shadow-root
  <button></button>
</awesome-button>
Enter fullscreen mode Exit fullscreen mode

变成...

<awesome-button>
  <button class="style-scope awesome-button"></button>
</awesome-button>
Enter fullscreen mode Exit fullscreen mode

var(--foo)如果浏览器不支持CSS 自定义属性 ( ),ShadyCSS 还将对其进行填充。

动态样式

由于 ShadyCSS polyfill 的工作方式,建议需要支持旧版浏览器的 Web 组件作者不要使用动态生成的 CSS,例如:

const getTemplate = ({disabled}) => `
  <style>
    button {
      background-color: ${disabled ? 'grey' : 'white'};
    }
  </style>
`

class AwesomeButton extends HTMLElement {
  set disabled(disabled) {
    this.render()
  }

  connectedCallback() {
    this.attachShadow({mode: 'open'})
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = getTemplate(this.disabled)
  }
}
Enter fullscreen mode Exit fullscreen mode

不要使用这个例子(由于许多不同的原因,这个例子构思很差,不仅仅是为了兼容 ShadyCSS),而是使用 CSS 自定义属性,并且每当发生动态更新时,使用ShadyCSS.styleSubTreeShadyCSS.styleDocument

const template = document.createElement('template')
template.innerHTML = `
  <style>
    button {
      background-color: var(--awesome-button-background, white);
    }
  </style>
  <button></button>
`;

class AwesomeButton extends HTMLElement {
  static get observedAttributes() {
    return ['disabled']
  }

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({mode: 'open'})
      this.shadowRoot.appendChild(template.content.cloneNode(true))
    }
  }

  attributesChangedCallback(name, oldVal, newVal) {
    name === 'disabled' &&
    ShadyCSS &&
    ShadyCSS.styleDocument({
      '--awesome-button-background' : newVal ? 'grey' : 'white',
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

对于那些好奇的人来说,原生地执行此操作的方法(即,如果不涉及 polyfill)就是仅设置文档的样式:

// No Polyfill
document.documentElement.style
  .setProperty('--awesome-button-background', newVal ? 'grey' : 'white');
Enter fullscreen mode Exit fullscreen mode

这些都是人为的例子。在现实世界中,你更有可能完全用 CSS 来解决问题,例如:

:host { background: white; }
:host([disabled]) { background: grey; }
Enter fullscreen mode Exit fullscreen mode

但是,如果您想根据触摸事件旋转色调或根据 websocket 更新转换元素,那么 CSS 自定义属性就是可行的方法。

ShadyCSS 提供了一些其他功能,例如针对现已弃用的@applyCSS 语法的填充程序,但我们不会介绍它们,因为该规范已经失效。

ShadyCSS polyfill也有一些已知的限制。剧透:

  • 由于 ShadyCSS 删除了所有<slot>元素,您无法直接选择它们,因此您必须使用一些上下文包装器,例如.context ::slotted(*)
  • 由于 polyfill 仅模拟封装,因此文档样式可能会泄漏到您的隐藏树中。

有关已知限制的详细信息,请参阅README

ShadyCSS 概要:

因此,基本上,即使在旧版浏览器和 Edge 上,只要你

  • 在元素中定义元素的样式<template>
  • 考虑元素阴影槽位时要考虑 polyfill;在元素中进行适当的修改connectedCallback;并且
  • ShadyCSS.styleDocument使用或动态更新 CSS 自定义属性ShadyCSS.styleSubTree,或者使用其他基于 CSS 的解决方案来避免该问题。

自定义元素 Polyfill

定义元素 polyfill使用自定义元素规范中的 API 修补了几个 DOM 构造函数:

  • HTMLElement获取自定义元素回调,如connectedCallbackattributeChangedCallback(我们将在下一篇文章中详细讨论)。在其原型上。
  • ElementgetsattachShadow和类似方法setAttribute以及innerHTMLsetter 被修补以与 polyfilled 自定义元素回调一起使用。
  • Node类似的DOM API 也appendChild进行了类似的修补
  • 等人Document#createElement 得到类似的待遇。

它还公开了customElements对象window,以便您可以注册您的组件。

polyfill 会在 之后升级自定义元素DOMContentLoaded,然后初始化 以MutationObserver升级随后使用 JavaScript 附加的任何自定义元素。

支持 IE11

当有人告诉我他们需要支持 IE11 时我的感受如何。

<rant>

虽然 polyfill 支持 IE11,但并非总是那么美好。IE11 已不再由微软开发,这意味着它不应该被使用。决定支持 IE11 意味着增加开发时间、增加复杂性、增加 bug 的几率,以及让用户接触到漏洞百出的过时浏览器。任何时候提出支持 IE11 的要求时,都必须仔细评估。不要仅仅把它当成“有就好”。有它可不是件好事。如果不是基于不可避免的情况而提出的绝对要求,最好根本不支持它。

</rant>

。好的,继续表演。

根据规范,自定义元素必须使用 JavaScript 定义class,但 IE11 永远不会支持 ES6 的该功能。因此,我们必须使用 Babel 或类似的工具将类转译为 ES5。如果您使用的是Polymer CLI,则可以选择将 JS 转译为 ES5。

在理想情况下,您将构建两个或更多版本的网站:

  1. class使用关键字和 es2015+ 功能为常青/现代浏览器编写
  2. function使用关键字类转换为 ES5
  3. 以及您想要支持的任何其他色调。

然后,您将为您的应用程序提供差异化​​服务,将快速、轻便、现代的代码发送给有能力的用户代理,将缓慢、转换、遗留的代码发送给旧浏览器。

但这并非总是可行。如果你有简单的静态托管,并且需要为所有浏览器构建一个包,那么你将被迫转译到 ES5,而这与原生customElements实现不兼容。

对于这样的情况,polyfill为本机 customElements 实现提供了一个垫片,该垫片支持 ES5 样式的function关键字元素。如果您使用相同的捆绑包定位新旧浏览器,请确保将其包含在您的构建中(不要转换此文件!)。

<script src="/node_modules/@webcomponents/webcomponentsjs/entrypoints/custom-elements-es5-adapter-index.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
Enter fullscreen mode Exit fullscreen mode

活跃的 web-components 社区成员@ruphin提出了一个巧妙的技巧,即使在静态主机上也可以提供一种差异化服务,那就是利用浏览器的nomodule功能:

<!-- This loads the app as a module on Chrome, Edge, Firefox, and Safari -->
<!-- Modules are always nonblocking, and they load after regular scripts, so we can put them first -->
<script type="module" src="/index.js"></script>

<!-- This loads the app on IE11 -->
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js"></script>
<!-- Take a look at rollup.config.js to see how to build this guy -->
<script nomodule src="./index.nomodule.js"></script>
Enter fullscreen mode Exit fullscreen mode

查看他的轻量级 Web 组件框架 gluonjs

GitHub 徽标 ruphin / gluonjs

轻量级的 Web Component 基础

Gluonjs

构建状态 NPM 最新版本 代码风格:Prettier

用于构建 Web 组件和应用程序的轻量级库


  • 基于平台: GluonJS 旨在充分利用最新的 Web 平台功能,使其体积极小,并在现代浏览器上拥有卓越的性能。此外,这意味着构建/编译步骤是可选的;GluonJS 组件无需任何预处理即可在现代浏览器上运行。
  • 组件模型:构建封装逻辑和样式的组件,然后组合它们以创建复杂的界面。使用 Web 组件标准,所有相关 API 均直接向开发者开放。
  • 高度可复用:由于 GluonJS 创建的 Web 组件符合标准,因此您几乎可以在任何现有应用程序中使用由 GluonJS 创建的组件。请查看Custom Elements Everywhere,获取与现有框架的最新兼容性表。
  • 强大的模板: GluonJS 使用lit-html进行模板,使其具有高度的表现力和灵活性。

概念

import { GluonElement } from '/node_modules/@gluon/gluon/gluon.js';
class MyElement extends GluonElement {
  // ...
}

customElements.define(MyElement.is, MyElement
Enter fullscreen mode Exit fullscreen mode

结论

webcomponentsjs polyfill 让你可以在旧版浏览器中运行你的 WebComponents。诚然,你需要克服一些障碍才能使其正常工作,但如果你使用 WebComponent 辅助库来定义元素,那么这些障碍基本上都会被解决。

在我们的下一篇文章中,如果上帝愿意的话,我们将探索使用原始浏览器 API 编写 Web 组件,以实现最大程度的控制和互操作性。

勘误表

  • 本文的先前版本建议像这样在模块中导入 polyfill:import '@webcomponents/webcomponentsjs/webcomponents-loader.js';请勿这样做。相反,应该在加载任何其他模块之前,在 document 中加载 polyfill head。本文已通过更新的示例进行了更正。
  • 本文的先前版本建议不要加载特定的 polyfill。最新版本更深入地解释了为什么以及何时应该加载 polyfill。
  • 本文之前的版本使用了this.shadowRoot.append,它适用于支持的浏览器。建议使用this.shadowRoot.appendChild,它也能与 polyfill 兼容。
  • 本文之前的版本展示了如何在connectedCallback不先检查影子根是否已存在的情况下附加影子根的示例。这些示例现已更新。
  • 自本文最初发布以来,微软已开始在 Edge 中开发 Web 组件标准。狂欢时间到了!

查看本系列的下一篇文章

您想参加关于这里涉及的任何主题的一对一辅导课程吗?通过 Codementor 联系我

鏂囩珷鏉ユ簮锛�https://dev.to/bennypowers/lets-build-web-components-part-2-the-polyfills-dkh
PREV
Promise 链非常棒
NEXT
通过从头构建来理解数组 reduce