大家好,这里是大家的林语冰。

免责声明

本文属所以语冰的直男翻译了属所以,仅供粉丝参阅,英文原味版请临幸 WRITING COMPONENTS THAT WORK IN ANY FRONTEND FRAMEWORK

浏览器有一种以Web Component(Web 组件)的形式编写可复用组件的内置办法。它们是构建可在任何前端结构中运转的交互式可复用组件的不二法门。话虽如此,编写高度交互且鲁棒的 Web Component 并不简略。

它们需求大量模板文件,而且感觉比您在 React、Svelte 和 Vue 等结构中编写的组件要直观得多。

在本文中,我将表演一个交互式组件编写为 Web Component 的示例,然后运用一个软化边缘并删除大量模板文件的库对其进行重构。

假如您不熟悉 Web Component,也不用担心。鄙人一节中,我将(非常简略且有限地)概述 Web Component 是什么以及它们是由什么组成的。假如您对它们有一些根本经验,则能够跳过下一节。

Web Component 是什么?

在 Web Component 出现之前,浏览器没有编写可复用组件的规范办法。许多库都处理了此问题,但它们常常遇到功用、互操作性和 Web 规范问题等约束。

它们是由 3 种不同的浏览器功用组成的技能:

  • 自界说元素
  • Shadow DOM(影子 DOM)
  • HTML 模板

咱们将对这些技能“走码观猫”,但这绝不是抽丝剥茧。

自界说元素

运用自界说元素,您能够创作自己的自界说 HTML 元素,并能够在整个站点中重复运用这些元素。它们能够像文本、图画或视觉装饰相同简略。您能够更进一步并构建交互式组件、杂乱部件或整个 Web App。

您不仅限于在项目中运用它们,还能够发布它们并允许其他开发者在它们的网站上运用。

以下是我的 A2K 库中的若干可复用组件。您能够看到它们有各种形状和尺寸,而且具有一大坨不同功用。在项目中运用它们与运用任何旧的 HTML 元素类似。

【译】编写兼容全部前端结构的组件

以下是在项目中运用进度条元素的办法:

<!doctype html>
<html>
  <head>
    <title>Quick Start</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <!-- 像一般的内置元素相同在 HTML 中运用 Web Component。 -->
    <a2k-progress progress="50" />
    <!-- a2k web component 运用 JS 模块。 -->
    <script type="module">
      import 'https://cdn.jsdelivr.net/npm/@a2000/progress@0.0.5/lib/src/a2k-progress.js'
    </script>
  </body>
</html>

导入第三方脚本后,您就能够开端像这样运用 a2k-progress 组件,就像其他 HTML 元素相同。

假如您正在构建自己的 Web Component,那么自界说元素的杂乱程度几乎没有约束。我最近创立了一个 Web Component,能够在浏览器中出现 CodeSandbox 代码编辑器。

由于它是一个 Web Component,所以您能够在任何您喜欢的结构中运用它!

Shadow DOM

假如您有 CSS 的运用知识,您就会知道一般 CSS 的效果域是大局的。在你的 global.css 中这样写,如下所示:

p {
  color: tomato;
}

假设没有其他更具体的 CSS 挑选器运用于 p 元素,这会给全部 p 元素提供美丽的橙/赤色。

以此挑选菜单为例:

【译】编写兼容全部前端结构的组件

它具有由视觉规划驱动的鲜明特征。您可能想要运用此组件,但假如您的大局款式影响字体系列、色彩或字体大小等内容,则可能会导致组件的外观出现问题:

<head>
  <style>
    body {
      color: blue;
      font-size: 12px;
      font-family: system-ui;
    }
  </style>
</head>
<body>
  <a2k-select></a2k-select>
</body>

【译】编写兼容全部前端结构的组件

这便是 Shadow DOM 的用武之地。Shadow DOM 是一种封装机制,能够防止 DOM 的其余部分搅扰您的 Web Component,这能够确保 Web App 的大局款式不会搅扰您运用的任何组件。

这也意味着,组件库开发者能够定心编写组件,确保它们在不同的 Web App 中的外观和行为契合预期。

HTML 模板

咱们“走码观猫”的最终一个 Web Component 的功用是 HTML 模板。

该 HTML 元素与其他元素的差异在于,浏览器不会将其内容烘托到页面上。假如您要编写下述的 HTML,您将不会在页面上看到文本“I’m a header”:

<body>
  <template>
    <h1>I'm a header</h1>
  </template>
</body>

模板的内容不是用于直接烘托内容,而是用于仿制。然后能够运用仿制的模板将内容烘托到页面。您能够将模板元素视为 3D 打印的模板。

该模板不是物理实体,但它用于创立现实生活中的克隆。

然后,您能够在 Web Component 中引用模板元素,克隆它,并将克隆烘托为组件符号。

您将鄙人一节中看到,构建 Web Component 的心智模型并不像其他组件结构那样简略粗暴。

根本的 Web Component

现在咱们已经概述了支持 Web Component 的根本技能,下面介绍怎么构建 hello world 组件:

const template = document.createElement('template')
template.innerHTML = `<p>Hello World</p>`
class HelloWorld extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    this.shadowRoot.append(template.content.cloneNode(true))
  }
}
customElements.define('hello-world', HelloWorld)

这是咱们能够编写的最简略的组件,但麻雀虽小,五脏俱全。

于我而言,至少有两个要害原因导致 Web Component 难以编写,至少在 hello world 示例的上下文中是这样。

解耦符号与组件逻辑

在许多结构中,组件符号通常被视为一等公民。

它通常是从组件函数回来的内容,或许能够直接读写组件状况,或许具有内置东西来辅佐操作符号(比方循环、条件等)。

Web Component 的状况并非如此。事实上,符号通常界说在组件类之外。模板也没有内置办法来引用组件的当前状况。

跟着组件杂乱性熵增,这将成为一个头大的约束。

在前端领域,组件旨在辅佐开发者在多个页面中重用符号。因而,符号和组件逻辑有着千丝万缕的联络,它们应该相濡以沫。

编写 Web Component 需求了解其全部底层技能

如上所示,Web Component 由三种技能组成。您还能够在 hello world 代码片段中看到,咱们明确需求了解并理解这三种技能。

  1. 咱们创立了一个模板元素并设置其 innerHTML
  2. 咱们创立了一个shadow root,并显式地将其形式设置为 open
  3. 咱们克隆了模板并将其附加到shadow root
  4. 咱们在文档中注册了一个新的自界说元素

这本质上并没有什么问题,由于 Web Component 应该是“较较低阶”的浏览器 API,这使得它们成为在其上构建笼统的主要东西。

但关于 React 或 Svelte 背景的开发者而言,不得不了解这些新的浏览器功用,然后有必要用它们编写组件,可能会有点头大。

高档 Web Component

让咱们瞄一眼更高档的 Web Component:计数器按钮。

【译】编写兼容全部前端结构的组件

单击该按钮,计数器就会递加。

以下示例包括若干额外的 Web Component 概念,比方生命周期函数和可调查特点。您不需求了解代码片段中发生的全部作业。

这个例子实际上只是用来说明最根本的交互界面(一个计数器按钮)需求多少样板:

const templateEl = document.createElement("template");
templateEl.innerHTML = `
<button>Press me!</button>
<p>You pressed me 0 times.</p>
`;
export class OdysseyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.appendChild(templateEl.content.cloneNode(true));
    this.button = this.shadowRoot.querySelector("button");
    this.p = this.shadowRoot.querySelector("p");
    this.setAttribute("count", "0");
  }
	// 留意: Web components 有生命周期办法,
  // 假如咱们在将组件添加到 DOM 时设置事件侦听器,
  // 当它从 DOM 移除时,整理它们是咱们的作业
  connectedCallback() {
    this.button.addEventListener("click", this.handleClick);
  }
  disconnectedCallback() {
    this.button.removeEventListener("click", this.handleClick);
  }
  // 不同于 React 等结构,当 prop 或 attribute 变化时,Web Component 不会主动烘托。
  // 相反,咱们需求显式界说需求观测的 attribute。
  static get observedAttributes() {
    return ["disabled", "count"];
  }
  // 当上述特点之一变化时,此生命周期办法会运转,
  // 而且咱们对新特点的值作出反应。
  attributeChangedCallback(name, _, newVal) {
    if (name === "count") {
      this.p.innerHTML = `You pressed me ${newVal} times.`;
    }
    if (name === "disabled") {
      this.button.disabled = true;
    }
  }
  // 在 HTML 中,attribute 值总是字符串。
  // 这意味着,咱们需求转化类型。
  // 如下所示,咱们正在转化 string-> number,然后再转化回 string
  handleClick = () => {
    const counter = Number(this.getAttribute("count"));
    this.setAttribute("count", `${counter + 1}`);
  };

作为 Web 组件作者,咱们需求考虑一大坨作业:

  • 设置 shadow DOM
  • 设置 HTML 模板
  • 整理事件监听器
  • 界说想要调查的特点
  • 当 prop 变化时做出呼应
  • 处理 attribute 的类型转化

这并不是说 Web Component 不好或您不应该编写它们,事实上,我以为通过运用它们进行构建,您能够习得一大坨浏览器渠道的知识。

可是,我以为假如您的首要任务是以愈加简化和契合人体工程学的方法编写可互操作的组件,那么有更好的办法来编写组件。

用更少的样板编写 Web Component

如上所述,有一大坨东西能够辅佐您更轻松地编写 Web Component。其间一个东西叫做 Lit,它是由一个谷歌团队开发的。Lit 是一个轻量级库,旨在通过移除上述的样板文件来简化编写 Web Component。

正如咱们将看到的,Lit 在底层做了一大坨繁重作业,以此将代码总行数削减近一半!而且由于 Lit 是 Web Component 和其他原生浏览器功用的包装器,因而您全部关于 Web Component 的现有知识都可转移。

要开端了解 Lit 怎么简化 Web Component,下面是之前的 hello world示例,但已运用 Lit 重构而不是一般 Web Component:

import { LitElement, html } from "lit";
export class HelloWorld extends LitElement {
  render() {
    return html`<p>Hello World!</p>`;
  }
}`
customElements.define('hello-world', HelloWorld);

Lit 组件的样板代码少了许多,而且 Lit 处理我之前说到的两个问题的方法略有不同:

  1. 符号直接界说在组件类中。尽管您能够在类外部界说模板,但通常的做法是从 render 函数回来模板。这更契合其他 UI 结构中出现的心理模型,其间 UI 是状况的函数。
  2. Lit 也不要求开发者附加 shadow DOM,或创立模板和克隆模板元素。尽管了解底层 Web Component 功用有助于开发 Lit 组件,但入门时不需求了解,因而入门门槛要低得多。

最终一步,当咱们将计数器组件迁移到 Lit 会是什么样子呢?

import { LitElement, html } from "lit";
export class OdysseyCounter extends LitElement {
  static properties = {
		// 咱们界说组件的特点以及它们的类型。
		// 当 prop 的值变化时,这会触发组件从头烘托。
		// 尽管它们不相同,但你能够幻想这些“properties”是 Lit 对“可调查 attributes”的代替方案
		// 假如该值作为 attribute 传递,Lit 将其转化为正确类型
    count: { type: Number },
    disabled: { type: Boolean },
  };
  constructor() {
    super();
    // 无需创立 shadow DOM,克隆模板,或存储 DOM 节点引用。
    this.count = 0;
  }
  onCount() {
    this.count = this.count + 1;
  }
  render() {
		// 作为运用 attributeChangedCallback 的替换方案,
		// render 函数能够读写组件的全部特点,
		// 这简化了模板操作的进程。
    return html`
      <button ?disabled=${this.disabled} @click=${this.onCount}>
        Press me!
      </button>
      <p>You pressed me ${this.count} times.</p>
    `;
  }
}`

咱们编写的代码量几乎削减了一半!当创立更杂乱的 UI 时,这种差异愈加明显。

我为什么要继续谈论 Lit?

我是 Web Component 的迷弟,但我认识到,关于许多开发者而言,入门门槛很高。

编写杂乱的 Web Component 需求了解一大坨浏览器功用,而且围绕 Web Component 的教程并不像 React 或 Vue 等其他技能那么全面。

这便是为什么我以为运用像 Lit 这样的东西能够简化编写高功用和可互操作的 Web Component。假如您期望组件在任何前端结构中作业,这非常有用。

友谊赞助

【译】编写兼容全部前端结构的组件

您现在收看的是前端翻译方案,学废了的小伙伴能够订阅此专栏合集,咱们每天佛系投稿,欢迎继续关注前端生态。谢谢大家的点赞,掰掰~