现在有很多插件可以做到markdown解析,下面给大家介绍一下业务中常用的
主流 Markdown 解析库
-
特点:轻量级(无依赖)、高性能(流式处理)、支持 CommonMark 和 GFM 规范。
使用场景:快速解析静态内容或实时预览(如编辑器)。
示例:
js
import { marked } from 'marked';
const html = marked.parse('**Bold text**'); // 输出: <p><strong>Bold text</strong></p>
-
特点:高扩展性(插件生态)、支持自定义渲染规则(如表格、目录生成)。
使用场景:复杂需求(数学公式、代码高亮、Vue 组件嵌入)。
示例(配合代码高亮):
js
import markdownIt from 'markdown-it';
import hljs from 'highlight.js';
const md = markdownIt({ highlight: (code, lang) => hljs.highlight(code, { language: lang }).value });
markdown-it
我们项目使用的是markdown-it。它是一个功能强大的Markdown解析器,支持丰富的Markdown语法,可以将Markdown文本转换为HTML格式。它还支持各种配置选项和插件系统。
安装:npm install markdown-it highlight.js
配置选项:
js
const md = new MarkdownIt({
html: false, // 在源码中启用 HTML 标签
xhtmlOut: false, // 使用 '/' 来闭合单标签 (比如 <br />)。
// 这个选项只对完全的 CommonMark 模式兼容。
breaks: false, // 转换段落里的 '\n' 到 <br>。
langPrefix: 'language-', // 给围栏代码块的 CSS 语言前缀。对于额外的高亮代码非常有用。
linkify: false, // 将类似 URL 的文本自动转换为链接。
// 启用一些语言中立的替换 + 引号美化
typographer: false,
// 双 + 单引号替换对,当 typographer 启用时。
// 或者智能引号等,可以是 String 或 Array。
// 比方说,你可以支持 '<<>>„"' 给俄罗斯人使用, '„"‚'' 给德国人使用。
// 还有 ['<<\xA0', '\xA0>>', '‹\xA0', '\xA0›'] 给法国人使用(包括 nbsp)。
quotes: '""''',
// 高亮函数,会返回转义的HTML。
// 或 '' 如果源字符串未更改,则应在外部进行转义。
// 如果结果以 <pre ... 开头,内部包装器则会跳过。
highlight: function (/*str, lang*/) { return ''; }
});
demo:
在公共组件markdown文件下创建copy.ts公共方法
ts
// 复制方法
export const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
// 显示成功反馈
console.log("复制成功!");
} catch (err) {
console.error("复制失败:", err);
// 回退到document.execCommand方法
const textarea = document.createElement("textarea");
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
// 显示成功反馈
}
};
创建highlight.ts
ts
// https://github.com/highlightjs/highlight.js/tree/main/src/styles highlight.js样式库
import hljs from "highlight.js";
// import "highlight.js/styles/github.css"; // GitHub 风格
import 'highlight.js/styles/atom-one-light.css' // Atom One Light 风格
// import 'highlight.js/styles/ascetic.css'
// 注册 Vue 语法
hljs.registerLanguage('vue', function(hljs) {
return {
subLanguage: 'xml',
contains: [
hljs.COMMENT('<!--', '-->', {
relevance: 10
}),
{
begin: /^(\s*)(<script>)/,
end: /^(\s*)(<\/script>)/,
subLanguage: 'javascript',
excludeBegin: true,
excludeEnd: true
},
{
begin: /^(\s*)(<style(\s+scoped)?>)/,
end: /^(\s*)(<\/style>)/,
subLanguage: 'css',
excludeBegin: true,
excludeEnd: true
}
]
};
});
// 然后初始化 highlight.js
hljs.initHighlightingOnLoad();
export default hljs;
封装markdowm.vue组件
js
<template>
<div ref="containerRef" class="markdown-body" v-html="safeHtml" @click="handleClick" />
</template>
<script setup lang="ts">
import MarkdownIt from "markdown-it";
import DOMPurify from "dompurify";
import hljs from "./highlight";
import { ref, watch, nextTick } from "vue";
import { copyToClipboard } from "./copy";
const props = defineProps<{
content: string;
}>();
const containerRef = ref<HTMLElement | null>(null);
const safeHtml = ref("");
// 渲染Markdown内容
const renderMarkdown = (content: string) => {
const md = new MarkdownIt({
html: true, // 允许HTML标签
breaks: true, // 允许换行符
linkify: true, // 允许链接
typographer: true,
highlight: (code: string, lang: string) => {
// Base64编码特殊字符
const base64Content = btoa(encodeURIComponent(code));
if (lang && hljs.getLanguage(lang)) {
try {
return `<div class="markdown_header">
<span>${lang}</span>
<div class="copy-btn" data-copy="${base64Content}">复制</div>
</div><pre class="hljs"><code>${
hljs.highlight(code, {
language: lang,
ignoreIllegals: true
}).value
}</code></pre>`;
} catch (__) {}
}
// 确保所有情况都有复制按钮
return `
<div class="markdown_header">
<span>plaintext</span>
<div class="copy-btn" data-copy="${base64Content}">复制</div>
</div><pre class="hljs"><code>${md.utils.escapeHtml(code)}</code></pre>
`;
}
}) as MarkdownIt;
// 安全渲染方法
return DOMPurify.sanitize(md.render(content));
};
// 使用watch确保内容渲染完成
watch(
() => props.content,
(newContent) => {
safeHtml.value = renderMarkdown(newContent);
// 在DOM更新后处理特殊状态
nextTick(() => {
if (containerRef.value) {
containerRef.value.querySelectorAll(".copy-btn").forEach((btn) => {
if (!btn.hasAttribute("data-copy")) {
console.warn("未设置data-copy的按钮", btn);
}
});
}
});
},
{ immediate: true }
);
// 事件委托处理(解决多实例问题)
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const copyBtn = target.closest(".copy-btn");
if (!copyBtn) return;
if (!containerRef.value?.contains(copyBtn)) return;
// 处理无属性的情况
const base64Content = copyBtn.getAttribute("data-copy") || "";
if (!base64Content) {
console.error("复制按钮缺少data-copy属性", copyBtn);
return;
}
// 解码
const text = base64ToText(base64Content);
copyToClipboard(text);
// 视觉反馈
const originalText = copyBtn.textContent;
copyBtn.textContent = "已复制";
setTimeout(() => {
if (copyBtn.textContent === "已复制") {
copyBtn.textContent = originalText;
}
}, 1500);
};
// Base64解码函数(安全处理特殊字符)
function base64ToText(base64: string) {
try {
return decodeURIComponent(atob(base64));
} catch {
// 尝试备选解码方案
try {
return decodeURIComponent(
atob(base64)
.split("")
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join("")
);
} catch {
return base64; // 最终回退
}
}
}
</script>
<!-- 全局样式 -->
<style lang="scss">
.markdown-body {
font-family: system-ui, sans-serif;
line-height: 1.6;
ul,
ol {
padding-left: 2em;
margin: 0.8em 0;
}
table {
border-collapse: collapse;
margin: 1em 0;
th,
td {
padding: 0.6em 1em;
border: 1px solid #e5e7eb;
}
}
}
</style>
<style scoped lang="scss">
.markdown-body ::v-deep {
h1,
h2,
h3 {
margin: 1em 0;
}
.markdown_header {
background-color: #ededed;
border-radius: 10px 10px 0 0;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.copy-btn {
cursor: pointer;
padding: 4px 8px;
background-color: #f0f0f0;
border-radius: 4px;
font-size: 0.85em;
user-select: none;
transition: background-color 0.2s;
&:hover {
background-color: #e0e0e0;
}
// 添加点击效果
&:active {
transform: scale(0.95);
}
}
}
.hljs {
//新增
border: 1px solid #ededed;
padding: 1rem;
margin: 0;
}
}
</style>
使用
js
<Markdown :content="message.content"></Markdown>

安全处理(XSS 防护)
解析库最好配合消毒库,避免恶意脚本注入:
推荐库:DOMPurify
功能:
- 移除恶意脚本:删除或转义 HTML 中的
<script>
标签及其内容,防止执行恶意 JavaScript 代码。 - 过滤不安全的属性:移除或转义 HTML 标签中的不安全属性,例如
onload
、onclick
等事件处理器,这些属性可能被用来注入恶意代码。 - 处理不安全的 URL:过滤掉可能指向恶意网站或包含恶意代码的 URL,例如
javascript:
协议的链接。 - 自定义规则:允许开发者根据需要自定义允许或禁止的标签和属性,以满足特定的安全需求。
安装:npm install dompurify
示例:
js
import DOMPurify from 'dompurify';
const safeHTML = DOMPurify.sanitize(marked.parse(userInput));