Lit

背景

Lit 是一个用于创建 Web 组件的小型库,由 Google 维护。它旨在通过消除样板代码并提供更声明式的语法和重新渲染优化来改善 Web 组件的创作体验,对于有使用流行的基于组件的前端框架经验的开发人员来说应该会感到熟悉。

Mozilla 开发人员于 2021 年开始尝试使用 Lit 构建少量新的 Web 组件。开发人员体验和生产力优势非常明显,因此负责构建新的 可复用组件 库的团队在 2022 年底将 Lit 引入 mozilla-central 以使其可用。现在可以在代码库中的任何位置使用 Lit 创建新的 Web 组件。

使用 Lit

Lit 在其网站上提供了全面的文档,在构建基于 Lit 的自定义元素时应与本文档一起参考: https://lit.npmjs.net.cn/docs/

虽然 Lit 最初是为了帮助可复用组件团队的工作而引入的,但它也可以用于在整个 mozilla-central 中创建可复用和特定于领域的 UI 组件。迄今为止,使用 Lit 创建的自定义元素的一些示例包括 moz-togglemoz-button-group 和凭据管理团队的 login-timeline 组件。

何时使用 Lit

如果您正在构建需要高效响应状态更改的高度反应式元素,则 Lit 可能是特别好的选择。Lit 的声明式 模板反应式属性 可以处理很多确定 UI 的哪些部分应该响应特定更改而更新的工作。

因为 Lit 组件最终只是 Web 组件,所以您可能还想仅仅因为它提供的一些语法而使用它,例如允许您在 JavaScript 代码旁边编写模板代码、在模板中绑定事件监听器和属性,以及自动创建一个开放的 shadowRoot

何时不使用 Lit

在您想要 扩展内置元素 的情况下,无法使用 Lit。Lit 只能用于创建 自主自定义元素,即扩展 HTMLElement 的元素。

使用 Lit 编写组件

除了装饰器之外,Lit 库的所有标准功能都可以在 mozilla-central 中使用,但在将 Lit 用于 Firefox 代码时,您应该注意一些特殊考虑因素和特定文件。

使用外部样式表

尽管 Lit 文档 明确建议不要 使用这种方法,但在 mozilla-central 中,使用外部样式表是为基于 Lit 的组件设置样式的首选方法。他们列出的注意事项与我们的用例不相关,我们已实施平台级解决方法以确保外部样式不会导致未设置样式的内容闪烁。使用外部样式表可以使我们的自动 lint 和审查工具检测到 CSS 更改,并有助于提高 Mozilla 的 desktop-theme-reviewers 小组的可见性。

lit.all.mjs 供应商文件

可以在 toolkit/content/widgets/vendor/lit.all.mjs 中找到一个经过稍微定制的 Lit 供应商版本。 mozilla-central 中的 Lit 版本应用了许多补丁以禁用缩小、源映射和某些警告消息,以及替换 innerHTML 使用 DOMParser 的补丁以及稍微修改 styleMap 指令的行为。有关这些补丁的更多详细信息,以及有关如何更新 lit.all.mjs 的信息,可以 在这里 找到。

因为我们提供的 Lit 版本将一些不同的 Lit 源文件的内容捆绑到一个文件中,所以通常来自不同文件的导入将直接从 lit.all.mjs 中提取。例如,使用 Lit npm 包时,如下所示的导入

// Standard npm package.
import { LitElement } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";

mozilla-central 中将如下所示

// All imports come from a single file (relative path also works).
import { LitElement, classMap, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";

MozLitElementlit-utils.mjs

MozLitElementLitElement 类的一个扩展,它添加了一些功能使其更适合 Mozilla 开发人员的需求。在几乎所有情况下,都应使用 MozLitElement 作为新基于 Lit 的自定义元素的基础类,而不是 LitElement

它可以从 lit-utils.js 中导入并按如下方式使用

import { MozLitElement } from "chrome://global/content/lit-utils.mjs";

class MyCustomElement extends MozLitElement {
    ...
}

MozLitElement 在几个重要方面与 LitElement 不同

它为 shadow DOM 提供自动的 Fluent 支持

在 shadow DOM 中使用 Fluent 时,必须先连接元素的 shadowRoot 才能使用 Fluent。MozLitElement 通过扩展 LitElementconnectedCallback 来处理此问题,以 调用 document.l10n.connectRoot(如果需要)。MozLitElement 还在每次元素更新时自动在 renderRoot 上调用 document.l10n.translateFragment。这些修改的最终结果是,您可以在基于 Lit 的组件中像在 mozilla-central 中的其他任何标记中一样使用 Fluent。

它为本地化的反应式属性提供自动的 Fluent 支持

Fluent 要求如果属性不属于 允许的属性 的默认列表,则必须将其标记为安全。通过在反应式属性的定义中设置 fluent: trueMozLitElement 将自动填充 connectedCallback() 中的 data-l10n-attrs 以将属性标记为 Fluent 安全。

class MyCustomElement extends MozLitElement {
    static properties = {
        label: { type: String, fluent: true },
        description: { type: String, fluent: true },
        value: { type: String },
    };
}

它为标准 Web 属性提供映射的属性助手

当您想在组件级别接受 accesskey、title 或 aria-label 等标准属性,但它实际上应该设置在子元素上时,您可以在属性定义中设置 mapped: true 选项,并且在设置属性时它将从主机中删除。请注意,一旦设置了属性,就无法取消设置。

class MyElement extends MozLitElement {
  static properties = {
    accessKey: { type: String, mapped: true },
  };

  render() {
    return html`<button accesskey=${this.accessKey}>Hello</button>`;
  }
}

它实现了对 Lit 的 @query@queryAll 装饰器的支持

Lit 库包含 @query@queryAll 装饰器,它们提供了一种简单的方法来查找内部组件 DOM 中的元素。这些在 mozilla-central 中不起作用,因为我们不支持 JavaScript 装饰器。相反,MozLitElement 通过在子类上定义静态 queries 属性提供了等效的 DOM 查询功能。例如,以下查询组件的 DOM 以查找某些选择器并将结果分配给不同类属性的 Lit 代码

import { LitElement, html } from "lit";
import { query } from "lit/decorators/query.js";

class MyCustomElement extends LitElement {
    @query("#title");
    _title;

    @queryAll("p");
    _paragraphs;

    render() {
        return html`
            <p id="title">The title</p>
            <p>Some other paragraph.</p>
        `;
    }
}

mozilla-central 中等效于以下代码

import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";

class MyCustomElement extends MozLitElement {
    static queries = {
        _title: "#title", // equivalent to @query
        _paragraphs: { all: "p" }, // equivalent to @queryAll
    };

    render() {
        return html`
            <p id="title">The title</p>
            <p>Some other paragraph.</p>
        `;
    }
}

它添加了一个 dispatchOnUpdateComplete 方法

dispatchOnUpdateComplete 方法提供了一种简单的方法来告知测试代码或其他元素使用者反应式属性更改已生效。它利用 Lit 的 updateComplete promise 在应用所有更新并且组件的 DOM 准备好进行查询后发出事件。例如,当您需要在测试代码中查询 DOM 时,它可能特别有用

// my-custom-element.mjs
class MyCustomElement extends MozLitElement {
    static properties = {
        clicked: { type: Boolean },
    };

    async handleClick() {
        if (!this.clicked) {
            this.clicked = true;
        }
        this.dispatchOnUpdateComplete(new CustomEvent("button-clicked"));
    }

    render() {
        return html`
            <p>The button was ${this.clicked ? "clicked" : "not clicked"}</p>
            <button @click=${this.handleClick}>Click me!</button>
        `;
    }
}
// test_my_custom_element.mjs
add_task(async function testButtonClicked() {
    let { button, message } = this.convenientHelperToGetElements();
    is(message.textContent.trim(), "The button was not clicked");

    let clicked = BrowserTestUtils.waitForEvent(button, "button-clicked");
    synthesizeMouseAtCenter(button, {});
    await clicked;

    is(message.textContent.trim(), "The button was clicked");
});