Markdown渲染器完整指南 - 主流解析器对比与实战教程
Markdown 渲染器是干嘛的?说白了就是把 .md 文件里的 Markdown 文本变成浏览器能看懂的 HTML。你在 GitHub 上看到的 README、掘金上的技术博客、Notion 导出的文档——背后都有一个 markdown parser 在干活。
这篇文章会帮你搞清楚三件事:渲染器怎么工作的、市面上主流的几个库各有什么特点、以及在实际项目里怎么选怎么用。
Markdown 渲染器是怎么工作的
不管你用的是 markdown-it、marked 还是别的什么库,核心流程都差不多,分三步走:
第一步,词法分析(Tokenization)。渲染器读入原始的 Markdown 文本,按照 CommonMark 规范把它拆成一个个 token。比如 ## 标题 会被识别为一个 heading token,**粗体** 会被拆成 ** + 文本 + **。
第二步,构建中间结构。不同库的做法有差异:markdown-it 会构建一个 token 流然后直接输出 HTML;而 remark/unified 这类库会先构建完整的 AST(抽象语法树),让你可以在中间做各种变换。
第三步,生成 HTML。把中间结构映射成 HTML 标签输出。
Markdown 文本 → Token 流 → HTML 输出这个流程听起来简单,但魔鬼在细节里。比如下面这段 Markdown:
- 第一层
- 第二层
- 第三层不同渲染器对这种嵌套列表的解析结果可能不一样。有些严格要求缩进必须是空格(不能是 Tab),有些宽松一点。这也是为什么规范兼容性很重要——后面会详细聊。
主流 Markdown 渲染器对比
JavaScript 生态里有不少 markdown 转 html 的库,但真正广泛使用的主要是下面这几个。
markdown-it:功能最全的选择
markdown-it 是目前生态最成熟的选择。它严格遵循 CommonMark 规范,默认就是安全的(不会把用户输入里的 <script> 标签直接渲染出来),而且有非常丰富的插件生态。
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const result = md.render('# Hello Markdown');
// <h1>Hello Markdown</h1>markdown-it 支持很多配置项,常用的几个:
const md = new MarkdownIt({
html: false, // 禁止在 Markdown 中写原生 HTML
linkify: true, // 自动把 URL 转成链接
typographer: true, // 启用排版优化(比如把 (c) 转成 ©)
highlight: function (str, lang) {
// 自定义代码高亮,可以接入 highlight.js
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(str, { language: lang }).value;
}
return '';
}
});说实话,markdown-it 最大的卖点不是性能(虽然也不差),而是它的插件体系。你想要脚注、数学公式、定义列表、emoji、容器块……都有现成的插件:
const md = new MarkdownIt()
.use(require('markdown-it-footnote')) // 脚注支持
.use(require('markdown-it-mark')) // ==标记文本==
.use(require('markdown-it-emoji')); // :emoji: 支持
md.render('这是一个脚注[^1]\n\n[^1]: 脚注内容');21k+ GitHub stars,VS Code、Dillinger 这些知名项目都在用它,社区依赖者超过 88 万。如果你需要一个"不会出错"的选择,选它基本不会后悔。
marked:追求速度的首选
marked 的定位很明确:快。它是一个低级别的编译器,不搞 AST 那套复杂的东西,直接把 Markdown 编译成 HTML。
const { marked } = require('marked');
const html = marked.parse('# Hello **Markdown**');
// <h1>Hello <strong>Markdown</strong></h1>marked 的代码量少、包体积小(压缩后约 19KB),解析速度在基准测试里通常是几个主流库里最快的。它对 CommonMark 0.31 的通过率达到 98%,GFM 0.29 通过率 97%,兼容性也没问题。
但它有一个需要特别注意的点:marked 默认不过滤 HTML。也就是说用户如果输入 <script>alert('xss')</script>,marked 会原样输出。官方文档明确建议配合 DOMPurify 这类库一起用。
我曾经在一个博客系统里用 marked 渲染用户提交的评论,当时没注意到这个问题,结果有人提交了一条包含 <script> 标签的评论,直接在页面上弹了个 alert。后来加了 DOMPurify 做消毒才解决:
const { marked } = require('marked');
const DOMPurify = require('isomorphic-dompurify');
const dirty = marked.parse(userInput);
const clean = DOMPurify.sanitize(dirty);36k+ stars,依赖者超过 150 万,marked 在社区里使用非常广泛。
showdown:经典的双向转换器
showdown 是个比较有年头的老牌库了。它最大的特点是支持双向转换——不仅能把 Markdown 转成 HTML,还能把 HTML 转回 Markdown。
const showdown = require('showdown');
const converter = new showdown.Converter();
const html = converter.makeHtml('# Hello');
// <h1>Hello</h1>
const md = converter.makeMd('<h1>Hello</h1>');
// # Helloshowdown 的配置选项非常丰富(30 多个),支持 GitHub Flavored Markdown 的表格、任务列表、删除线等特性,还有 Angular、Vue 的封装库(ng-showdown、vue-showdown)。
不过 showdown 和 marked 一样,默认不过滤 HTML,存在 XSS 风险。而且项目更新频率不如 markdown-it 和 marked 活跃。
remark/unified:插件化生态方案
remark 是 unified 生态系统的一部分。它的思路跟前面几个库不太一样:先把 Markdown 解析成 AST,然后你可以通过插件对 AST 做任意变换,最后再输出 HTML。
Markdown → remark AST → 插件变换 → HTML这种架构的优势在于灵活性极高。你想自动给标题加 ID、检查 Markdown 语法规范、把相对链接转成绝对链接、甚至支持 MDX——都有对应的插件。
const { unified } = require('unified');
const remarkParse = require('remark-parse');
const remarkRehype = require('remark-rehype');
const rehypeStringify = require('rehype-stringify');
const result = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process('# Hello Markdown');Next.js、Astro、Remix 这些现代框架在处理 Markdown 时底层都用的是 remark。如果你的项目本身就在用这些框架,直接用 remark 是最自然的选择。
代价是学习曲线陡一些,包体积也可能更大(取决于你用了哪些插件)。
如何选择 Markdown 渲染器
说了这么多,到底该选哪个?我的建议是根据场景来:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 通用 Web 项目 | markdown-it | 功能全面、插件丰富、默认安全 |
| 追求极致性能 | marked | 解析速度最快、包体积小 |
| 需要双向转换 | showdown | 唯一支持 MD↔HTML 的方案 |
| Next.js/Astro 等构建框架 | remark/unified | 原生支持、MDX 兼容 |
| 超轻量场景 | marked 或更小的库 | 按需选择最小包 |
对了,如果你的项目里已经用了某个框架(比如 React 或 Vue),很多时候不需要直接操作底层渲染库,可以用框架封装好的组件——下面会讲到。
在 React 中渲染 Markdown
React 项目里渲染 Markdown,最直接的方案是用 react-markdown 组件。它底层基于 remark/unified,用起来比手动操作 markdown-it 再塞进 dangerouslySetInnerHTML 要安全得多:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
function MarkdownViewer({ content }) {
return (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{content}
</ReactMarkdown>
);
}如果你想用 markdown-it 也没问题,但要注意安全问题。我在早期项目里用过这种方式,后来发现 dangerouslySetInnerHTML 这个名字本身就是在提醒你——这里有风险:
import MarkdownIt from 'markdown-it';
import DOMPurify from 'isomorphic-dompurify';
const md = new MarkdownIt({ html: false });
function MarkdownViewer({ content }) {
const html = DOMPurify.sanitize(md.render(content));
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}如果还需要代码高亮,可以配合 rehype-highlight 插件(react-markdown 方案)或者手动在 markdown-it 里配置 highlight.js。
在 Vue 中渲染 Markdown
Vue 项目里,选择也不少。
方案一:markdown-it + v-html
<template>
<div class="markdown-body" v-html="renderedHtml"></div>
</template>
<script setup>
import { ref, watch } from 'vue';
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt({ html: false, linkify: true });
const props = defineProps({ source: String });
const renderedHtml = ref('');
watch(() => props.source, (val) => {
renderedHtml.value = md.render(val || '');
}, { immediate: true });
</script>方案二:vue-showdown
如果你喜欢 showdown 的双向转换能力,可以直接用 vue-showdown 组件:
<template>
<VueShowdown :markdown="source" flavor="github" />
</template>两种方案各有优劣。markdown-it 更灵活,插件更多;vue-showdown 开箱即用更省事。
安全性:渲染 Markdown 时必须注意的事
这个话题值得单独拿出来说,因为很多教程都一笔带过了。
当你把用户提交的 Markdown 内容渲染成 HTML 时,必须考虑 XSS(跨站脚本攻击)。三种情况:
情况一:内容来自可信来源(比如你自己写的文档)。这种情况下安全性问题不大,但最好还是把 html 选项设为 false,避免意外的 HTML 注入。
情况二:内容来自用户提交。这是最危险的情况。用户可以轻易在 Markdown 中嵌入 <script> 标签、javascript: 协议链接、或者 onerror 事件。markdown-it 默认会过滤这些,但 marked 和 showdown 不会。
情况三:需要在 Markdown 中使用 HTML。有时候你确实需要写 HTML(比如嵌入视频),这时就需要一个 HTML 消毒库。
无论哪种情况,只要你把 Markdown 渲染成 HTML 塞进页面,加一层 DOMPurify 都是好的习惯:
import DOMPurify from 'isomorphic-dompurify';
// 不管用的什么解析器,输出都过一遍消毒
const safeHtml = DOMPurify.sanitize(parser.render(userInput));CommonMark 规范为什么重要
你可能看到过 CommonMark 这个词。它是一个努力让 Markdown 语法标准化的项目——因为 Markdown 的发明者 John Gruber 最初并没有给出一个精确的规范,导致不同的 markdown parser 对同一段文本的解析结果可能不一致。
举个例子,下面这段 Markdown:
**bold *italic***
**bold *italic** text*不同的渲染器可能给出不同的 HTML 输出。CommonMark 规范就是为了消除这种歧义。
markdown-it 是 CommonMark 规范的忠实实现者,这也是为什么很多人在正式项目里选它——行为可预测,不会出现"换个渲染器效果就不一样"的问题。marked 也做到了 98% 的 CommonMark 兼容性,日常使用差别不大,但在一些边缘情况下可能会和 markdown-it 的输出不同。
代码高亮集成
代码高亮是 Markdown 渲染器最常见的搭配功能。不管你选哪个解析库,接入代码高亮的方式都差不多。
markdown-it + highlight.js
const hljs = require('highlight.js');
const md = require('markdown-it')({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
'</code></pre>';
} catch (__) {}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
}
});marked + highlight.js
marked 的配置方式类似,通过 marked.setOptions 设置:
const { marked } = require('marked');
const hljs = require('highlight.js');
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return code;
}
});记得在页面中引入 highlight.js 的 CSS 主题文件,不然只有代码标记没有颜色。
自定义渲染行为
有时候你需要改变渲染器的默认输出,比如给所有链接加 target="_blank"、给图片加 lazy loading、或者自定义标题的 ID 生成规则。
markdown-it 自定义渲染规则
const md = require('markdown-it')();
// 给所有链接加 target="_blank"
const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
tokens[idx].attrSet('target', '_blank');
tokens[idx].attrSet('rel', 'noopener noreferrer');
return defaultRender(tokens, idx, options, env, self);
};marked 自定义 renderer
const { marked } = require('marked');
const renderer = {
heading(text, depth) {
const slug = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${depth} id="${slug}">${text}</h${depth}>`;
},
image(href, title, text) {
return `<img src="${href}" alt="${text}" loading="lazy" />`;
}
};
marked.use({ renderer });两种方式都不复杂,但 markdown-it 的 token 操作更灵活一些,适合需要精细控制的场景。
参考来源
- CommonMark Spec — Markdown 语法标准化规范
- markdown-it GitHub — markdown-it 官方仓库及文档
- Marked.js 官方文档 — marked 渲染器规范兼容性报告
- MDN: innerHTML 安全性 — 关于 innerHTML 和 XSS 安全说明
- DOMPurify — HTML 消毒库,用于防范 XSS 攻击