目录
- 01 先看数字画像:一个"老"得离谱的库
- 02 它凭什么活了 16 年
- 03 内部流水线:它到底怎么把 Markdown 变成 HTML
- 04 五分钟上手:从安装到第一次渲染
- 05 选型对照:marked、markdown-it、remark,到底用谁
- 06 安全红线:marked 不管输出,坑在这里
- 07 判断框架:你的项目该不该用 marked
这篇文章写给前端和后端都在写 Markdown 的开发者——尤其是想搞清楚「Markdown 到底是怎么变成 HTML」的人。读完你会知道四件事:marked 凭什么活了 16 年、它内部怎么工作、怎么五分钟上手、以及一个很多人踩过的安全坑。
你写完一篇 Markdown,点了保存,网页上立刻出现排版好的文章。中间发生了什么?
浏览器只认识 HTML,不认识# 标题和**加粗**。总得有个东西把 Markdown "翻译"成 HTML——这个翻译官,很可能是marked。
它不是今年冒出来的新玩具:项目从2011 年就在写了,到现在 16 年,GitHub 上3.7 万+ Star;npm 官方数据显示,过去一周它被下载了 8166 万次——这个数字,比很多爆款前端框架还夸张。
今天我们不看官方那句"为速度而生"的广告词,用数据和代码,把这个 16 岁的老库拆开看一遍:它靠什么活到今天、内部怎么工作、怎么上手、和同类库比该怎么选,以及一个很多人踩过的安全坑。
01 先看数字画像:一个"老"得离谱的库
先看仓库实拍(上面这张图就是 github.com/markedjs/marked 首页):37.2k Star、3.8k Fork、225 个 Tag、3881 次提交——一个 16 岁的项目,维护节奏到现在还是每周都有提交。
再把 npm 官方数据摆出来(2026 年 10 月抓取):
| 指标 | 数值 |
|---|---|
| npm 周下载量 | 约 8166 万次(81,654,018) |
| GitHub Star | 3.7 万+(37.2k) |
| 依赖项 | 0(零依赖) |
| 被多少包依赖 | 14,957 个 |
| 历史版本 | 225 个 |
| 最新版本 | 18.0.14(npm)/ v18.1.0(GitHub tag) |
| 语言 | TypeScript |
| 许可证 | MIT |
| 创建时间 | 2011 年(Christopher Jeffrey) |
两个数字值得停下来看:
第一,"0 依赖"在 npm 生态里是稀有物种。装一个包拖进来几百个依赖是常态,而 marked 一个依赖都没有——体积小、安装快、不污染你的依赖树。这对体量敏感的前端项目,本身就是卖点。
第二,8166 万次/周是什么概念?对比一下:同赛道的 markdown-it,周下载约 2100 万次。marked 差不多是它的4 倍。博客、文档站、在线编辑器、Markdown 预览插件……大量你叫不出名字的产品,背后都在用同一个翻译官。
02 它凭什么活了 16 年
开源项目死掉的原因通常是同一个:没人维护了。marked 能活 16 年,我把它归结为三个"没变":
没变的是定位:只做一件事,且做到底。它的仓库简介一句话:A markdown parser and compiler. Built for speed.——只把 Markdown 变成 HTML,不为别的分心。低层编译器(low-level compiler),不做缓存、不长时间阻塞主线程,渲染完就走。
没变的是对规范的较真。官方文档给出了合规测试数据:Markdown 1.0 通过率 100%,CommonMark 0.31 通过率 100%(仅 Links 一项 99%),GFM(GitHub 风格)0.29 通过率 99%,其中表格、任务列表、删除线这三个 GitHub 常用扩展全部 100%。意味着你平时写的语法,它基本都接得住,不用为"这个语法它支不支持"操心。
没变的是"吃自己的狗粮"。下面这张图是 marked 的官方文档首页(marked.js.org)——这个文档站本身就是用 marked 渲染的。文档团队天天用自己写的解析器,这比任何宣传都有说服力。类似的还有 zero-md、TeXMe、StrapDown.js、Homebrewery(D&D 跑团内容制作工具)等一批工具都建立在它之上——生态粘性就是这么攒下来的。
顺带一个冷知识:官方在首页很诚实地标注了一句——“仍在完善性能对比基准的度量(Still working on metrics)”。也就是说,"最快"至今没有官方基准背书。网上那些"速度极快"的说法,更多来自社区口碑和它的轻量设计,这一点我们后面选型时再展开。
03 内部流水线:它到底怎么把 Markdown 变成 HTML
你不用把源码读一遍,记住这条流水线就够了:
输入 → Lexer(词法分析)→ Token 树 → Parser(语法分析)→ HTML
- Lexer:把 Markdown 字符串按规则切成一块块token,组成一棵嵌套的 token 树——
# 标题变成{type: "heading", depth: 1},列表、代码块、引用同理; - walkTokens(可选):按需对每个 token 做后处理;
- Parser:遍历 token 树,把每个 token 喂给对应的renderer,拼出 HTML 字符串。
关键在设计:中间那层 token 树是"可插拔"的。你想改标题的输出样式,不用重写整个解析器,override 一个 renderer 就行:
import{marked}from'marked';constrenderer={heading({tokens,depth}){if(depth===1){return`<h1>${this.parser.parseInline(tokens)}</h1>`;}returnfalse;// 其他级别走默认逻辑}};marked.use({renderer});console.log(marked.parse('# 我的博客标题'));// <h1>我的博客标题</h1>想加一种全新的语法,就注册一个自定义扩展(tokenizer + renderer 成对出现);想在解析前后插手,还有hooks(preprocess / postprocess);想要异步处理(比如渲染前请求某个链接),打开async模式即可。官方还提供了扩展模板仓库,直接起新项目。
一句话总结它的设计哲学:解析与渲染分离,扩展点开放,核心保持轻量。这也是它能被塞进各种千奇百怪场景还不崩的原因。
04 五分钟上手:从安装到第一次渲染
前面讲了半天"是什么",现在讲"怎么用"——其实简单到超出你的预期。
第一步,安装(Node 项目里):
npminstallmarked第二步,三行代码跑起来:
import{marked}from'marked';consthtml=marked.parse('# 你好,marked\n\n这是**加粗**的测试。');console.log(html);// <h1>你好,marked</h1>// <p>这是<strong>加粗</strong>的测试。</p>浏览器里直接用 CDN——想做个本地 Markdown 预览页,这是最快的方式:
<scriptsrc="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script><script>document.getElementById('content').innerHTML=marked.parse('# 在浏览器里也能渲染');</script>命令行一键转换——装全局命令后,文件进、HTML 出:
npminstall-gmarked marked-ireadme.md-oreadme.html常用配置——用marked.use()全局设置,不需要每次都传参数:
import{marked}from'marked';marked.use({gfm:true,// GitHub 风格(表格/任务列表/删除线),默认就是 truebreaks:true,// 单个换行也变成 <br>,更像聊天框async:false// 开启后 walkTokens 可异步,parse 返回 Promise});consthtml=marked.parse('第一行\n第二行');只想解析一段内联内容(不包<p>标签),用parseInline:
marked.parse('**加粗** _斜体_');// <p><strong>加粗</strong> <em>斜体</em></p>marked.parseInline('**加粗** _斜体_');// <strong>加粗</strong> <em>斜体</em>一句话总结上手成本:装一个包,调一个函数,完事。剩下所有进阶玩法(自定义渲染、扩展语法、异步处理)都在marked.use()这一个入口里做文章——这也是它 16 年没被淘汰的设计红利。
05 选型对照:marked、markdown-it、remark,到底用谁
写 Markdown 渲染,JavaScript 生态里绕不开四兄弟:marked、markdown-it、remark(unified 生态)、showdown。很多人纠结怎么选,一张表说清楚:
| 维度 | marked | markdown-it | remark / unified | showdown |
|---|---|---|---|---|
| 定位 | 快、轻、零依赖 | 严格 CommonMark + 插件生态 | AST 内容处理管线 | 老牌全能型 |
| 周下载量 | 约 8166 万 | 约 2100 万 | 百万级 | 百万级 |
| 核心模型 | Token 树 | Token 流 + 插件 | AST 树(可编程变换) | 正则为主 |
| GFM 表格 | 内置 | 需插件 | 需插件 | 支持 |
| 插件生态 | 中等(官方扩展模板 + 30+ 已知扩展) | 大(footnotes/math/container 等) | 极大(lint、转换、格式化) | 小 |
| 适用场景 | 博客渲染、实时预览、CLI、批量转换、体积敏感 | 需要严格规范 + 丰富插件的站点 | 需要对内容做加工(目录生成、格式检查、格式转换) | 老项目兼容 |
选型就三句话:
- 渲染、预览、转换——默认 marked。快、零依赖、够用;
- 要严格规范 + 大量现成插件(脚注、数学公式、自定义容器)——选 markdown-it;
- 要对内容"动手术"(自动生成目录、检查格式、把 Markdown 转成别的格式)——上 remark/unified,它的 AST 让你能编程式地处理文档;
- showdown只在老项目里见到,新项目没必要从它开始。
06 安全红线:marked 不管输出,坑在这里
这是参考文章绝不会告诉你的部分,但官网首页写得明明白白:
⚠️ Marked does not sanitize the output HTML.
如果你处理的是不可信输入,必须对输出的 HTML 做过滤(sanitize),否则有 XSS 风险。
更值得玩味的是:marked以前是有内置 sanitize 选项的,在 v8.0.0 里主动删掉了(官方 Options 文档明确写着 “Removed in v8.0.0, use a sanitize library like DOMPurify”)。为什么删?因为官方认为"消毒"和"翻译"是两件事,解析器做好解析,安全交给专业的消毒库——职责分离。
什么意思?marked 的哲学是"我只负责翻译,不负责消毒"。你给它)或一段带<script>的文本,它原样输出。如果你把用户输入(评论区、自定义文档、在线编辑器内容)直接喂给 marked 再塞进页面,就存在被注入脚本的风险。
正确姿势是组合使用:marked 负责翻译,DOMPurify 负责消毒:
importDOMPurifyfrom'dompurify';consthtml=DOMPurify.sanitize(marked.parse(userInput));更优雅的玩法是用 marked 的 hooks,把消毒直接挂进渲染管线,一劳永逸:
import{marked}from'marked';importDOMPurifyfrom'isomorphic-dompurify';marked.use({hooks:{postprocess(html){returnDOMPurify.sanitize(html);}}});再补一个进阶提醒:如果处理的内容可能被恶意构造(防止 ReDoS 正则攻击),官方还建议把解析放到worker 线程/Web Worker里跑,超时直接终止进程,别让一个恶意字符串卡死你的主线程。
判断标准:只有你自己写、或内部可信的内容,才可以直接渲染;任何来自用户的内容,必须过一道 sanitize。这个坑官方首页就写了,但很多教程不提,值得单独拎出来。
07 判断框架:你的项目该不该用 marked
最后给个能直接用的清单:
该用 marked 的场景:
- 博客 / 文档站渲染 Markdown(八成场景就是它);
- 编辑器里的实时预览(快,不卡主线程);
- 命令行批量转换(
marked -i readme.md -o readme.html); - 项目对体积敏感、不想被依赖树污染。
别用 marked 的场景:
- 需要对内容做结构化加工(自动目录、代码统计、格式 lint)——上 remark;
- 需要大量开箱即用的语法插件——上 markdown-it;
- 处理用户输入却不想配 sanitize——这不是 marked 的问题,是没读官网的问题。
回到开头那个问题:你写的 Markdown 是怎么变成网页的?大概率是一个 2011 年出生、每周被下载 8166 万次、却只有几 KB、零依赖的老库完成的。
它没有炫酷的发布会,也没有天天刷屏的更新日志,但 16 年如一日地干好"翻译"这一件事。这种项目在开源世界里,反而是最稀缺的那类。
作者:蜗牛工具箱,Java 开发/培训,目前全职独立开发者。专注 AI 工具实战与技术事件拆解,欢迎关注同名公众号,也欢迎评论区聊聊。