如何用 peerDependencies 声明 Lexical Extension 之间的可选依赖?
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical
当你为自己的编辑器框架写一个 Lexical Extension 时,经常会遇到两个问题:你的扩展只在某个其他扩展存在时才能提供完整功能(例如只有装了 Markdown 扩展才需要序列化代码),或者两个扩展互相引用会形成循环导入。Lexical Extensions 的peerDependencies属性就是为这类"按名称、可选"的依赖设计的:它不要求对方一定在场,但声明之后你可以在运行时发现它,并在对方确实被构建进编辑器时覆盖它的配置。
本文基于 Lexical 仓库中的 Peer Dependencies 文档、Defining Extensions 文档 以及lexical、@lexical/extension包的源码与测试,给出声明、读取和验证 peer 依赖的完整操作路径。
什么情况下应该用 peerDependencies
文档对peerDependencies的定位是:按名称(by name)声明的可选扩展数组。它与dependencies的关键区别:
dependencies是按引用(by reference)的直接依赖,所有依赖必须构成有向无环图(DAG),不允许任何依赖路径从某个扩展绕回自身,否则会抛出类似LexicalBuilder: Circular dependency detected for Extension A from B的错误;peerDependencies不是硬性要求。声明它们是为了让扩展"在运行时找到它们,并在对方被构建进编辑器时覆盖其配置"。由于是可选且按名称的,扩展之间的 peer 关系图不受 DAG 约束,允许循环。
文档明确给出的两个典型用例(见 peer-dependencies.md):
- 你的扩展需要 React 相关代码才能完全工作,但脱离 React 也能用——于是把
ReactProviderExtension放进peerDependencies,只在宿主是 React 时才启用相关功能; - 你的扩展提供了自定义节点类型,想为它提供 Markdown 序列化/导入,但仅当
MarkdownExtension在该编辑器中存在时才生效。
文档同时说明这是一个"实践中很少需要的高级用法,典型目的是避免直接导入或依赖循环"。如果你只是简单引用另一个扩展,用dependencies即可。
用 declarePeerDependency 声明 peer 依赖
peerDependencies数组的元素推荐通过核心模块lexical中的defineExtension、configExtension、declarePeerDependency等函数来构造,其中declarePeerDependency专门用于"按名称声明对另一个扩展的间接依赖"并附带类型推断(见 intro.md 的 Core API Overview)。
其实现与签名定义在 defineExtension.ts:第一个参数是扩展名称(类型上必须是该扩展的name),第二个参数是可选的配置覆盖对象。函数返回NormalizedPeerDependency<Extension>,即一个[name, config?]元组。
文档中给出的标准写法如下。注意这里刻意使用import type/typeof import()——peer 依赖最常见的目的就是避免运行时导入,所以只引入类型而不引入值:
import type {FooExtension} from "foo"; export const PeerExtension = defineExtension({ name: 'PeerExtension', peerDependencies: [ declarePeerDependency<FooExtension>("foo"), declarePeerDependency<typeof import("bar").BarExtension>("bar", {config: "bar"}), ], });第二处还演示了声明 peer 的同时附带给对方的配置覆盖({config: "bar"}):只要BarExtension真正被构建进编辑器,这份配置就会参与它的配置合并。
仓库中的真实例子是ReactExtension(ReactExtension.tsx)。它并不想对ReactProviderExtension形成直接依赖,于是用 peer 声明,源码中的注释也说明了这一动机:
name: '@lexical/react/React', peerDependencies: [ // We are not trying to avoid the import, just the direct dependency, // so using the extension directly is fine. declarePeerDependency<typeof ReactProviderExtension>( ReactProviderExtension.name, ), ],在扩展构建阶段读取 peer
声明 peer 只是登记了名称,真正的使用发生在构建阶段。Lexical Extensions 的构建按config→mergeConfig→init→build→register→afterRegistration的顺序进行(见 defining-extensions.md 的 Phases 一节)。文档说明init阶段"可以引用 peer 的配置、计算build期间需要的数据"。
ReactExtension展示了在build阶段按名称查询 peer 的完整路径:先查询,不存在时抛出明确的 invariant 错误,错误信息还列出了哪些扩展依赖了它:
build(editor, config, state) { const providerPeer = state.getPeer<typeof ReactProviderExtension>( ReactProviderExtension.name, ); if (!providerPeer) { invariant( false, 'No ReactProviderExtension detected. You must use ReactPluginHostExtension or LexicalExtensionComposer to host React extensions. The following extensions depend on ReactExtension: %s', [...state.getDirectDependentNames()].join(' '), ); } // ... }如果 peer 是可选的(这正是 peer 依赖的常态),查询到undefined就是正常的"对端不在场"信号,此时跳过相关功能即可,不需要抛错。
只有 editor 引用时按名称解析 peer
在LexicalNode的实现里,你往往拿不到构建阶段的state,只拿得到editor。@lexical/extension为此提供了两个函数(getPeerDependencyFromEditor.ts):
getPeerDependencyFromEditor:按名称获取该扩展最终化的 config 与 output,找不到时返回undefined;getPeerDependencyFromEditorOrThrow:找不到时抛出 invariant 错误(getPeerDependencyFromEditorOrThrow: Editor was not built with Extension %s),适用于你确定 peer 必须在场的场合。
源码注释给出的用法示例(需要同时显式传入扩展类型和名称字符串):
import type { EmojiExtension } from "./EmojiExtension"; export class EmojiNode extends TextNode { // other implementation details not included createDOM( config: EditorConfig, editor?: LexicalEditor | undefined ): HTMLElement { const dom = super.createDOM(config, editor); addClassNamesToElement( dom, getPeerDependencyFromEditorOrThrow<typeof EmojiExtension>( editor || $getEditor(), "@lexical/playground/emoji", ).config.emojiClass, ); return dom; } }注意名称字符串必须与 peer 扩展的name属性完全一致(例如@lexical/playground/emoji),因为解析依赖LexicalBuilder内部的extensionNameMap按名称查找。
验证:peer 配置覆盖是否生效
验证方式直接参考仓库中的单元测试 LexicalBuilder.test.ts。它构造了一个带 peer 配置覆盖的扩展,与直接配置合并后检查最终 config。测试代码(以下为测试文件中的真实用例):
const PeerExtension = defineExtension({ name: 'Peer', peerDependencies: [ declarePeerDependency<typeof PairExtension>('Pair', {second: 'peer'}), ], }); expect( configOf( configExtension(PairExtension, {first: 'direct'}), PeerExtension, ), ).toEqual({first: 'direct', second: 'peer'});该测试断言的结果是:来自configExtension的直接配置{first: 'direct'}与来自 peer 声明的配置{second: 'peer'}同时保留,合并为{first: 'direct', second: 'peer'}——即 peer 的配置覆盖与直接配置各管各的字段,互不冲突。
同一测试文件中还有更完整的构建期校验("handles peer dependency configuration" 用例):用buildEditorFromExtensions(ExtensionA, ConfigExtension)构建编辑器后,通过LexicalBuilder.fromEditor(editor)拿到 builder,再用builder.sortedExtensionReps()检查扩展的最终排序与合并后的 config(期望为{a: 1, b: 'A'},其中b: 'A'来自declarePeerDependency<typeof ConfigExtension>('Config', {b: 'A'})的覆盖)。你可以照这个模式写自己的测试:构建带/不带 peer 的两种编辑器,分别断言 peer 的覆盖是否参与合并、对端缺席时功能是否按你的 fallback 行为走。
限制与边界
- 可选性与命名:peer 依赖按名称匹配,名称对不上等于不存在;声明 peer 不会自动把对方拉进编辑器,对端必须出现在
dependencies或构建参数中才会在场。 - 循环约束不同:直接
dependencies必须是 DAG,而 peer 关系图允许循环。用 peer 打破循环导入时,要确保自己的直接依赖链不因此产生环,否则构建时抛出Circular dependency detected错误。 - 配置合并策略:peer 附带的配置覆盖走的是目标扩展的
mergeConfig(默认shallowMergeConfig,浅合并)。数组类字段如果希望追加而非覆盖,需要在目标扩展里实现自定义mergeConfig(如仓库中StringArrayExtension示例所示)。 - peer 缺席不是错误:
getPeerDependencyFromEditor返回undefined、state.getPeer查不到都是合法状态;只有当你用getPeerDependencyFromEditorOrThrow或在 build 阶段主动invariant(如ReactExtension那样)时,缺席才会抛错。声明可选功能时不要用OrThrow变体。
下一步
声明了 peer 依赖之后,通常还需要了解扩展的其他高级机制:@lexical/extension的 Signals 文档 介绍如何用信号在运行时动态启停扩展行为;Included Extensions 列出 Lexical 各包内置的扩展及其名称,供你声明 peer 时对照name字符串。
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考