使用tiptap-pagination-plus时insertContentAt插入流式Markdown块出现多余字符问题
我之前踩过这个分页插件和流式插入的坑,折腾了好一阵才找到几个可行的解决方向,给你参考下:
1. 核心问题:用字符串长度计算插入位置完全错误
你现在用toScrible.value += newChunk.length来更新下一次插入的位置,这是最可能导致多余字符的原因——Markdown解析成ProseMirror节点后的文档长度,和原Markdown字符串的长度完全不一样。比如你插入# 标题,原字符串长度是4,但解析成Heading节点后,文档中的实际字符长度是2(去掉#和空格),多次累加后位置会严重偏移,导致后续插入到错误的节点位置,产生重复或多余字符。
修改代码:用编辑器选区来获取正确的插入位置
把原来的toScrible.value += newChunk.length替换成:
// 插入后,选区会自动跳到内容末尾,直接取这个位置就是正确的下一次插入点 toScrible.value = tiptapEditor.value.state.selection.to
2. 分页插件的实时计算干扰内容插入
tiptap-pagination-plus会在每次内容变化时自动计算分页、插入分页元素,这个过程会干扰ProseMirror的文档状态,导致插入的内容被分页节点分割,产生多余的标记或字符。可以在流式插入时暂时禁用分页更新,完成后再重新计算:
watch( () => props.scribbleNewContent, async newChunk => { if (!tiptapEditor.value || !newChunk) return // 找到分页插件实例 const paginationExt = tiptapEditor.value.extensionManager.extensions.find(ext => ext.name === 'paginationPlus') // 流式插入时禁用分页自动更新 if (paginationExt) paginationExt.options.disabled = true const { from, to } = initialSelectionRange.value if (toScrible.value === null || toScrible.value === undefined) toScrible.value = from try { if (isFirstChunk) { tiptapEditor.value.commands.deleteRange({ from, to }) isFirstChunk = false } await tiptapEditor.value.commands.insertContentAt(toScrible.value, newChunk, { applyInputRules: true, applyPasteRules: true, errorOnInvalidContent: false }) // 用选区位置更新下一次插入点 toScrible.value = tiptapEditor.value.state.selection.to } catch (e) { console.warn('Erro ao inserir chunk:', e) } markdownBuffer.value += newChunk if (props.scribbleCompleted) { // 完成后重新启用分页并强制更新 if (paginationExt) { paginationExt.options.disabled = false tiptapEditor.value.view.dispatch(tiptapEditor.value.state.tr.setMeta('paginationPlus', { forceUpdate: true })) } // 你的原有最终插入逻辑... const docSize = tiptapEditor.value.state.doc.content.size if (from < 0 || toScrible.value > docSize) { console.warn('Invalid range') return } tiptapEditor.value?.chain() .focus() .deleteRange({ from, to: toScrible.value }) .insertContentAt(from, markdownBuffer.value, { parseOptions: { preserveWhitespace: false }, applyPasteRules: true }) .run() // 重置状态 isFirstChunk = true markdownBuffer.value = '' } }, )
3. 最稳妥的替代方案:只缓冲Markdown,最后一次性插入
如果你的场景可以接受“最后一次性渲染完整内容”而不是实时流式显示,那直接放弃中间的实时插入,只往markdownBuffer里累加chunk,直到scribbleCompleted时再一次性替换目标范围,完全避开分页插件的中间干扰:
watch( () => props.scribbleNewContent, async newChunk => { if (!tiptapEditor.value || !newChunk) return const { from, to } = initialSelectionRange.value // 只累加缓冲区,不实时插入 markdownBuffer.value += newChunk if (props.scribbleCompleted) { try { tiptapEditor.value?.chain() .focus() .deleteRange({ from, to }) .insertContentAt(from, markdownBuffer.value, { parseOptions: { preserveWhitespace: false }, applyPasteRules: true }) .run() console.log('CONFIRMA') } catch (e) { console.warn('Erro ao inserir conteúdo final:', e) } // 重置所有状态 isFirstChunk = true markdownBuffer.value = '' toScrible.value = null } }, )
额外排查小技巧
打开ProseMirror的开发者工具(可以装prosemirror-dev-tools插件),查看文档的节点结构,看看多余字符是来自分页插件插入的临时节点,还是位置偏移导致的重复插入——这能帮你快速定位问题根源。
另外注意下你PaginationPlus配置里的headerLeft有个小笔误:diplay: flex应该是display: flex,虽然不影响多余字符,但会导致header布局异常😉




