一、概述昨天主要修复了两个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 就会发生冲突。用CtrlP查找相关文件查看编译产物是否存在多份。如果存在多份可能是依赖版本冲突也可能是模块解析路径不一致、代码分割策略或构建配置导致的重复打包。需要进一步查看产物中的版本号或模块路径来确认根本原因。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然后在对应的wikiListUI层的渲染里调用这个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要怎么获取当前动态更新的数据呢useEffect((){consthandleUpdate(e:any){const{id,title}e.detail;setDocs((prevDocs)prevDocs.map((doc)doc._idid?{...doc,title:title}:doc,),);};window.addEventListener(WIKI_TITLE_UPDATED,handleUpdate);return()window.removeEventListener(WIKI_TITLE_UPDATED,handleUpdate);},[]);这里用了事件广播把事件监听挂载到window对象上。这个做法因为直接挂载在浏览器web上其实不是太优雅但是胜在简单。我觉得需要注意的一点是前后端与数据库的交互逻辑新建了一个标题title之后title是怎么存入数据库的页面刷新之后也要保证逻辑正常。updateDocument(currentDocId,{title:note.title,content:note.content,}).catch((){});新增了title字段。这里bug的验证是打印控制台看看是否存入后端了当然应该也可以直接看database的GUI界面。四、小结本次修复的两个问题虽然规模不大但涉及依赖管理、架构设计、技术选型等多个维度。日常开发中应保持对报错信息的敏感度深入排查根因而非仅修复表象同时注重代码的可维护性与可扩展性。特别声明本次代码实现仅仅是能跑通功能并不是优雅的做法存在设计等层面的缺陷还请见谅。限于个人经验文中若有疏漏还请不吝赐教。
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 就会发生冲突。用CtrlP查找相关文件查看编译产物是否存在多份。如果存在多份可能是依赖版本冲突也可能是模块解析路径不一致、代码分割策略或构建配置导致的重复打包。需要进一步查看产物中的版本号或模块路径来确认根本原因。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然后在对应的wikiListUI层的渲染里调用这个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要怎么获取当前动态更新的数据呢useEffect((){consthandleUpdate(e:any){const{id,title}e.detail;setDocs((prevDocs)prevDocs.map((doc)doc._idid?{...doc,title:title}:doc,),);};window.addEventListener(WIKI_TITLE_UPDATED,handleUpdate);return()window.removeEventListener(WIKI_TITLE_UPDATED,handleUpdate);},[]);这里用了事件广播把事件监听挂载到window对象上。这个做法因为直接挂载在浏览器web上其实不是太优雅但是胜在简单。我觉得需要注意的一点是前后端与数据库的交互逻辑新建了一个标题title之后title是怎么存入数据库的页面刷新之后也要保证逻辑正常。updateDocument(currentDocId,{title:note.title,content:note.content,}).catch((){});新增了title字段。这里bug的验证是打印控制台看看是否存入后端了当然应该也可以直接看database的GUI界面。四、小结本次修复的两个问题虽然规模不大但涉及依赖管理、架构设计、技术选型等多个维度。日常开发中应保持对报错信息的敏感度深入排查根因而非仅修复表象同时注重代码的可维护性与可扩展性。特别声明本次代码实现仅仅是能跑通功能并不是优雅的做法存在设计等层面的缺陷还请见谅。限于个人经验文中若有疏漏还请不吝赐教。