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>
);
}如果你想在 React 裡用 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 攻擊