Yihui’s Blog

第八章:高级概念与Lit生态系统

本章核心问题: 如何优雅地处理异步数据?自定义指令能做什么?Lit 项目需要哪些配套工具(路由、测试、状态管理)?

本章你将了解:

  • Task 控制器管理异步任务状态
  • until() 指令处理异步渲染
  • 自定义指令的应用场景和实现方式
  • Lit 生态系统:路由、测试、状态管理的最佳实践

8.1 异步工作:Task控制器与until()指令

在现代Web应用中,处理异步操作(如从API获取数据)是家常便饭。Lit提供了强大的工具来优雅地管理异步任务的生命周期。

  • @lit/task控制器:这是一个官方的响应式控制器,专门用于管理异步任务的状态 。当你创建一个  

    Task实例并为其提供一个异步函数(如fetch请求)时,它会自动追踪该任务的执行状态:初始(INITIAL)、进行中(PENDING)、成功(COMPLETE)或失败(ERROR)。Task会根据状态变化自动触发宿主组件的更新,并在其render()方法中提供便捷的方式来处理不同状态下的UI呈现。

  • until()指令:这是一个内置指令,用于声明式地处理异步渲染 。它可以接收一个或多个Promise作为参数。在Promise等待(pending)期间,  

    until()会渲染一个你提供的占位符内容(如加载指示器)。一旦Promise成功解析(resolve),它会自动将占位符替换为解析后的值。如果Promise被拒绝(reject),它还可以渲染一个错误状态的UI。这使得在模板中处理异步数据流变得异常简洁。

结合使用Task控制器和until()指令,可以构建出健壮且用户体验良好的数据加载组件。

⚙️ 完整示例:使用 Task 控制器实现数据加载

以下是使用 @lit/task 控制器管理异步数据加载的完整示例:

用户信息组件(user-profile.ts)

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Task } from '@lit/task';

interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
}

@customElement('user-profile')
export class UserProfile extends LitElement {
  static styles = css`
    :host {
      display: block;
      padding: 16px;
      border: 1px solid #ddd;
      border-radius: 8px;
    }

    .loading {
      text-align: center;
      color: #666;
    }

    .error {
      color: #d32f2f;
      padding: 12px;
      background: #ffebee;
      border-radius: 4px;
    }

    .profile {
      display: flex;
      gap: 16px;
      align-items: center;
    }

    .avatar {
      width: 64px;
      height: 64px;
      border-radius: 50%;
    }

    .info h3 {
      margin: 0 0 8px 0;
    }

    .info p {
      margin: 0;
      color: #666;
    }
  `;

  @property({ type: Number }) userId = 1;

  // 使用 Task 控制器管理异步数据加载
  private userTask = new Task(this, {
    // task 函数:接收参数并返回 Promise
    task: async ([userId]) => {
      // 模拟 API 延迟
      await new Promise(resolve => setTimeout(resolve, 1000));

      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`
      );

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const user = await response.json();

      // 模拟添加头像字段
      return {
        ...user,
        avatar: `https://i.pravatar.cc/150?u=${userId}`
      } as User;
    },

    // args 函数:返回 task 函数的参数数组
    // 当这些参数变化时,task 会自动重新执行
    args: () => [this.userId]
  });

  render() {
    // Task 提供 render() 方法处理不同状态
    return this.userTask.render({
      // PENDING 状态:数据加载中
      pending: () => html`
        <div class="loading">
          <p>⏳ 加载用户信息...</p>
        </div>
      `,

      // COMPLETE 状态:数据加载成功
      complete: (user) => html`
        <div class="profile">
          <img class="avatar" src="${user.avatar}" alt="${user.name}" />
          <div class="info">
            <h3>${user.name}</h3>
            <p>${user.email}</p>
            <p><small>用户 ID: ${user.id}</small></p>
          </div>
        </div>
      `,

      // ERROR 状态:数据加载失败
      error: (error) => html`
        <div class="error">
          <strong>❌ 加载失败</strong>
          <p>${error.message}</p>
          <button @click=${() => this.userTask.run()}>重试</button>
        </div>
      `
    });
  }
}

使用示例

<!-- 默认加载用户 1 -->
<user-profile></user-profile>

<!-- 加载用户 3 -->
<user-profile user-id="3"></user-profile>

<!-- 动态切换用户 -->
<script>
  const profile = document.querySelector('user-profile');

  // 切换到用户 5(Task 会自动重新加载)
  setTimeout(() => {
    profile.userId = 5;
  }, 3000);
</script>

Task 控制器的关键特性:

  1. 自动状态管理:自动追踪 INITIAL → PENDING → COMPLETE/ERROR
  2. 响应式参数:args 函数返回的参数变化时,自动重新执行 task
  3. 取消管理:参数变化时,自动取消之前未完成的请求
  4. 声明式渲染:通过 render() 方法优雅处理不同状态的 UI
  5. 手动控制:可通过 task.run() 手动触发重新加载

8.2 扩展模板:自定义指令简介

Lit的模板系统本身已经非常强大,但它还提供了一种更高级的扩展机制:自定义指令。指令是可以自定义表达式渲染行为的函数 。当你需要实现以下功能时,自定义指令会非常有用:  

  • 在多次渲染之间保持状态。

  • 直接、命令式地访问和操作DOM。

  • 在组件的常规渲染周期之外异步更新DOM。

Lit允许创建基于类的指令,通过继承Directive或AsyncDirective基类来实现 。虽然编写自定义指令属于高级用法,但了解其存在,意味着你掌握了一种可以突破  

lit-html常规限制、实现复杂交互和性能优化的终极武器。

8.3 探索更广阔的生态:路由、测试及其他

虽然Lit的核心库保持轻量,但其周围已经形成了一个丰富的生态系统,为构建功能完备的单页应用(SPA)提供了必要的工具。

  • 路由 (Routing):Lit本身不包含路由功能,但社区提供了多种选择。官方的@lit-labs/router是一个实验性的路由库,值得关注 。Vaadin Router是一个成熟的、与框架无关的客户端路由器,与Lit配合良好,广受欢迎 。此外,还有将路由与状态管理结合的方案,如  

    lit-redux-router 。  

  • 测试 (Testing):Lit组件是标准的Web组件,因此可以使用任何支持在真实浏览器环境中运行的JavaScript测试框架进行测试 。官方推荐的工具包括Web Test Runner和WebdriverIO,它们对现代Web特性(如Shadow DOM)有很好的支持 。官方的入门套件已经预配置了完整的测试环境,为开发者提供了一个开箱即用的起点 。  

  • 状态管理 (State Management):对于组件内部状态,Lit的响应式属性已经足够。但对于跨组件的全局状态管理,开发者可以自由选择任何主流的状态管理库,如Redux、MobX或Zustand,它们都能与Lit组件很好地集成 。社区也提供了一些与Lit结合更紧密的库,如  

    @lit-app/state 。  

🧪 测试实战示例

组件(counter-button.ts)

import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('counter-button')
export class CounterButton extends LitElement {
  @state() count = 0;
  render() { return html`<button @click=${() => this.count++}>点击: ${this.count}</button>`; }
}

测试(counter-button.test.ts)

import { expect, fixture, html } from '@open-wc/testing';

it('increments on click', async () => {
  const el = await fixture(html`<counter-button></counter-button>`);
  el.shadowRoot!.querySelector('button')!.click();
  await el.updateComplete;
  expect(el.shadowRoot!.querySelector('button')!.textContent).to.include('1');
});

8.4 下一步:从这里走向何方

恭喜你完成了Lit的基础学习之旅!你现在已经掌握了使用这个现代库构建高效、可维护Web组件的核心技能。Lit的核心哲学是拥抱并增强Web标准,这意味着你所学的知识不仅适用于Lit,也加深了你对整个Web平台的理解。

为了继续你的学习和成长,以下是一些宝贵的资源:

  • 官方文档 (lit.dev):这是最权威、最全面的信息来源,务必常备手边 。  

  • 官方博客 (lit.dev/blog):关注Lit团队的最新动态、版本发布和深度技术文章 。  

  • 社区交流 (Discord):加入官方的Discord服务器,与全球的Lit开发者和核心团队成员交流问题、分享经验 。  

  • awesome-lit资源库:在GitHub上有一个由社区维护的awesome-lit列表,它汇集了大量优秀的教程、工具、组件库和示例项目,是探索Lit生态的绝佳起点 。  

继续构建项目,不断实践,探索Lit生态提供的更多可能性。你已经踏上了一条构建面向未来的Web应用的坚实道路。


📝 本章小结

通过本章学习,你应该了解了:

  1. 异步处理工具:
    • @lit/task 控制器:自动追踪异步任务状态(INITIAL → PENDING → COMPLETE/ERROR),触发组件更新
    • until() 指令:声明式处理 Promise,渲染占位符直到数据加载完成
  2. 自定义指令:当需要在多次渲染间保持状态、直接操作 DOM 或异步更新时,通过继承 Directive 或 AsyncDirective 创建自定义指令
  3. 生态系统工具:
    • 路由:@lit-labs/router(官方实验性)、Vaadin Router(成熟方案)
    • 测试:Web Test Runner、WebdriverIO(支持 Shadow DOM 的真实浏览器测试)
    • 状态管理:Redux、MobX、Zustand 等通用库,或 @lit-app/state 等 Lit 专用库
  4. 学习资源:lit.dev 官方文档、Discord 社区、awesome-lit 资源列表

🎓 学习建议:从官方 starter 模板开始实践,逐步引入路由和状态管理,在真实项目中深化理解。

恭喜你完成 Lit 基础学习!你已经掌握了构建现代 Web 组件的核心技能。

🤔 思考题

  1. Task 控制器和直接在 connectedCallback() 中 fetch 数据有什么本质区别?为什么推荐使用 Task?
  2. 什么场景下需要编写自定义指令?Lit 内置指令(如 repeat()、when())无法满足的需求有哪些?
  3. 为什么 Lit 推荐在真实浏览器环境中测试而不是 JSDOM?Web Components 的哪些特性在模拟环境中无法准确测试?
Maintained by · YihuiEdit on GitHub