1. 环境准备与依赖选型

在构建一个现代 Markdown 实时预览器前,明确技术栈边界是安全与可维护性的第一道防线。本教程默认采用纯前端、浏览器运行环境(兼容 Vite/React/Vue/甚至原生 HTML 页面),不依赖服务端渲染——这意味着所有解析、渲染、防护逻辑必须在客户端健壮执行。

基础要求

  • Node.js ≥ 18.0(确保 ESM 原生支持与现代 API 兼容性)
  • 构建工具无强绑定:markdown-it 是纯 JS 库,import 即用,Vite/Webpack/Rollup 均无缝支持

🔍 主流 Markdown 解析库横向对比

XSS 默认防护插件生态性能(10KB 文档)维护状态备注
marked❌(需手动禁用 html: true中等⚡ 快(但 v4+ 移除同步 API)活跃配置项少,扩展性弱于 markdown-it
remark✅(纯 AST,无 HTML 输出)⚙️ 极强(统一 AST 生态)🐢 中等(AST 转换链长)活跃学习成本高,需搭配 rehype-stringify 等,适合复杂处理流
markdown-it✅(默认 html: false,xhtmlOut 安全)🌟 丰富(>200 官方/社区插件)⚡⚡ 快(C 语言级优化 parser)活跃推荐首选:开箱即用的安全基线 + 插件即插即用

⚠️ 明确避坑

  • showdown(v2.x 已停止维护,v3.x 重构未稳定,且默认开启 HTML 解析)
  • markdown(npm 上同名废弃包,非 markdown-it
  • commonmark(规范严格但生态单薄,无语法高亮原生支持)

📌 推荐初始化命令

npm install markdown-it dompurify highlight.js
# 或使用 pnpm/yarn

Markdown 解析库生态对比示意图


2. 基础渲染器搭建:从字符串到 HTML

我们从最简实例出发,验证核心链路是否通畅。注意:这一步仅验证「解析能力」,不涉及任何 DOM 插入或安全处理

import MarkdownIt from 'markdown-it';

// 创建默认配置实例(已禁用 HTML 标签解析)
const md = new MarkdownIt({
  html: false,      // 🔒 关键!禁止原始 HTML 解析
  xhtmlOut: true,   // 输出自闭合标签(如 <br />),更规范
  breaks: false,    // 暂不启用 \n → <br>(避免干扰列表/代码块)
  langPrefix: 'language-', // 为 <code> 添加 class 前缀,便于高亮
});

const input = '# Hello\n\n- Item 1\n- Item 2';
const html = md.render(input);

console.log(html);
// 输出:
// <h1>Hello</h1>
// <ul>
// <li>Item 1</li>
// <li>Item 2</li>
// </ul>

💡 关键理解md.render() 返回的是未经转义的 HTML 字符串,它本质是「可信中间产物」——但一旦你把它交给 innerHTML,就等于把解析权交给了浏览器的 HTML 解析器,而该解析器完全不关心你的 Markdown 来源是否可信

⚠️ 绝对禁止element.innerHTML = md.render(userInput) —— 这是 XSS 的黄金入口。


3. 实现实时预览:监听输入并触发重渲染

<textarea> 与预览区联动,需兼顾响应性与性能:

  • ✅ 使用 input 事件(捕获所有输入:键盘、粘贴、语音输入)
  • ✅ 防抖(debounce)控制渲染频率(避免每敲一个字都解析)
  • ✅ DOM 更新策略:预览区用 innerHTML(因内容经后续净化),输入区保持 value 不变(防光标跳动)
const textarea = document.getElementById('md-input');
const preview = document.getElementById('html-preview');
const md = new MarkdownIt({ html: false });

let debounceTimer;
textarea.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    try {
      const html = md.render(textarea.value);
      // ⚠️ 此处仍不安全!待第4节加固后才可赋值
      preview.innerHTML = html; 
    } catch (err) {
      preview.textContent = '⚠️ 解析错误,请检查语法';
    }
  }, 300); // 推荐 200–500ms:短于 200ms 易卡顿,长于 500ms 感知延迟明显
});

🔍 常见问题排查

  • 中文标点后光标跳动? → 检查是否误写了 textarea.value = textarea.value(触发重排重绘)
  • 防抖失效? → 在 setTimeout 外加 console.log(performance.now()),连续输入应只触发 1 次日志
  • 移动端 iOS 光标错位? → 确保 textarearesize: none + min-height 固定,或监听 scrollIntoView 补偿

4. 安全加固:防止 XSS 攻击

假设用户输入:

[点击](javascript:alert('xss'))
<img src=x onerror=alert(1)>

若直接 innerHTML 渲染,上述 payload 将立即执行——这是典型的「DOM-based XSS」。

🛡️ 双保险策略(推荐组合)

方案 A(生产环境首选):markdown-it 基础防护 + DOMPurify 终极清洗

npm install dompurify
import DOMPurify from 'dompurify';

// 渲染后立即净化
preview.innerHTML = DOMPurify.sanitize(html);

// ✅ DOMPurify 默认白名单:仅保留 <h1>~<h6>, <p>, <ul>, <ol>, <li>, <pre><code>, <strong> 等语义化标签
// ✅ 自动移除 javascript:、onerror、style="background:url(javascript:..." 等危险属性

方案 B(超轻量场景):纯 markdown-it 配置禁用

const md = new MarkdownIt({
  html: false,        // 禁用原始 HTML 解析(默认即 true)
  typographer: false, // 禁用智能引号等富文本转换(减少潜在注入面)
  linkify: false,     // 禁用自动链接(避免 `http://x.com` 被包裹为 <a>)
});

⚠️ 注意:html: false<br> 不会生成,需显式设置 breaks: true 并接受其对列表缩进的微小影响(见第7节速查)。

XSS 攻击与 DOMPurify 防护原理对比图


5. 增强体验:语法高亮与样式美化

没有高亮的代码块如同无盐的汤。我们集成 highlight.js(体积小、语言全、主题丰富):

npm install highlight.js
# 加载主题 CSS(Vite 中可直接 import)
import 'highlight.js/styles/github-dark.css';
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import python from 'highlight.js/lib/languages/python';
import css from 'highlight.js/lib/languages/css';

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('python', python);
hljs.registerLanguage('css', css);

const md = new MarkdownIt({
  highlight: (str, lang) => {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang }).value;
      } catch (e) {
        console.warn(`Highlight failed for ${lang}:`, e);
      }
    }
    return md.utils.escapeHtml(str); // 降级为转义
  }
});

🎨 样式提示

  • 确保 <pre><code> 未被全局 CSS 重置(如 white-space: pre-line 会破坏缩进)
  • 推荐添加最小样式保障:
    pre code {
      display: block;
      padding: 1rem;
      overflow-x: auto;
    }
    

6. 进阶优化:性能监控与错误处理

面向真实用户,需主动观测与兜底:

textarea.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    console.time('markdown-render');
    try {
      const html = md.render(textarea.value);
      console.timeEnd('markdown-render'); // 如 "markdown-render: 8.23ms"

      // 超长文本降级(>50KB 时改用 textContent 提示)
      if (textarea.value.length > 50 * 1024) {
        preview.textContent = '📝 文档过长,已切换为纯文本预览(支持 Ctrl+F 搜索)';
        return;
      }

      preview.innerHTML = DOMPurify.sanitize(html);
    } catch (err) {
      console.error('Markdown parse error:', err);
      preview.textContent = '⚠️ 解析失败,请检查语法(可能含未闭合的 ``` 或 $)';
    }
  }, 300);
});

🚀 Web Worker 分离(可选高阶)
当文档 >100KB 时,主线程可能卡顿。可将 md.render() 移至 Worker:

// worker.js
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
self.onmessage = ({ data }) => {
  self.postMessage({ html: md.render(data) });
};

主页面通过 postMessage 通信,彻底解耦渲染与 UI 线程。


7. 调试清单与部署检查

上线前,请逐项核验(建议打印为 checklist 贴在团队看板):

  • markdown-it 实例已明确设置 html: false
  • 所有 innerHTML = ... 赋值前均调用 DOMPurify.sanitize()
  • 防抖生效:连续快速输入 5 次,控制台 console.time 日志 ≤ 1 条
  • 高亮覆盖:测试 js/ts/py/html/css 五种语言代码块均正确着色
  • 移动端实测:iOS Safari 下 textarea 输入、粘贴、光标定位无异常

🔍 高频问题速查表

Q:LaTeX 数学公式(如 $E=mc^2$)不渲染?
A:markdown-it 默认不支持。安装插件:npm install markdown-it-texmath,并引入 KaTeX:

import katex from 'katex';
import 'katex/dist/katex.min.css';
import texmath from 'markdown-it-texmath';
md.use(texmath, { engine: katex, delimiters: 'dollars' });

Q:列表项缩进丢失,变成段落?
A:检查是否误启 breaks: true —— 它会将所有换行转为 <br>,破坏 Markdown 列表解析规则。关闭即可:breaks: false(默认值)。

Markdown 预览器完整功能架构图

至此,你已构建出一个安全、高性能、可扩展、跨平台兼容的 Markdown 实时预览核心引擎。下一步可按需接入:TOC 自动生成、图片拖拽上传、导出 PDF、协同编辑等高级能力——而这一切,都建立在今天打下的坚实基础上。