Markdown 实时预览怎么实现双向同步?

永莲🍀 阅读 63

我用 contenteditable 做了个 Markdown 编辑器,左边写源码右边实时预览,但改预览区内容没法同步回源码区,这咋整?

试过监听 input 事件,但预览区是渲染后的 HTML,转回 Markdown 会丢格式。比如我加粗文字 **hello** 渲染成 <strong>hello</strong>,再转回去就变不回来了。

有没有靠谱的库或者思路能保持两边同步?现在用的是 marked.js 渲染:

const markdown = document.getElementById('markdown');
const preview = document.getElementById('preview');

markdown.addEventListener('input', () => {
  preview.innerHTML = marked.parse(markdown.value);
});
我来解答 赞 9 收藏
二维码
手机扫码查看
1 条解答
宇文若彤
这个问题的核心难点在于 HTML 转 Markdown 本身就是一件困难的事,因为 HTML 丢失了原始的 markdown 语法标记(比如你提到的 **)。

不过有现成的库可以一定程度上解决这个问题,我推荐用 turndown 这个库来做反向转换。

首先你需要引入 turndown:

然后改造你的代码,大概思路是这样的:

// 引入 marked.js(你已经有了)
// 引入 turndown:<script src="https://unpkg.com/turndown/dist/turndown.js"></script>

const markdown = document.getElementById('markdown');
const preview = document.getElementById('preview');

// 创建 turndown 服务实例
const turndownService = new TurndownService({
headingStyle: 'atx', // 使用 # 标题
codeBlockStyle: 'fenced', // 使用
代码块
});

// 标记是否来自源码区的更新(避免死循环)
let isUpdatingFromSource = false;

// 源码区 → 预览区
markdown.addEventListener('input', () => {
isUpdatingFromSource = true;
preview.innerHTML = marked.parse(markdown.value);
isUpdatingFromSource = false;
});

// 预览区 → 源码区(关键部分)
preview.addEventListener('input', () => {
if (isUpdatingFromSource) return;

// 获取预览区的 HTML,转成 Markdown
const html = preview.innerHTML;
const convertedMarkdown = turndownService.turndown(html);

// 更新源码区
markdown.value = convertedMarkdown;
});
``

**原理说明:**

turndown 这个库做的事情就是尽可能把 HTML 转回 Markdown。它会分析 HTML 标签结构,比如看到
就转成 **,看到 转成 *,看到

转成 #

**注意事项:**

这种方案有个现实问题无法完全避免:某些复杂格式可能会丢失或者略有变化。比如你原来写的
hello 渲染成 hello,turndown 会转回 hello`,这个其实没问题。但有些情况比如嵌套的复杂列表、表格之类的,转换后可能会有细微差别。

如果你的场景对格式要求特别严格,可能需要考虑换一个思路:不用 contenteditable + 渲染的方式,而是直接用支持双向绑定的编辑器库,比如 ByteMD 或者 Milkdown,这些库内部维护的是 Markdown AST,编辑和渲染都基于同一份数据,天然支持双向同步。

但如果你只是想快速实现一个基本可用的版本,turndown 这个方案够用了,自己加一些边界情况的处理就行。

点赞
2026-03-16 17:04