上下文
上下文是一种使数据可用于整个组件子树而不必手动将属性绑定到每个组件的方式。数据是“上下文”可用的,这样在数据提供者和数据消费者之间的祖先元素甚至不知道它。
Lit 的上下文实现可在 @lit/context
包中使用
npm i @lit/context
上下文对于需要由多种类型和大量组件消费的数据很有用 - 比如应用程序的数据存储、当前用户、UI 主题 - 或者当数据绑定不是选项时,比如当元素需要为其 light DOM 子元素提供数据时。
上下文与 React 的上下文或 Angular 的依赖注入系统非常相似,但也有一些重要区别,这些区别使上下文能够与 DOM 的动态特性协同工作,并实现跨不同 Web 组件库、框架和纯 JavaScript 的互操作性。
使用上下文涉及一个上下文对象(有时称为键)、一个提供者和一个消费者,它们通过上下文对象进行通信。
上下文定义(logger-context.ts
)
import {createContext} from '@lit/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/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/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/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
。请注意,此检查目前仅针对公共字段。
上下文相等性
“上下文相等性”的永久链接提供者使用上下文对象来匹配上下文请求事件与值。上下文使用严格相等性 (===
) 进行比较,因此提供者只会处理其上下文键等于请求上下文键的上下文请求。
这意味着有两种主要方法可以创建上下文对象
- 使用全局唯一的 value,比如一个对象 (
{}
) 或符号 (Symbol()
) - 使用一个在严格相等性下可以相等的 value,比如一个字符串 (
'logger'
) 或全局符号 (Symbol.for('logger')
)。
如果你想让两个独立的 createContext()
调用引用相同的上下文,那么使用一个在严格相等性下会相等的键,比如字符串
// true
createContext('my-context') === createContext('my-context')
但要注意,应用程序中的两个模块可以使用相同的上下文键来引用不同的对象。为了避免意外冲突,你可能需要使用一个相对唯一的字符串,例如 'console-logger'
而不是 'logger'
。
通常最好使用全局唯一的上下文对象。符号是最简单的方法之一。
提供上下文
“提供上下文”的永久链接在 @lit/context
中,有两种方法可以提供上下文值:ContextProvider 控制器和 @provide()
装饰器。
@provide()
“@provide()”的永久链接 如果你使用装饰器,@provide()
装饰器是提供值的 easiest 方法。它会为你创建一个 ContextProvider 控制器。
使用 @provide()
装饰属性,并赋予它上下文键
import {LitElement, html} from 'lit';
import {property} from 'lit/decorators.js';
import {provide} from '@lit/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
“ContextProvider”的永久链接ContextProvider
是一个响应式控制器,它为你管理 context-request
事件处理程序。
import {LitElement, html} from 'lit';
import {ContextProvider} from '@lit/context';
import {myContext} from './my-context.js';
export class MyApp extends LitElement {
private _provider = new ContextProvider(this, {context: myContext});
}
ContextProvider 可以将初始值作为构造函数中的选项
private _provider = new ContextProvider(this, {context: myContext, initialValue: myData});
或者你可以调用 setValue()
this._provider.setValue(myData);
消费上下文
“消费上下文”的永久链接@consume()
装饰器
“@consume() 装饰器”的永久链接 如果你使用装饰器,@consume()
装饰器是消费值的 easiest 方法。它会为你创建一个 ContextConsumer 控制器。
使用 @consume()
装饰属性,并赋予它上下文键
import {LitElement, html} from 'lit';
import {consume} from '@lit/context';
import {myContext, MyData} from './my-context.js';
class MyElement extends LitElement {
@consume({context: myContext})
myData: MyData;
}
当此元素连接到文档时,它会自动触发 context-request
事件,获取提供的 value,将其分配给属性,并触发元素的更新。
ContextConsumer
“ContextConsumer”的永久链接ContextConsumer 是一个响应式控制器,它为你管理调度 context-request
事件。该控制器将导致主机元素在提供新值时进行更新。然后,提供的 value 在控制器的 .value
属性中可用。
import {LitElement, property} from 'lit';
import {ContextConsumer} from '@lit/context';
import {myContext} from './my-context.js';
export class MyElement extends LitElement {
private _myData = new ContextConsumer(this, {context: myContext});
render() {
const myData = this._myData.value;
return html`...`;
}
}
订阅上下文
“订阅上下文”的永久链接消费者可以订阅上下文值,以便如果提供者有新值,它可以将其提供给所有订阅的消费者,从而导致它们更新。
你可以使用 @consume()
装饰器订阅
@consume({context: myContext, subscribe: true})
myData: MyData;
以及 ContextConsumer 控制器
private _myData = new ContextConsumer(this,
{
context: myContext,
subscribe: true,
}
);
示例用例
“示例用例”的永久链接当前用户、区域设置等
“当前用户、区域设置等”的永久链接最常见的上下文用例涉及全局于页面,并且可能仅在页面中的组件中稀疏使用的数据。如果没有上下文,大多数或所有组件可能都需要接受和传播数据的响应式属性。
应用程序全局服务(如日志记录器、分析、数据存储)可以通过上下文提供。与从通用模块导入相比,上下文的优势在于它提供的延迟耦合和树范围。测试可以轻松地提供模拟服务,或者页面的不同部分可以提供不同的服务实例。
主题是一组应用于整个页面或页面内整个子树的样式 - 正是上下文提供的范围数据。
构建主题系统的一种方法是定义一个Theme
类型,容器可以提供该类型来保存命名样式。想要应用主题的元素可以消费主题对象并按名称查找样式。自定义主题反应式控制器可以包装ContextProvider和ContextConsumer来减少样板代码。
基于 HTML 的插件
“基于 HTML 的插件”的永久链接上下文可用于将数据从父级传递到其 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 并将自身添加到编辑器。
数据格式化器、链接生成器等
“数据格式化程序、链接生成器等”的永久链接有时,可重用组件需要以应用程序特定的方式格式化数据或 URL。例如,渲染指向另一个项目的链接的文档查看器。该组件将不知道应用程序的 URL 空间。
在这些情况下,该组件可以依赖于上下文提供的函数,该函数将对数据或链接应用应用程序特定的格式。
API
“API”的永久链接在生成 API 文档可用之前,这些 API 文档是一个摘要。
createContext()
“createContext()”的永久链接 创建一个类型化的 Context 对象。
导入:
import {createContext} from '@lit/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 中提供准确的类型。
@provide()
“@provide()”的永久链接 一个属性装饰器,它将 ContextProvider 控制器添加到组件中,使其响应来自其子级消费者的任何context-request
事件。
导入:
import {provide} from '@lit/context';
签名:
@provide({context: Context})
@consume()
“@consume()”的永久链接 一个属性装饰器,它将 ContextConsumer 控制器添加到组件中,该控制器将通过上下文协议为该属性检索一个值。
导入:
import {consume} from '@lit/context';
签名:
@consume({context: Context, subscribe?: boolean})
subscribe
默认情况下为false
。将其设置为true
以订阅对上下文提供值的更新。
ContextProvider
“ContextProvider”的永久链接 一个反应式控制器,它通过监听context-request
事件,向自定义元素添加上下文提供者行为。
导入:
import {ContextProvider} from '@lit/context';
构造函数:
ContextProvider(
host: ReactiveElement,
options: {
context: T,
initialValue?: ContextType<T>
}
)
成员
setValue(v: T, force = false): void
设置提供的值,如果该值已更改,则通知所有已订阅的消费者新值。
force
即使该值未更改也会导致通知,这在对象具有深层属性更改时很有用。
ContextConsumer
“ContextConsumer”的永久链接 一个反应式控制器,它通过分派context-request
事件,向自定义元素添加上下文消费行为。
导入:
import {ContextConsumer} from '@lit/context';
构造函数:
ContextConsumer(
host: HostElement,
options: {
context: C,
callback?: (value: ContextType<C>, dispose?: () => void) => void,
subscribe?: boolean = false
}
)
成员
value: ContextType<C>
上下文的当前值。
当主机元素连接到文档时,它将使用其上下文键发出context-request
事件。当满足上下文请求时,控制器将调用回调(如果存在)并触发主机更新,以便它可以响应新值。
它还将在主机元素断开连接时调用提供者提供的 dispose 方法。
ContextRoot
“ContextRoot”的永久链接 ContextRoot 可用于收集未满足的上下文请求,并在满足匹配上下文键的新提供者可用时重新分派这些请求。这允许在消费者之后将提供者添加到 DOM 树或升级提供者。
导入:
import {ContextRoot} from '@lit/context';
构造函数:
ContextRoot()
成员
attach(element: HTMLElement): void
将 ContextRoot 附加到此元素并开始监听
context-request
事件。detach(element: HTMLElement): void
从此元素分离 ContextRoot,停止监听
context-request
事件。
ContextRequestEvent
“ContextRequestEvent”的永久链接 消费者为请求上下文值而触发的事件。该事件的 API 和行为由上下文协议指定。
导入:
import {ContextRequestEvent} from '@lit/context';
context-request
冒泡并被组合。
成员
readonly context: C
此事件正在请求值的上下文对象。
readonly callback: ContextCallback<ContextType<C>>
要调用的函数,以提供上下文值。
readonly subscribe?: boolean
消费者是否希望订阅新的上下文值。
ContextCallback
“ContextCallback”的永久链接 一个回调,由上下文请求者提供,并使用满足该请求的值调用。
上下文提供者可以多次调用此回调,因为请求的值已更改。
导入:
import {type ContextCallback} from '@lit/context';
签名:
type ContextCallback<ValueType> = (
value: ValueType,
unsubscribe?: () => void
) => void;