ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

如何用 peerDependencies 声明 Lexical Extension 之间的可选依赖?

如何用 peerDependencies 声明 Lexical Extension 之间的可选依赖? 如何用 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第二个参数是可选的配置覆盖对象。函数返回NormalizedPeerDependencyExtension即一个[name, config?]元组。文档中给出的标准写法如下。注意这里刻意使用import type/typeof import()——peer 依赖最常见的目的就是避免运行时导入所以只引入类型而不引入值import type {FooExtension} from foo; export const PeerExtension defineExtension({ name: PeerExtension, peerDependencies: [ declarePeerDependencyFooExtension(foo), declarePeerDependencytypeof import(bar).BarExtension(bar, {config: bar}), ], });第二处还演示了声明 peer 的同时附带给对方的配置覆盖{config: bar}只要BarExtension真正被构建进编辑器这份配置就会参与它的配置合并。仓库中的真实例子是ReactExtensionReactExtension.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. declarePeerDependencytypeof 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.getPeertypeof 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.tsgetPeerDependencyFromEditor按名称获取该扩展最终化的 config 与 output找不到时返回undefinedgetPeerDependencyFromEditorOrThrow找不到时抛出 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, getPeerDependencyFromEditorOrThrowtypeof 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: [ declarePeerDependencytypeof 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来自declarePeerDependencytypeof 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进