JavaScript Markdown 编辑器实现双屏同步滚动

作者:桌面漫游时间:2026-10-26 04:59:45浏览:539

本文详细解析了 JavaScript Markdown 编辑器中双屏同步滚动的三种实现方案。通过对比百分比滚动、元素高度渲染差异及索引定位法,最终采用基于 data-index 的精确同步策略。文章还深入探讨了表格等嵌套元素导致的滚动偏差问题,并给出了将标记应用于具体行元素的解决方案,帮助开发者实现流畅的编辑体验。

前言

由于长期依赖 Markdown 编辑器撰写技术文档,我对编辑体验有着较高的敏感度。观察各大社区的 Markdown 编辑器,发现同步滚动已成为基本功能,但实现质量参差不齐。出于技术好奇,我决定自行探索并实现这一功能。

经过思考,我梳理出三种可行的技术路径:

  • 百分比滚动
  • 双屏同时渲染占用面积大的元素
  • 为每一行元素分配索引,依据索引精确同步滚动高度

百分比滚动

假设当前正在操作 A 屏,其滚动百分比的计算公式为:a 屏的滚动高度 / a 屏的内容总高度,代码表示为 a.scrollTop / a.scrollHeight。当 A 屏发生滚动时,需手动同步 B 屏的滚动高度,即根据 A 屏的滚动百分比推算出 B 屏对应的滚动距离:

a.onscroll = () => {
	b.scrollTo({ top: a.scrollTop / a.scrollHeight * b.scrollHeight })
}

原理看似简单,但实际效果并不理想。

前言

从演示动图可见,在第二个大标题处停留时,左右双屏内容同步良好。然而,当滚动至第三个大标题时,左右双屏的内容高度差已接近 300 像素。因此,该方案仅能勉强使用,聊胜于无。

双屏同时渲染占用面积大的元素

双屏内容高度不一致的主要原因在于,Markdown 元素在渲染后的高度往往与渲染前存在显著差异。例如,图片在 Markdown 中仅占一行代码,但渲染后高度可能从几十像素到几百像素不等。若在双屏同时渲染图片,或许能缓解此问题。

前言

然而,除了图片外,仍有不少元素在渲染前后存在高度差异,尽管不如图片显著。例如 h1、h2 等标题元素,随着文章内容增加,这些微小差异累积起来,会导致双屏内容高度差距越来越大。因此,该方案并不可靠。

赋上一个索引

为每一行元素分配索引,依据索引精确同步每一行的滚动高度。

前两种方案均存在局限,而第三种方案则更为优越,几乎能实现每一行内容的精确同步。具体实现方法如下:

第一步,监听 Markdown 编辑框的内容变化,为每个元素分配一个索引,空行和空文本除外。

前言

当将编辑框的 HTML 传递给右侧框进行渲染时,需将 data-index 赋值给渲染后的元素。这样即可通过 data-index 精确定位渲染前后的同一元素。

前言

第二步,根据 A 屏元素的滚动高度计算 B 屏上同一索引元素的滚动高度。

在 A 屏滚动时,需从上到下遍历 A 屏的所有元素,并找到第一个位于屏幕内的元素。找到第一个在屏幕内的元素 这句话的意思是,因为在滚动过程中,部分元素可能会因滚动而移出屏幕(原本在屏幕内,滚动后变为屏幕外),这些元素无需计算。

判断一个元素是否在屏幕内的方法如下:

// dom 是否在屏幕内
function isInScreen(dom) {
    const { top, bottom } = dom.getBoundingClientRect()
    return bottom >= 0 && top < window.innerHeight
}

除了判断元素是否在屏幕内,还需判断该元素在屏幕内的部分占整个元素高度的百分比。例如,一个图片的 Markdown 字符串,由于滚动原因,导致一半在屏幕内,一半在屏幕外。为了实现精确同步,渲染后的图片也必须保持一半在屏幕内、一半在屏幕外的状态。

前言

计算元素在屏幕内百分比的代码如下:

// dom 在当前屏幕展示内容的百分比
function percentOfdomInScreen(dom) {
	// 已经通过另一个函数 isInScreen() 确定了这个 dom 在屏幕内,所以只需要计算它在屏幕内的百分比,而不需要考虑它是否在屏幕外
    const { height, bottom } = dom.getBoundingClientRect()
    if (bottom <= 0) return 0 // 不在屏幕内
    if (bottom >= height) return 1 // 完全在屏幕内
    return bottom / height // 部分在屏幕内
}

现在,我们可以从上到下遍历 A 屏的所有元素,找到第一个在屏幕内的元素:

// scrollContainer 即上面说的 a 屏,ShowContainer 是 b 屏
const nodes = Array.from(scrollContainer.children)
for (const node of nodes) {
    // 从上往下遍历,找到第一个在屏幕内的元素
    if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
        const index = node.dataset.index
        // 根据滚动元素的索引,找到它在渲染框中对应的元素
        const dom = ShowContainer.querySelector(`[data-index="${index}"]`)

		// 获取滚动元素在 a 屏中展示的内容百分比
		const percent = percentOfdomInScreen(node)
		// 计算这个对等元素在 b 屏中距离容器顶部的高度
        const heightToTop = getHeightToTop(dom)
        // 根据 percent 算出对等元素在 b 屏中需要隐藏的高度
        const domNeedHideHeight = dom.offsetHeight * (1 - percent)
		// scrollTo({ top: heightToTop }) 会把对等元素滚动到在 b 屏中恰好完全展示整个元素的位置
		// 然后再滚动它需要隐藏的高度 domNeedHideHeight,组合起来就是 scrollTo({ top: heightToTop + domNeedHideHeight })
        ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
        break
    }
}

前言

从动图来看,目前已实现行内容的精确同步。

踩坑

部分元素在渲染后会变为嵌套元素,例如表格 table,其渲染后的内容层级结构为:

<table>
	<tbody>
		<tr>
			<td></td>
		</tr>
	</tbody>
</table>

按照当前的渲染逻辑,假如我编写了一个表格:

|1|b|
...

那么 |1|b| 上的 data-index 会对应到 table 上。

前言

前言

这将导致一个 bug:当 |1|b| 滚动到 50% 时,整个 table 也会滚动到 50%。

这一现象如下图所示:

前言

这与我们期望的效果不符。A 屏连一行的内容都未滚完,B 屏整个内容却已滚动到一半。

因此,对于此类嵌套元素,在打 data-index 标记时,需将其打在真正的内容上。以表格 table 为例,需将 data-index 的标记打在 tr 上。

前言

这样一来,同步滚动即可恢复正常。同理,其他嵌套元素(如 ul、ol)也需遵循此规则。

前言

到此这篇关于 JavaScript Markdown 编辑器实现双屏同步滚动的文章就介绍到这了,更多相关 JS Markdown 双屏同步滚动内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章