Nuxt 评论区完美支持 Markdown：从解析、高亮到安全渲染全攻略

h1Nuxt 评论区完美支持 Markdown：从解析、高亮到安全渲染全攻略blockquotep内含与 Nuxt Content 配色保持一致的技巧，让你的评论区和文档浑然一体hrh2📦 适用版本p本文基于以下版本编写，请确保你的项目版本与之匹配：tabletheadtrth依赖th版本th备注tbodytrtdNuxttdstrongv4td核心框架trtdNuxt Contenttdstrongv3td文档内容管理trtdNuxt UItdstrongv4td提供 codeuseColorModetrtdmarkedtdstrongv15+tdMarkdown 解析器trtdshikitdstrongv3+td代码高亮引擎trtdisomorphic-dompurifytdstrongv2+tdXSS 防护blockquotep💡 如果你使用其他版本，核心思路仍可参考，但具体 API 可能需要调整。hrdetailssummary评论区原理p在开始之前，先聊聊评论区的本质。p很多人（包括我一开始）觉得评论区很复杂——要处理嵌套、要实时更新、要防攻击……但实际上，strong一个最小可用的评论区，核心就是最简单的增删改查：ullistrong增：用户提交评论，存到数据库（codeINSERT）listrong删：用户删除自己的评论（codeDELETE）listrong改：编辑评论（codeUPDATE，可选）listrong查：加载文档下的所有评论（codeSELECT）p没有算法、没有实时推送、没有复杂的机制——strong就是最基础的后端操作 + 前端展示。blockquotep评论区没那么可怕，它只是一个长得像对话框的 CRUD 而已。p本文就是在“增删改查”的基础上，给你的评论加上 strongMarkdown 渲染 能力。如果你连基础的评论功能都还没做，可以先花 30 分钟搭一个简单的版本，再回来看本文。hrdetailssummary前置要求p本文默认读者已经具备以下能力：ulli✅ strong能独立完成评论的基础 CRUD（数据表设计、API 编写、前端展示）li✅ strong熟悉 Vue / Nuxt 组件开发（知道 codeprops、coderef、codewatch 怎么用）li✅ strong了解 Markdown 基本语法（知道 code**粗体**、code`代码` 是什么意思）li✅ strong能自行查阅文档（marked、Shiki、DOMPurify 的官网用法）p如果你还不具备这些，建议先补充基础：ulliaVue 3 中文官方文档liaNuxt 4 中文官方文档liaMarkdown 中文官方文档pstrong本文不会解释 SQL 怎么写、不会教 Vue 基础、不会重复官网 API——只讲“如何把评论区升级为 Markdown 渲染”。hrdetailssummary为什么写这篇文档？p我在实现评论区 Markdown 功能时，搜遍全网发现：ulli官网文档：只给 API，不给实战li个人博客：要么复制粘贴，要么浅尝辄止li中文社区：全是其他平台的教程（WordPress、Typecho）pstrong没有一篇是专门针对 Nuxt 4 的、完整的、经过实战检验的评论区 Markdown 集成教程。p所以我把自己折腾了七八个小时的过程写下来——包括踩过的坑、填过的土、以及那些“网上搜不到”的解决方案。希望能让后来的人少走些弯路。hrdetailssummary本文能给你什么ulli✅ strong完整的 Markdown 渲染方案（marked + Shiki + DOMPurify）li✅ strong代码块高亮与文档配色统一（深浅色自动切换）li✅ strongXSS 防护的正确姿势（不只是过滤标签）li✅ strong两种性能方案对比（预加载 vs 懒加载）li✅ strong7 个实战踩坑记录（code$ 陷阱、codewatch 监听、主题不匹配等）hrh2一、痛点与目标p许多 Nuxt 博主在搭建评论区时，会遇到以下问题：ulli评论只能输入纯文本，无法贴代码、加粗、列表等。li即使勉强支持 Markdown，代码块样式与文档正文（通常由 Nuxt Content 渲染）不一致，显得格格不入。li担心 XSS 攻击，不敢直接渲染用户输入的 HTML。pstrong本文目标：手把手教你为 Nuxt 博客评论区添加strong安全、美观、功能完整的 Markdown 渲染支持，并且代码块配色与文档（Nuxt Content v3）strong自动保持统一，深浅色模式无缝切换。hrh2二、技术选型与原理tabletheadtrth需求th选型th理由tbodytrtdMarkdown → HTMLtdcodemarkedtd轻量、快速、可扩展，支持 GFM，社区活跃trtd代码语法高亮tdcodeshikitdNuxt Content 同款，主题丰富，输出稳定 HTMLtrtdXSS 防护tdcodeisomorphic-dompurifytdSSR 兼容，过滤恶意标签和属性，保障安全trtd深浅色模式tdcodeuseColorMode (Nuxt UI)td自动监听系统/用户主题切换，动态更新渲染trtd代码块样式统一tdCSS 变量 + 覆盖td复用 Nuxt UI 主题变量，使评论块与文档块视觉一致h3为什么不用 Prism 或 highlight.js？pShiki 与 Nuxt Content 内部使用的高亮器一致，可以直接复用其主题和语言包，确保代码块颜色与文档strong完全一致，无需额外维护两套配色。blockquotepstrong主题与语言参考：Shiki 支持的主题和语言列表请查阅官方文档：a主题列表 | a语言列表。你可以根据自己博客的实际配色需求，从中选择与 Nuxt Content 匹配的主题（例如 codematerial-theme-* 系列）。hrh2三、逐步实现（附详细注释）h33.1 安装依赖precodespanspanpnpmspan addspan -Dspan shikispan markedspan isomorphic-dompurify spanspan# Nuxt UI v4 已内置 useColorMode，无需额外安装 h33.2 创建 Shiki 插件（全局单例）pShiki 初始化较慢，且应只创建一次。我们通过 Nuxt 插件在客户端创建全局高亮器实例，供所有组件共享。precodespanspan// plugins/shiki.client.ts spanspanimportspan {span createHighlighterspan }span fromspan "spanshikispan"span; spanspan spanspanexportspan defaultspan defineNuxtPluginspan(spanasyncspan ()span =>span { spanspan // 预加载文档用到的主题和语言（可根据实际需求调整） spanspan // 主题列表：https://shiki.zhcndoc.com/themes spanspan // 语言列表：https://shiki.zhcndoc.com/languages spanspan constspan highlighterspan =span awaitspan createHighlighterspan(span{ spanspan themesspan:span [span"spanmaterial-theme-lighterspan"span,span "spanmaterial-theme-palenightspan"span]span, spanspan langsspan:span [ spanspan "spanjavascriptspan"span, spanspan "spantypescriptspan"span, spanspan "spanhtmlspan"span, spanspan "spancssspan"span, spanspan "spanvuespan"span, spanspan "spanpythonspan"span, spanspan "spanbashspan"span, spanspan "spanjsonspan"span, spanspan "spanmarkdownspan"span, spanspan "spanxmlspan"span, spanspan "spanyamlspan"span, spanspan "spanshellspan"span, spanspan "spandiffspan"span, spanspan ]span, spanspan }span)span; spanspan spanspan returnspan { spanspan providespan:span { spanspan shikispan:span highlighterspan,span // 通过 $shiki 注入全局 spanspan }, spanspan }; spanspan}span)span; h33.3 封装 Markdown 渲染组件p创建 codecomponents/MarkdownRenderer.vue，核心逻辑如下：ulli使用 codemarked.Renderer 自定义代码块处理，交给 Shiki 高亮。li监听 codecolorMode 动态切换主题。li最后通过 DOMPurify 过滤输出。precodespanspan spanspan spanspan spanspan spanspan spanspan spanspanimportspan {span markedspan }span fromspan "spanmarkedspan"span; spanspanimportspan DOMPurify spanfromspan "spanisomorphic-dompurifyspan"span; spanspan spanspanconstspan props span=span definePropsspan(span{span contentspan:span {span typespan:span Stringspan,span requiredspan:span truespan }span }span)span; spanspan spanspan// 从 Nuxt 插件中获取全局 Shiki 高亮器实例（已在客户端插件中预加载主题和语言） spanspanconstspan {span $shiki span}span =span useNuxtAppspan()span; spanspanconstspan colorMode span=span useColorModespan()span; spanspan spanspanconstspan renderedContent span=span refspan(span""span)span; spanspan spanspan// 根据当前颜色模式动态选择 Shiki 主题，确保与文档代码块配色一致 spanspanconstspan currentTheme span=span computedspan(span()span =>span { spanspan returnspan colorModespan.spanvaluespan ===span "spandarkspan" spanspan ?span "spanmaterial-theme-palenightspan"span // 深色主题 spanspan :span "spanmaterial-theme-lighterspan"span;span // 浅色主题 spanspan}span)span; spanspan spanspan// 核心渲染函数：将用户输入的 Markdown 内容转换为安全的、高亮的 HTML spanspanconstspan renderContent span=span asyncspan ()span =>span { spanspan // 如果 Shiki 未就绪或内容为空，则直接显示原始内容（降级处理） spanspan ifspan (span!span$shikispan ||span !spanpropsspan.spancontentspan) span{ spanspan renderedContentspan.spanvaluespan =span propsspan.spancontentspan; spanspan returnspan; spanspan } spanspan spanspan tryspan { spanspan // ---------- 第一步：手动提取并高亮所有代码块 ---------- spanspan letspan processedspan =span propsspan.spancontentspan; spanspan // 正则匹配围栏代码块：```lang\n代码\n```（支持语言可选） spanspan constspan codeBlockRegexspan =span /span```span([spana-zA-Z0-9+#-span]+)span\nspan([span\s\Sspan]*?)span```span/spangspan; spanspan constspan matchesspan =span [span...spanprocessedspan.spanmatchAllspan(spancodeBlockRegexspan)]span; spanspan spanspan forspan (spanconstspan matchspan ofspan matchesspan) span{ spanspan constspan [spanfullMatchspan,span langspan,span codespan]span =span matchspan; spanspan tryspan { spanspan // 调用 Shiki 进行语法高亮，返回 HTML 字符串或包含 HTML 的对象 spanspan constspan highlightedspan =span $shikispan.spancodeToHtmlspan(spancodespan.spantrimspan()span,span { spanspan langspan:span langspan ||span "spantextspan"span,span // 未指定语言时当作纯文本 spanspan themespan:span currentThemespan.spanvaluespan,span // 使用当前主题 spanspan }span)span; spanspan spanspan // 兼容 Shiki 不同版本的返回值（可能直接返回字符串，也可能返回 { html } 对象） spanspan constspan htmlStrspan = spanspan typeofspan highlightedspan ===span "spanstringspan" spanspan ?span highlighted spanspan :span highlightedspan.spanhtmlspan ||span highlightedspan.spanvaluespan ||span Stringspan(spanhighlightedspan)span; spanspan spanspan // 用高亮后的 HTML 替换原始代码块（使用函数替换避免 $ 符号被转义） spanspan processedspan =span processedspan.spanreplacespan(spanfullMatchspan,span ()span =>span htmlStrspan)span; spanspan }span catchspan (spanespan) span{ spanspan consolespan.spanerrorspan(span"span高亮失败:span"span,span espan)span; spanspan // 高亮失败时保留原始代码块（不做高亮） spanspan } spanspan } spanspan spanspan // ---------- 第二步：将处理后的内容（代码块已替换）解析为 Markdown ---------- spanspan constspan htmlspan =span awaitspan markedspan.spanparsespan(spanprocessedspan,span { spanspan breaksspan:span truespan,span // 将换行符转换为
spanspan gfmspan:span truespan,span // 启用 GitHub 风格 Markdown（表格、删除线等） spanspan }span)span; spanspan spanspan // ---------- 第三步：使用 DOMPurify 过滤不安全内容，防止 XSS 攻击 ---------- spanspan renderedContentspan.spanvaluespan =span DOMPurifyspan.spansanitizespan(spanhtmlspan,span { spanspan // 明确允许的 HTML 标签（涵盖所有 Markdown 可能生成的标签） spanspan ALLOWED_TAGSspan:span [ spanspan "spanpspan"span, spanspan "spanbrspan"span, spanspan "spanstrongspan"span, spanspan "spanemspan"span, spanspan "spanuspan"span, spanspan "spansspan"span, spanspan "spandelspan"span, spanspan "spaninsspan"span, spanspan "spanspanspan"span, spanspan "spandivspan"span, spanspan "spanh1span"span, spanspan "spanh2span"span, spanspan "spanh3span"span, spanspan "spanh4span"span, spanspan "spanh5span"span, spanspan "spanh6span"span, spanspan "spanulspan"span, spanspan "spanolspan"span, spanspan "spanlispan"span, spanspan "spanaspan"span, spanspan "spanblockquotespan"span, spanspan "spancodespan"span, spanspan "spanprespan"span, spanspan "spantablespan"span, spanspan "spantheadspan"span, spanspan "spantbodyspan"span, spanspan "spantrspan"span, spanspan "spanthspan"span, spanspan "spantdspan"span, spanspan "spanhrspan"span, spanspan "spanimgspan"span, spanspan "spansubspan"span, spanspan "spansupspan"span, spanspan ]span, spanspan // 允许的属性（class/style 用于代码高亮样式，其他为链接、图片等常用属性） spanspan ALLOWED_ATTRspan:span [ spanspan "spanclassspan"span, spanspan "spanstylespan"span, spanspan "spanhrefspan"span, spanspan "spanlangspan"span, spanspan "spansrcspan"span, spanspan "spanaltspan"span, spanspan "spantitlespan"span, spanspan "spantargetspan"span, spanspan "spanrelspan"span, spanspan ]span, spanspan // 限制 URL 只能使用以下安全协议： spanspan // - http: / https: → 网页链接、图片链接（评论区核心需求） spanspan // - ftp: → 文件下载链接（极少出现，但保留无害） spanspan // - mailto: → 邮箱联系方式（偶尔有人留邮箱） spanspan // - tel: → 电话联系方式（虽少但保留） spanspan // - blob: → 临时文件/本地文件（为可能的图片上传预留） spanspan // - data: → base64 图片（用户直接贴 base64 图片时用） spanspan // 其他协议（如 javascript:、vbscript:、file: 等）一律拦截，防止 XSS 攻击 spanspan ALLOWED_URI_REGEXPspan:span /span^span(spanhttpsspan?|spanftpspan|spanmailtospan|spantelspan|spanblobspan|spandataspan)span:span/spanispan, spanspan spanspan // 是否允许未在 ALLOWED_URI_REGEXP 中列出的协议： spanspan // - true → 正则只作为“推荐列表”，未知协议可能被放行（不安全） spanspan // - false → 正则作为“强制列表”，只有列出的协议才允许（安全） spanspan // 评论区场景必须设置为 false，确保所有 URL 都经过协议白名单检查 spanspan ALLOW_UNKNOWN_PROTOCOLSspan:span falsespan, spanspan }span)span; spanspan }span catchspan (spanerrorspan) span{ spanspan consolespan.spanerrorspan(span"span渲染失败:span"span,span errorspan)span; spanspan // 发生任何错误时，回退显示原始内容 spanspan renderedContentspan.spanvaluespan =span propsspan.spancontentspan; spanspan } spanspan}; spanspan spanspan// 监听内容或主题变化，立即执行一次渲染，之后每次变化重新渲染 spanspanwatchspan([span()span =>span propsspan.spancontentspan,span ()span =>span colorModespan.spanvalue]span,span renderContentspan,span { spanspan immediatespan:span truespan, spanspan}span)span; spanspan spanspan spanspan spanspan/* 样式部分见 3.5 节，此处先省略 */ spanspan h33.4 在评论区中使用p在你的评论区组件（如 codeCommentSection.vue）中引入并使用：precodespanspan spanspan spanspan spanspan spanspan spanspan spanspan spanspan h33.5 样式统一：让评论代码块与文档融为一体pNuxt Content 默认代码块样式带有背景、边框和圆角。我们通过 CSS 变量（由 Nuxt UI 提供）覆盖评论区的 code
和 code 样式，实现视觉统一。如果你未使用 Nuxt UI，可替换为具体的颜色值。p在 codeMarkdownRenderer.vue 的 code