Lexical依赖版本冲突与标题渲染

一、概述

昨天主要修复了两个bug:一是 PDF/Doc 导出时的依赖冲突导致的崩溃问题,二是文档标题更新后的 UI 同步问题。本文记录问题现象、排查过程、根因分析及解决方案,并对过程中的技术选型与排查方法进行复盘总结。

二、文件导出

导出文件PDF Doc 格式的时候存在id冲突,文件导出失败。

是因为Lexical编辑器基于节点框架设计,存在依赖冲突或多次初始化导致的问题。

依赖库版本不一致,后续也证实了是这个原因,BlockNote官方开源库已经明确说明,依赖版本要保持唯一、保持一致。 多节点重复使用选择 JSON ID · 问题 #1718 · TypeCellOS/BlockNote --- Duplicate use of selection JSON ID multiple-node · Issue #1718 · TypeCellOS/BlockNote

关于这个问题的bug追踪,我学会了通过F12控制台查看调用堆栈,分析错误具体出现在哪几层,事件是如何冒泡的。 ​​​​​​​​​​​​ 从下往上看(按时间顺序):

  • 用户点击:在菜单上点了一下(onInternalClick -> onMenuClick)。
  • 进入代码:程序运行到了 useFileExport.ts 的第 43 行,进入了 exportFile 函数。
  • 触发报错:在执行导出逻辑时,代码内部调用了 Lexical 的选择器逻辑(MultipleNodeSelection.ts)。
  • 报错根源:Selection.jsonID 发现 multiple-node 这个 ID 已经被注册过了。

项目可能同时加载了两个不同版本的 @lexical/selection 或相关插件包。当它们各自尝试注册 MultipleNodeSelection 时,ID 就会发生冲突。

Ctrl+P 查找相关文件,查看编译产物是否存在多份。

如果存在多份,可能是依赖版本冲突,也可能是模块解析路径不一致、代码分割策略或构建配置导致的重复打包。需要进一步查看产物中的版本号或模块路径来确认根本原因。

BlockNote 是基于 Lexical 构建的开源富文本编辑器框架,在 Lexical 基础上做了业务层封装。其底层的Lexical编辑器版本可能存在细微差异,在文档导出时,节点对应的ID发生了冲突。各自注册multiple selection相关包时,ID出现冲突,导致无法正确匹配对应实例。

我排查后发现,是之前引入AI库时做了全局配置分发,没有校验BlockNote开源库的版本一致性,这里涉及到依赖配置问题。 同时pnpm在打包编译和预加载时,也必须保证依赖版本统一。直接采用了pnpm overrides。

因为BlockNote是多层封装的开源库,所以在package.json里无法直接找到对应的Lexical编辑器依赖,需要通过命令行做深度检索和依赖输出。

根本原因:

  • 依赖树分析: 由于 pnpm 的依赖提升(hoisting)机制,以及部分包通过动态 import 懒加载, 导致 @lexical/selection 在不同模块中被解析到了不同的版本路径。 虽然 package.json 中版本范围一致,但实际安装时存在版本漂移或重复实例。

  • 打包逻辑分析: 在 Vite 构建时,由于模块解析(module resolution)的路径规则, 主包和 exporter 模块中的 Lexical 依赖被解析到了不同的 node_modules 路径, 导致运行时加载了多个 Lexical 实例。

  • 运行时分析: 导出操作触发了动态加载 -> 加载了第二个 Lexical 实例 -> 第二个实例尝试在全局注册已存在的 ID -> 崩溃

这次的收获主要是学会看堆栈信息,也考虑一下报错的更多原因。判断编译文件是否存在多个版本,从而快速定位错误。也思考了为什么出现报错的,除了排查代码逻辑,还要思考一下版本冲突等问题。

关于版本管理的问题,比较好的做法我认为应该在CI流程中加入脚本,检查所有 @blocknote/* 包的版本号是否一致。但是现在我还没有着手做这个。目前的package.json也比较少,现在看来自己维护也不是很麻烦。

三、文档标题提取

我一开始认为应该创建一个hook,然后在对应的wikiList(UI层的渲染)里调用这个hook。

但其实不是所有逻辑都适合用hook,写一个纯工具函数也可以。我当时也没太分清hook和utils的区别,自定义hook和utils呢,我个人感觉是有没有涉及到状态变更,像utils其实是没有动态变化的状态要管理的,更多是直接处理数据层。

React 官方文档在 Building Your Own Hooks 中强调:

Custom Hooks let you share stateful logic, not state itself. 自定义 Hook 让你共享有状态的逻辑,而不是状态本身。

关键在"有状态的逻辑"------这意味着自定义 Hook 的本质是封装那些涉及状态管理和副作用的行为。

核心还是要做到UI和逻辑解耦。

然后wiki list要怎么获取当前动态更新的数据呢?

ts 复制代码
 useEffect(() => {
    const handleUpdate = (e: any) => {
      const { id, title } = e.detail;
      setDocs((prevDocs) =>
        prevDocs.map((doc) =>
          doc._id === id ? { ...doc, title: title } : doc,
        ),
      );
    };

    window.addEventListener("WIKI_TITLE_UPDATED", handleUpdate);
    return () => window.removeEventListener("WIKI_TITLE_UPDATED", handleUpdate);
  }, []);

这里用了事件广播,把事件监听挂载到window对象上。这个做法因为直接挂载在浏览器web上,其实不是太优雅,但是胜在简单。

我觉得需要注意的一点是前后端与数据库的交互逻辑:新建了一个标题title之后,title是怎么存入数据库的,页面刷新之后也要保证逻辑正常。

ts 复制代码
 updateDocument(currentDocId, {
          title: note.title,
          content: note.content,
        }).catch(() => {});

新增了title字段。这里bug的验证是打印控制台,看看是否存入后端了,当然应该也可以直接看database的GUI界面。

四、小结

本次修复的两个问题虽然规模不大,但涉及依赖管理、架构设计、技术选型等多个维度。日常开发中,应保持对报错信息的敏感度,深入排查根因而非仅修复表象,同时注重代码的可维护性与可扩展性。

特别声明:本次代码实现仅仅是能跑通功能,并不是优雅的做法,存在设计等层面的缺陷,还请见谅。限于个人经验,文中若有疏漏,还请不吝赐教。

相关推荐
起风了___2 小时前
解决大数据渲染卡顿:Vue3 虚拟列表组件的完整实现方案
前端·程序员
前端fun2 小时前
React如何远程加载组件
前端·react.js
淑子啦2 小时前
React录制视频和人脸识别
javascript·react.js·音视频
代码煮茶2 小时前
Vue3 路由实战 | Vue Router 从 0 到 1 搭建权限管理系统
前端·javascript·vue.js
gaozhiyong08132 小时前
深度技术拆解:豆包2 Pro vs Gemini 3—国产工程派与海外原生派的巅峰对决
前端·spring boot·mysql
JosieBook3 小时前
【C#】C# 访问修饰符与类修饰符总结大全
前端·javascript·c#
遨游建站3 小时前
谷歌SEO之网站内部优化策略
前端·搜索引擎
华洛3 小时前
聊聊我逃离前端开发前的思考
前端·javascript·vue.js
梦梦代码精3 小时前
开源即商用,预期产出、风险与优化建议
人工智能·gitee·前端框架·开源·github