你正在查看 Lit 的旧版本文档。点击 这里查看最新版本。

上下文

This package is part of the Lit Labs family of experimental packages. See the Lit Labs page for guidance on using Labs software in production.

上下文(Context)是一种将数据提供给整个组件子树的方式,无需手动将属性绑定到每个组件。数据是"上下文"可用的,使得数据提供者和数据消费者之间的祖先元素甚至不需要感知它的存在。

Lit 的上下文实现是 Lit Labs 的一部分,可在 @lit-labs/context 包中使用:

npm i @lit-labs/context

上下文适用于需要被大量各种组件消费的数据——如应用程序的数据存储、当前用户、UI 主题——或者当数据绑定不可用时,例如当元素需要向其 light DOM 子元素提供数据时。

上下文与 React 的 Context 或 Angular 等依赖注入系统非常相似,但有一些重要的区别,使上下文能够与 DOM 的动态特性配合工作,并实现跨不同 Web Components 库、框架和纯 JavaScript 的互操作性。

使用上下文涉及一个 上下文对象(有时称为键)、一个_提供者_和一个_消费者_,它们通过上下文对象进行通信。

上下文定义(logger-context.ts):

import {createContext} from '@lit-labs/context';
import type {Logger} from 'my-logging-library';
export type {Logger} from 'my-logging-library';
export const loggerContext = createContext<Logger>('logger');

提供者:

import {LitElement, property, html} from 'lit';
import {provide} from '@lit-labs/context';

import {Logger} from 'my-logging-library';
import {loggerContext} from './logger-context.js';

@customElement('my-app')
class MyApp extends LitElement {

@provide({context: loggerContext})
logger = new Logger();

render() {
return html`...`;
}
}

消费者:

import {LitElement, property} from 'lit';
import {consume} from '@lit-labs/context';

import {type Logger, loggerContext} from './logger-context.js';

export class MyElement extends LitElement {

@consume({context: loggerContext})
@property({attribute: false})
public logger?: Logger;

private doThing() {
this.logger?.log('A thing was done');
}
}

Lit 的上下文基于 W3C Web Components 社区组上下文社区协议

该协议实现了元素之间(甚至非元素代码之间)的互操作性,无论它们是如何构建的。通过上下文协议,基于 Lit 的元素可以向非 Lit 构建的消费者提供数据,反之亦然。

上下文协议基于 DOM 事件。消费者触发一个 context-request 事件,该事件携带它所需的上下文键,其上方的任何元素都可以监听 context-request 事件并为该上下文键提供数据。

@lit-labs/context 实现了这个基于事件的协议,并通过一些响应式控制器和装饰器使其可用。

上下文通过_上下文对象_或_上下文键_来标识。它们是代表通过上下文对象标识共享的潜在数据的对象。你可以将它们类似于 Map 的键来理解。

提供者通常是元素(但可以是任何事件处理代码),为特定的上下文键提供数据。

消费者请求特定上下文键的数据。

当消费者请求某个上下文的数据时,它可以告诉提供者它想要_订阅_上下文的更改。如果提供者有新数据,消费者将被通知并可以自动更新。

上下文的每次使用都必须有一个上下文对象来协调数据请求。此上下文对象表示所提供数据的标识和类型。

上下文对象使用 createContext() 函数创建:

export const myContext = createContext(Symbol('my-context'));

建议将上下文对象放在单独的模块中,以便它们可以独立于特定的提供者和消费者进行导入。

createContext() 接受任何值并直接返回它。在 TypeScript 中,该值被转换为一个类型化的 Context 对象,该对象携带上下文_值_的类型。

如果出现这样的错误:

const myContext = createContext<Logger>(Symbol('logger'));

class MyElement extends LitElement {
@provide({context: myContext})
name: string
}

TypeScript 将警告类型 string 不能赋值给类型 Logger。请注意,此检查目前仅针对公共字段。

上下文对象被提供者用于将上下文请求事件匹配到一个值。上下文使用严格相等(===)进行比较,因此提供者只会在其上下文键与请求的上下文键相等时才处理上下文请求。

这意味着创建上下文对象有两种主要方式:

  1. 使用全局唯一的值,如对象({})或 Symbol(Symbol()
  2. 使用非全局唯一的值,使其在严格相等下可以相等,如字符串('logger')或_全局_ Symbol(Symbol.for('logger'))。

如果你希望两个_独立的_ createContext() 调用引用同一个上下文, 那么使用在严格相等下会相等的键,例如字符串:

// true
createContext('my-context') === createContext('my-context')

但要注意,你应用中的两个模块可能使用相同的上下文键来引用不同的对象。为避免意外冲突,你可能需要使用相对唯一的字符串,例如使用 'console-logger' 而不是 'logger'

通常最好使用全局唯一的上下文对象。Symbol 是最简单的方法之一。

@lit-labs/context 中有两种方式提供上下文值:ContextProvider 控制器和 @provide() 装饰器。

如果你使用装饰器,@provide() 装饰器是提供值的最简单方式。它会为你创建一个 ContextProvider 控制器。

@provide() 装饰一个属性并给出上下文键:

import {LitElement, html} from 'lit';
import {property} from 'lit/decorators.js';
import {provide} from '@lit-labs/context';
import {myContext, MyData} from './my-context.js';

class MyApp extends LitElement {
@provide({context: myContext})
myData: MyData;
}

你可以使用 @property()@state() 使该属性也成为响应式属性,这样设置它时会同时更新提供者元素和上下文消费者。

@provide({context: myContext})
@property({attribute: false})
myData: MyData;

上下文属性通常是私有的。你可以使用 @state() 使私有属性成为响应式的:

@provide({context: myContext})
@state()
private _myData: MyData;

将上下文属性设为公共可以让元素向其子树提供公共字段:

html`<my-provider-element .myData=${someData}>`

ContextProvider 是一个响应式控制器,为你管理 context-request 事件处理器。

import {LitElement, html} from 'lit';
import {ContextProvider} from '@lit-labs/context';
import {myContext, MyData} from './my-context.js';

export class MyApp extends LitElement {
private _provider = new ContextProvider(this, myContext);
}

ContextProvider 可以在构造函数中接受初始值:

private _provider = new ContextProvider(this, myContext, initialData);

或者你可以调用 setValue()

this._provider.setValue(myData);

如果你使用装饰器,@consume() 装饰器是消费值的最简单方式。它会为你创建一个 ContextConsumer 控制器。

@consume() 装饰一个属性并给出上下文键:

import {LitElement, html} from 'lit';
import {consume} from '@lit-labs/context';
import {myContext, MyData} from './my-context.js';

class MyElement extends LitElement {
@consume({context: myContext})
myData: MyData;
}

当此元素连接到文档时,它将自动触发 context-request 事件,获取提供的值,将其赋给属性,并触发元素的更新。

ContextConsumer 是一个响应式控制器,为你管理 context-request 事件的派发。当提供新值时,控制器会使宿主元素更新。提供的值随后可通过控制器的 .value 属性获得。

import {LitElement, property} from 'lit';
import {ContextConsumer} from '@lit-labs/context';
import {Logger, loggerContext} from './logger.js';

export class MyElement extends LitElement {
private _myData = new ContextConsumer(this, myContext);

render() {
const myData = this._myData.value;
return html`...`;
}
}

消费者可以订阅上下文值,这样当提供者有新值时,可以将新值发送给所有已订阅的消费者,使它们更新。

你可以使用 @consume() 装饰器进行订阅:

@consume({context: myContext, subscribe: true})
myData: MyData;

以及 ContextConsumer 控制器:

private _myData = new ContextConsumer(this,
myContext,
undefined, /* callback */
true /* subscribe */
);

最常见的上下文使用场景涉及页面全局的数据,这些数据可能只在页面中的部分组件中需要。如果不使用上下文,大多数或所有组件可能都需要接受并传播这些数据的响应式属性。

应用全局的服务(如日志记录器、分析工具、数据存储)可以通过上下文提供。与从公共模块导入相比,上下文的优势在于它提供的延迟耦合和树作用域。测试可以轻松提供模拟服务,或者页面的不同部分可以使用不同的服务实例。

主题是应用于整个页面或页面内整个子树的样式集合——正是上下文提供数据的那种作用域。

构建主题系统的一种方式是定义一个 Theme 类型,容器可以提供包含命名样式的主题。想要应用主题的元素可以消费主题对象并通过名称查找样式。自定义主题响应式控制器可以包装 ContextProvider 和 ContextConsumer 来减少样板代码。

上下文可用于从父元素向其 light DOM 子元素传递数据。由于父元素通常不创建 light DOM 子元素,它无法利用基于模板的数据绑定向它们传递数据,但它可以监听并响应 context-request 事件。

例如,考虑一个带有不同语言模式插件的代码编辑器元素。你可以使用上下文创建一个用于添加功能的纯 HTML 系统:

<code-editor>
<code-editor-javascript-mode></code-editor-javascript-mode>
<code-editor-python-mode></code-editor-python-mode>
</code-editor>

在这种情况下,<code-editor> 将通过上下文提供一个用于添加语言模式的 API,插件元素将消费该 API 并将自身添加到编辑器中。

Permalink to "数据格式化器、链接生成器等"

有时可复用组件需要以特定于应用程序的方式格式化数据或 URL。例如,一个渲染指向另一个项目的链接的文档查看器。该组件不会知道应用程序的 URL 空间。

在这些情况下,组件可以依赖于一个通过上下文提供的函数,该函数将对数据或链接应用特定于应用程序的格式化。

这些 API 文档是摘要,直到生成的 API 文档可用

创建一个类型化的上下文对象

导入

import {createContext} from '@lit-labs/context';

签名

function createContext<ValueType, K = unknown>(key: K): Context<K, ValueType>;

上下文使用严格相等进行比较。

如果你希望两个独立的 createContext() 调用引用同一个上下文,那么使用在严格相等下会相等的键,例如字符串或 Symbol.for()

// true
createContext('my-context') === createContext('my-context')
// true
createContext(Symbol.for('my-context')) === createContext(Symbol.for('my-context'))

如果你希望上下文是唯一的,以保证不会与其他上下文冲突,请使用在严格相等下唯一的键,如 Symbol() 或对象:

// false
createContext(Symbol('my-context')) === createContext(Symbol('my-context'))
// false
createContext({}) === createContext({})

ValueType 类型参数是此上下文可以提供的值的类型。它用于在其他上下文 API 中提供准确的类型。

一个属性装饰器,向组件添加 ContextProvider 控制器,使其响应来自子组件消费者的任何 context-request 事件。

导入

import {provide} from '@lit-labs/context';

签名

@provide({context: Context})

一个属性装饰器,向组件添加 ContextConsumer 控制器,该控制器将通过上下文协议为该属性检索值。

导入

import {consume} from '@lit-labs/context';

签名

@consume({context: Context, subscribe?: boolean})

subscribe 默认为 false。设置为 true 以订阅上下文提供值的更新。

一个响应式控制器,通过监听 context-request 事件为自定义元素添加上下文提供行为。

导入

import {ContextProvider} from '@lit-labs/context';

构造函数

ContextProvider(
host: ReactiveElement,
context: T,
initialValue?: ContextType<T>
)

成员

  • setValue(v: T, force = false): void

    设置提供的值,如果值发生变化则通知所有已订阅的消费者。force 即使值未变化也会触发通知,这在对象发生深层属性变化时很有用。

一个响应式控制器,通过派发 context-request 事件为自定义元素添加上下文消费行为。

导入

import {ContextConsumer} from '@lit-labs/context';

构造函数

ContextConsumer(
host: HostElement,
context: C,
callback?: (value: ContextType<C>, dispose?: () => void) => void,
subscribe: boolean = false
)

成员

  • value: ContextType<C>

    上下文的当前值。

当宿主元素连接到文档时,它将发出一个带有其上下文键的 context-request 事件。当上下文请求被满足时,控制器将调用回调(如果存在),并触发宿主更新,以便它能响应新值。

当宿主元素断开连接时,它还会调用提供者给出的 dispose 方法。

ContextRoot 可用于收集未满足的上下文请求,并在有满足匹配上下文键的新提供者可用时重新派发它们。这允许提供者在消费者之后添加到 DOM 树中,或在之后升级。

导入

import {ContextRoot} from '@lit-labs/context';

构造函数

ContextRoot()

成员

  • attach(element: HTMLElement): void

    将 ContextRoot 附加到此元素并开始监听 context-request 事件。

  • detach(element: HTMLElement): void

    从该元素上分离 ContextRoot,停止监听 context-request 事件。

消费者触发的用于请求上下文值的事件。此事件的 API 和行为由上下文协议规定。

导入

import {ContextRequestEvent} from '@lit-labs/context';

context-request 会冒泡并且是组合的。

成员

  • readonly context: C

    此事件正在为其请求值的上下文对象

  • readonly callback: ContextCallback<ContextType<C>>

    用于提供上下文值的调用函数

  • readonly subscribe?: boolean

    消费者是否想要订阅新的上下文值

一个由上下文请求者提供的回调,在满足请求的值可用时被调用。

此回调可以被上下文提供者多次调用,因为请求的值可能发生变化。

导入

import {type ContextCallback} from '@lit-labs/context';

签名

type ContextCallback<ValueType> = (
value: ValueType,
unsubscribe?: () => void
) => void;