ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Lexical NodeState 全面指南:在任意节点上添加可序列化状态

Lexical NodeState 全面指南:在任意节点上添加可序列化状态 Lexical NodeState 全面指南在任意节点上添加可序列化状态【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexicalNodeState 是 Lexical v0.26.0 引入的一套 API允许开发者以即插即用的方式为任意节点附加状态且该状态自动参与 reconciliation协调、history历史/撤销重做与 JSON 序列化。本文以官方文档 node-state.md 为主线结合 LexicalNodeState.ts 源码与 node-state-style 示例 的完整实现讲解 StateConfig 的创建、读写 API、序列化格式、$config扁平化迁移方案、copy-on-write 效率机制及能力边界帮助你掌握这套零样板zero-boilerplate的节点状态方案。为什么需要 NodeState在 v0.26.0 之前想要在节点上存储额外数据开发者通常需要继承LexicalNode/TextNode等基类声明__property实例变量手工编写constructor、clone、afterCloneFrom、exportJSON、importJSON、updateFromJSON一整套样板代码自行保证序列化、克隆、粘贴copy paste时数据不丢失。NodeState 解决了这一痛点你的应用可以定义任意 key将其存储在任何节点上且 JSON 序列化是自动完成的。这意味着很多场景下你甚至不需要自定义节点子类——状态可以直接挂在现有节点上。特别地NodeState 允许你在RootNode上存储文档级元数据例如文档标题、作者、自定义属性这在旧版本中完全无法实现。与相关 API 的组合使用官方文档明确指出将 NodeState 与以下 API 组合大多数编辑器定制需求都能满足无需走到 Node Customization 那一步Listeners监听状态变化、节点变更Transforms在节点变换流程中读写状态DOMRenderExtension渲染与 HTML 导出DOMImportExtensionHTML 导入。即使你确实在子类化节点用 NodeState 代替额外属性存储数据也更高效并且免去在 constructor、updateFromJSON、exportJSON 中的大量样板代码。核心 API 详解NodeState 的公共 API 由三个函数组成createState、$getState、$setState外加一个辅助函数$getStateChange。它们的实现位于 LexicalNodeState.ts如$getState、$setState并统一从lexical包导出。createState定义状态的 key 与配置createState创建一个StateConfig它定义了 NodeState 值的key与配置const questionState createState(question, { parse: (v) (typeof v string ? v : ), });key 必须是局部唯一的同一节点上不能使用两个具有相同字符串 key 的不同StateConfig。从源码看开发模式下$checkCollision会在$setState时检测冲突并抛出错误LexicalNodeState.ts$setState: State key collision %s detected in %s node with type %s and key %s. Only one StateConfig with a given key should be used on a node.StateConfig的构造函数LexicalNodeState.ts会缓存三个关键值defaultValuestateValueConfig.parse(undefined)的结果只计算一次isEqual默认使用Object.isunparse默认是透传假定值是 JSON 可序列化的。parse 函数的两个职责官方文档强调parse是必填项它有双重作用类型安全 运行时安全地解析 JSON 序列化后的值当被传入undefined或任何无效值时返回默认值——这个默认值可以是undefined、null或你选择的任何值。上面的例子中question 必须是字符串默认值是空字符串。非原始值类型的可选配置unparse / isEqual / resetOnCopyNodeStateValueConfig接口LexicalNodeState.ts定义了四个字段配置项必填默认值说明parse是—解析 JSON 值到类型 V传入undefined时必须返回默认值unparse否透传假定值可 JSON 序列化将 V 转回 JSON 值当 V 不是 JSON 可序列化类型时必填如Date、Map、SetisEqual否Object.is相等性判断当 V 是数组/对象时建议改用fast-deep-equal之类工具以便省略等于默认值的键resetOnCopyNode否false当节点通过$copyNode复制非克隆时是否将该值重置为默认值源码注释还给出一个针对非原始值的高级示例——存储 ISO 日期const isoDateState createState(isoDate, { parse: (v): null | Date { const date typeof v string ? new Date(v) : null; return date !isNaN(date.valueOf()) ? date : null; }, isEqual: (a, b) a b || (a b a.valueOf() b.valueOf()), unparse: (v) v v.toString(), });官方建议为常用数据类型构建一个小型可复用 parse 函数库或使用能生成这类函数的库如 zod、ArkType、Effect、Valibot 等尤其是处理非原始类型时。$getState读取状态$getState从给定节点读取 NodeState 值如果该 key 从未在此节点上设置过则返回默认值const question $getState(pollNode, questionState);$getState的签名还接受可选的第三个参数versionLexicalNodeState.tsNODE_STATE_LATEST默认读取前先调用node.getLatest()符合 Lexical 只操作节点最新版本的习惯NODE_STATE_DIRECT直接读取当前这个节点实例上存储的状态不调用getLatest()。适合读取某个历史版本节点或在updateDOM等场景中使用。源码中定义的这两个常量分别为字符串direct与latestLexicalNodeState.ts。$getStateChange高效比较两份状态如果你需要判断同一节点的两个版本之间状态是否发生变化$getStateChange比分别调用两次$getState再手动比较更高效LexicalNodeState.tsconst change $getStateChange(node, prevNode, questionState); // change null 表示无变化 // 否则为 [value, prevValue]它内部对两个节点都使用NODE_STATE_DIRECT读取并用stateConfig.isEqual判断相等性。典型用途是实现updateDOM也可用于 update listener 或 mutation listener。$setState写入状态$setState在给定节点上设置 NodeState 值LexicalNodeState.tsconst question $setState( pollNode, questionState, Are you planning to use NodeState?, );第三个参数是ValueOrUpdaterV与 React 的useStatesetter 完全一致——可以是值也可以是(prevValue) nextValue更新函数const toggle createState(toggle, {parse: Boolean}); // 直接设置 $setState(node, toggle, true); // 使用更新函数 $setState(node, toggle, (prev) !prev);一个重要细节当使用更新函数且新值与旧值相等按isEqual判断时节点及其 NodeState 不会被标记为 dirtyLexicalNodeState.ts从而避免无谓的 reconciliation 与历史记录写入。为节点类封装 setter 方法源码提供了StateValueOrUpdater类型别名方便你在自定义节点类上封装类型安全的 setterconst fooState createState(foo, { parse: ... }); class MyClass extends TextNode { setFoo(valueOrUpdater: StateValueOrUpdatertypeof fooState): this { return $setState(this, fooState, valueOrUpdater); } }序列化格式默认聚合在 NODE_STATE_KEY$下只要某节点存在非默认值的状态就会被序列化到单个NODE_STATE_KEY值等于字符串$下的 record 中{ type: poll, $: { question: Are you planning to use NodeState? } }toJSON()的实现细节LexicalNodeState.ts已知状态knownState中等于默认值的键会被删除delete state[stateConfig.key]不等于默认值的键通过unparse转换后写入扁平键flatKeys会被提升到顶层若$record 为空则不输出。高级场景非 JSON 可序列化的值默认假定解析后的值可直接 JSON 序列化。但高级场景中你可能使用Date、Map、Set等需要转换的值——此时必须通过unparse定义值 → JSON的转换详见上文 非原始值类型的可选配置。注意undefined 无法序列化为 JSON因此如果你的类型 V 包含undefined它应同时被视为默认值同样如果 V 是函数类型使用$setState时必须用更新函数形式因为函数值与更新函数无法区分。扁平序列化$configflat: true在节点的$config中声明StateConfig并设置flat: true时该 key 会被提升到序列化 JSON 的顶层而不是嵌套在$之下。这与旧式节点把值存为__property实例变量时的 JSON 形状完全一致因此存量 payload 在节点迁移到 NodeState 后仍能无损往返round-trip。例如一个继承TextNode的ColoredNode声明了扁平的color状态$config() { return this.config(colored, { extends: TextNode, stateConfigs: [{flat: true, stateConfig: colorState}], }); }序列化结果为{ type: colored, text: hello, color: red }而同一节点上非扁平默认的状态则会出现在$下{ type: colored, text: hello, $: { color: red } }两种情况下只有当当前值不等于其parse函数返回的默认值时才会输出该 key见 Efficiency。⚠️注意不要复用一个超类已经在序列化的扁平 key例如TextNode上的text。$config/stateConfigs的完整用法见官方文档 nodes.mdx - Creating custom nodes with $config and NodeState示例包括扩展ElementNode、扩展TextNode、扩展DecoratorNode扁平id状态、跨抽象基类共享$config等。将传统 JSON 属性升级为 NodeState一个以__property实例变量存储数据、手工编写exportJSON/importJSON/updateFromJSON的节点可以借助flat: true迁移到 NodeState而不改变序列化 JSON 形状。迁移前——带__color属性与手写序列化的ColoredNodeexport type SerializedColoredNode Spread {color?: string}, SerializedTextNode ; export class ColoredNode extends TextNode { __color: string; constructor(text: string , color: string DEFAULT_COLOR, key?: NodeKey) { super(text, key); this.__color color; } static getType(): string { return colored; } static clone(node: ColoredNode): ColoredNode { return new ColoredNode(node.__text, node.__color, node.__key); } static importJSON(serializedNode: SerializedColoredNode) { return new ColoredNode().updateFromJSON(serializedNode); } updateFromJSON(serializedNode: SerializedColoredNode) { const self super.updateFromJSON(serializedNode); self.__color typeof serializedNode.color string ? serializedNode.color : DEFAULT_COLOR; return self; } exportJSON(): SerializedColoredNode { return { ...super.exportJSON(), color: this.__color DEFAULT_COLOR ? undefined : this.__color, }; } }迁移后——线上 JSON 不变零手写序列化const colorState createState(color, { parse: (v) (typeof v string ? v : DEFAULT_COLOR), }); export class ColoredNode extends TextNode { $config() { return this.config(colored, { extends: TextNode, stateConfigs: [{flat: true, stateConfig: colorState}], }); } }迁移后不再需要exportJSON、importJSON、updateFromJSON、clone、afterCloneFrom中的任何覆写$config自动安装clone与importJSON而LexicalNode/TextNode基类上的exportJSON/updateFromJSON/afterCloneFrom已经支持 NodeState 往返。读取与写入改为// 读 const color $getState(node, colorState); // 写 $setState(node, colorState, red);无缝迁移要点保持 JSON key 名称不变——对扁平状态而言就是createState的第一个参数。从非扁平 NodeState 迁移到扁平 NodeState 同样可行$NODE_STATE_KEY下的状态在配置为扁平时仍会被解析且当两者同时存在时扁平值优先。效率机制Copy-on-Write写时复制NodeState 采用copy-on-write方案管理每个节点的状态如果没有任何状态发生变化NodeState 实例会在该节点的多个版本之间共享。官方文档补充的关键细节在给定的 reconciliation 周期中Lexical 节点第一次通过getWritable被标记 dirty 时会创建该节点的新实例旧版本的所有属性都被设置到新实例上。NodeState 作为单个属性存储在 NodeState 本身被标记为可写之前不会复制其内部状态。从源码看这一机制由NodeState.getWritable实现仅当关联节点不同时才浅拷贝knownStateMap并复用sharedNodeState而$setState内部先判断值是否变化再调用$getWritableNodeState触发真正的写入LexicalNodeState.ts。这意味着节点被克隆但状态未变的开销极小。序列化时省略默认值序列化为 JSON 时每个 key 只在值不等于默认值时才会被存储可以节省大量空间与带宽特别是文档级元数据、长文本场景。惰性解析只在网络边界解析解析与序列化只在网络边界发生——即与 JSON 或 Yjs 集成时当值来自外部源并发生变化时只在第一次被读取时才解析非外部来源的值从不解析从未被使用的值永远不解析。源码中unknownState字段正是为这一设计服务的JSON 导入时先原样保存为未解析的 recordgetValue首次读取时才解析并移入knownStateLexicalNodeState.ts。能力清单与边界当前已支持定义状态并添加到任意节点包括 RootNode 上的文档级元数据在节点 JSON 中自动序列化该状态支持版本化与复制粘贴与 reconciler 协同状态不同的 TextNode 不会被隐式合并见nodeStatesAreEquivalentLexicalNodeState.tslexical/yjs 支持NodeState 会像其他属性一样自动同步未使用的 NodeState 值直接透传pass-through当同一份数据被多种配置使用例如编辑器新旧版本共存、不同插件集时旧代码不会抹掉新代码写入的元数据——这正是unknownState设计的目的LexicalNodeState.ts 注释预注册系统节点可通过$config声明期望的状态并将其序列化为顶层属性flat可与 DOMRenderExtension 集成编辑器渲染与 HTML 导出可与 DOMImportExtension 集成HTML 导入。未来方向 / 已知限制尚不支持直接与 Yjs 集成例如你不能把Y.Map作为 NodeState 值存储。实战示例node-state-style官方文档末尾指向的 node-state-style 示例 展示了 NodeState 的一个高级用法在 TextNode 上用 NodeState 存储样式对象style object。该示例的 README 说明它演示了如何用 NodeState 配合DOMRenderExtension覆盖任意节点的创建与导出行为以及用DOMImportExtension在导入 HTML 时捕获任意内联style属性。其核心代码在 styleState.ts1. 定义带 parse / unparse / isEqual 的完整 StateConfig——因为样式是对象非原始类型三个配置都用上了export const styleState createState(style, { isEqual, parse, unparse, });其中parse把 CSS 字符串解析成StyleObjectunparse把StyleObject序列化回排序后的 CSS 字符串isEqual做深比较以省略默认值。2. 基于 $getState / $setState 封装便捷读写函数export function $getStyleObject(node: LexicalNode): StyleObject { return $getState(node, styleState); } export function $setStyleObjectT extends LexicalNode( node: T, valueOrUpdater: ValueOrUpdaterStyleObject, ): T { return $setState(node, styleState, valueOrUpdater); }示例还展示了用getStyleObjectDirect(node)读取直接版本状态$getState(node, styleState, direct)、用更新函数实现$setStyleProperty/$removeStyleProperty、以及diffStyleObjects/mergeStyleObjects等工具。3. 将状态接入 DOM 渲染与导入通过StyleStateExtensiondefineExtension把DOMRenderExtension的$decorateDOM用 diff 方式应用样式与$exportDOM导出样式到 HTML以及DOMImportExtension的通配导入规则注册进编辑器styleState.ts并在 App.tsx 中注册该扩展。这完整印证了文档中NodeState 可与 DOMRenderExtension / DOMImportExtension 集成的能力项。本地运行pnpm i pnpm run dev总结NodeState 为 Lexical 提供了一种声明式、可组合的节点状态方案createState定义 key 与解析规则$getState/$setState完成读写JSON 序列化、克隆、历史记录与 Yjs 同步全部自动完成$configflat: true则让传统__property节点可以零形状变化地平滑迁移。配合 copy-on-write 的共享机制与惰性解析它同时兼顾了内存效率与序列化带宽。对于希望减少节点子类化样板代码、或在任何节点乃至 RootNode上自由扩展数据的开发者NodeState 是优先考虑的方案。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表