上下文
上下文是一种在不需要手动将属性绑定到每个组件的情况下,使数据对整个组件子树可用的方式。数据是"上下文性"可用的,这意味着数据提供者和消费者之间的祖先元素甚至不知道这些数据的存在。
Lit 的上下文实现在 @lit/context
包中提供:
npm i @lit/context
上下文对于需要被各种各样且数量众多的组件消费的数据非常有用 - 比如应用的数据存储、当前用户、UI 主题 - 或者当数据绑定不是一个选项时,例如当一个元素需要向其 light DOM 子元素提供数据时。
上下文与 React 的 Context 或 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('已完成一件事');
}
}
上下文协议
Permalink to "上下文协议"Lit 的上下文基于 W3C Web Components 社区组的上下文社区协议。
该协议实现了元素(甚至非元素代码)之间的互操作性,无论它们是如何构建的。通过上下文协议,基于 Lit 的元素可以向非 Lit 构建的消费者提供数据,反之亦然。
上下文协议基于 DOM 事件。消费者触发一个携带其想要的上下文键的 context-request
事件,其上方的任何元素都可以监听 context-request
事件并为该上下文键提供数据。
@lit/context
实现了这个基于事件的协议,并通过一些响应式控制器和装饰器使其可用。
上下文对象
Permalink to "上下文对象"上下文由_上下文对象_或_上下文键_标识。它们是代表某些可能通过上下文对象标识共享的数据的对象。你可以将它们视为类似于 Map 键。
提供者通常是元素(但也可以是任何事件处理代码),它们为特定的上下文键提供数据。
消费者请求特定上下文键的数据。
当消费者请求上下文的数据时,它可以告诉提供者它想要_订阅_上下文中的变化。如果提供者有新数据,消费者将被通知并可以自动更新。
定义上下文
Permalink to "定义上下文"每次使用上下文都必须有一个上下文对象来协调数据请求。这个上下文对象代表所提供数据的标识和类型。
上下文对象通过 createContext()
函数创建:
export const myContext = createContext(Symbol('my-context'));
建议将上下文对象放在它们自己的模块中,这样它们就可以独立于特定的提供者和消费者进行导入。
上下文类型检查
Permalink to "上下文类型检查"createContext()
接受任何值并直接返回它。在 TypeScript 中,该值被转换为一个类型化的 Context
对象,它携带着上下文_值_的类型。
在出现这样的错误时:
const myContext = createContext<Logger>(Symbol('logger'));
class MyElement extends LitElement {
@provide({context: myContext})
name: string
}
TypeScript 会警告类型 string
不能赋值给类型 Logger
。注意,这个检查目前只适用于公共字段。
上下文相等性
Permalink to "上下文相等性"提供者使用上下文对象来将上下文请求事件与值匹配。上下文使用严格相等(===
)进行比较,所以提供者只会处理其上下文键等于请求的上下文键的上下文请求。
这意味着创建上下文对象有两种主要方式:
- 使用全局唯一的值,如对象(
{}
)或符号(Symbol()
) - 使用非全局唯一的值,以便在严格相等下相等,如字符串(
'logger'
)或_全局_符号(Symbol.for('logger')
)
如果你想要两个_独立的_ createContext()
调用引用相同的上下文,那么使用在严格相等下相等的键,比如字符串:
// true
createContext('my-context') === createContext('my-context')
但要注意,你的应用中的两个模块可能使用相同的上下文键来引用不同的对象。为了避免意外的冲突,你可能想要使用一个相对唯一的字符串,例如使用 'console-logger'
而不是 'logger'
。
通常最好使用全局唯一的上下文对象。符号是实现这一点最简单的方式之一。
提供上下文
Permalink to "提供上下文"在 @lit/context
中有两种提供上下文值的方式:ContextProvider 控制器和 @provide()
装饰器。
@provide()
Permalink to "@provide()" 如果你使用装饰器,@provide()
装饰器是提供值的最简单方式。它会为你创建一个 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
Permalink to "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);
消费上下文
Permalink to "消费上下文"@consume()
装饰器
Permalink to "@consume() 装饰器" 如果你使用装饰器,@consume()
装饰器是消费值的最简单方式。它会为你创建一个 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
事件,获取提供的值,将其分配给属性,并触发元素的更新。
ContextConsumer
Permalink to "ContextConsumer"ContextConsumer 是一个响应式控制器,它为你管理 context-request
事件的分发。当提供新值时,控制器会导致宿主元素更新。提供的值随后可以在控制器的 .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`...`;
}
}
订阅上下文
Permalink to "订阅上下文"消费者可以订阅上下文值,这样如果提供者有新值,它可以将其提供给所有订阅的消费者,导致它们更新。
你可以使用 @consume()
装饰器进行订阅:
@consume({context: myContext, subscribe: true})
myData: MyData;
以及 ContextConsumer 控制器:
private _myData = new ContextConsumer(this,
{
context: myContext,
subscribe: true,
}
);
当前用户、区域设置等
Permalink to "当前用户、区域设置等"最常见的上下文用例涉及对页面全局的数据,这些数据可能只在整个页面的组件中零星需要。如果没有上下文,可能大多数或所有组件都需要接受和传播这些数据的响应式属性。
应用全局服务,如日志记录器、分析、数据存储,可以通过上下文提供。与从公共模块导入相比,上下文提供的后期耦合和树作用域是一个优势。测试可以轻松提供模拟服务,或者页面的不同部分可以获得不同的服务实例。
主题是应用于整个页面或页面内整个子树的样式集合 - 这正是上下文提供的数据作用域类型。
构建主题系统的一种方式是定义一个容器可以提供的 Theme
类型,该类型包含命名的样式。想要应用主题的元素可以消费主题对象并按名称查找样式。自定义主题响应式控制器可以包装 ContextProvider 和 ContextConsumer 以减少样板代码。
基于 HTML 的插件
Permalink to "基于 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 并将自己添加到编辑器中。
数据格式化器、链接生成器等
Permalink to "数据格式化器、链接生成器等"有时可重用组件需要以应用特定的方式格式化数据或 URL。例如,一个渲染到另一个项目链接的文档查看器。该组件不会知道应用的 URL 空间。
在这些情况下,组件可以依赖于一个通过上下文提供的函数,该函数将对数据或链接应用应用特定的格式化。
这些 API 文档是一个摘要,直到生成的 API 文档可用
createContext()
Permalink to "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()
Permalink to "@provide()" 一个属性装饰器,它向组件添加一个 ContextProvider 控制器,使其响应来自其子消费者的任何 context-request
事件。
导入:
import {provide} from '@lit/context';
签名:
@provide({context: Context})
@consume()
Permalink to "@consume()" 一个属性装饰器,它向组件添加一个 ContextConsumer 控制器,该控制器将通过上下文协议为属性检索值。
导入:
import {consume} from '@lit/context';
签名:
@consume({context: Context, subscribe?: boolean})
subscribe
默认为 false
。将其设置为 true
以订阅上下文提供值的更新。
ContextProvider
Permalink to "ContextProvider" 一个 ReactiveController,它通过监听 context-request
事件向自定义元素添加上下文提供者行为。
导入:
import {ContextProvider} from '@lit/context';
构造函数:
ContextProvider(
host: ReactiveElement,
options: {
context: T,
initialValue?: ContextType<T>
}
)
成员
setValue(v: T, force = false): void
设置提供的值,如果值发生变化,则通知任何订阅的消费者新值。
force
会导致即使值没有改变也发送通知,这在对象有深层属性变化时很有用。
ContextConsumer
Permalink to "ContextConsumer" 一个 ReactiveController,它通过分发 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
Permalink to "ContextRoot" ContextRoot 可用于收集未满足的上下文请求,并在新的提供者可用于匹配上下文键时重新分发它们。这允许在消费者之后将提供者添加到 DOM 树中或升级。
导入:
import {ContextRoot} from '@lit/context';
构造函数:
ContextRoot()
成员
attach(element: HTMLElement): void
将 ContextRoot 附加到此元素并开始监听
context-request
事件。detach(element: HTMLElement): void
从此元素分离 ContextRoot,停止监听
context-request
事件。
ContextRequestEvent
Permalink to "ContextRequestEvent" 由消费者触发以请求上下文值的事件。此事件的 API 和行为由上下文协议指定。
导入:
import {ContextRequestEvent} from '@lit/context';
context-request
冒泡并被组合。
成员
readonly context: C
此事件请求值的上下文对象
readonly contextTarget: Element
发起上下文请求的 DOM 元素
readonly callback: ContextCallback<ContextType<C>>
调用以提供上下文值的函数
readonly subscribe?: boolean
消费者是否想要订阅新的上下文值
ContextCallback
Permalink to "ContextCallback" 由上下文请求者提供的回调,用于接收满足请求的值。
当请求的值发生变化时,上下文提供者可以多次调用此回调。
导入:
import {type ContextCallback} from '@lit/context';
签名:
type ContextCallback<ValueType> = (
value: ValueType,
unsubscribe?: () => void
) => void;