Vue3集成MarkDown格式+MathJax数学公式展示
前言
最近写了一个AI对话,接入了DeepSeek接口,但是苦于无法将AI回复的内容进行MarkDown格式化,虽然之前也做过一个UniApp版的,用的是ua-markdown插件,这次做的是Web版的,想换个插件试试,网上查了好多,真正能实现的并不多,所有我也整理一份,帮助大家快速实现。
一、功能概述与准备工作
核心功能实现目标
- 动态渲染Markdown内容:将Markdown文本转换为HTML并正确渲染
- 代码块高亮:自动识别多种编程语言并应用语法高亮
- LaTeX数学公式渲染:支持内联和块级数学公式展示
- 一键复制代码:为用户提供便捷的代码复制功能
我这里就直接用我现在做的Vue3项目,大家可以根据自己项目进行调整
二、依赖安装与配置
1. 安装必要依赖
npm install marked marked-highlight github-markdown-css highlight.js element-plus
npm i less -D
2. 调整MathJax的引用方式
因为mathjax不支持import方式引入,所以需要将MathJax的文件从node_modules目录下拷贝到public目录中
3. 自定义MathJax配置,解析数学公式
在src/utils文件夹下创一个文件MathJax.ts文件,编写如下代码:
这里我用的是TypeScript,如果是JavaScript自行将ts转为js
// Declare MathJax as a global property on the Window interface
// This is TypeScript syntax to extend the global Window type
declare global {
interface Window {
MathJax: any; // Declare that window.MathJax exists (type 'any' for simplicity)
}
}
// Configure MathJax settings by assigning to window.MathJax
window.MathJax = {
// TeX input processor configuration
tex: {
// Define delimiters for inline math expressions:
inlineMath: [
["$", "$"], // $...$ for inline math
["\\(", "\\)"] // \(...\) alternative syntax
],
// Define delimiters for display (block) math expressions:
displayMath: [
["$$", "$$"], // $$...$$ for block math
["\\[", "\\]"] // \[...\] alternative syntax
]
},
// General processing options
options: {
// HTML tags that MathJax should skip/ignore
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
// CSS class that tells MathJax to ignore elements
ignoreHtmlClass: "tex2jax_ignore",
// CSS class that tells MathJax to explicitly process elements
processHtmlClass: "tex2jax_process",
}
};
// Export the MathJax configuration (though it's already globally available)
export default window.MathJax;
这里还没完,还需要再main.ts中引入mathJax和上面组件
import "@/utils/MathJax"; // 必须在引入mathjax前引入mathjax的配置文件
import "mathjax/es5/tex-svg"; // 引入 tex-svg.js 样式
4. 创建渲染组件MarkDown.vue
这里的MarkDown.vue主要是将html渲染成markdown格式,代码如下:
<template>
<div v-html="htmlContent" class="markdown-body"></div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref, watch, defineProps} from 'vue';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import { ElMessage } from 'element-plus';
import 'github-markdown-css/github-markdown-light.css';
const props = defineProps<{ value: string }>();
const htmlContent = ref('');
let doCopy = (e: Event) => {
const code = (e.currentTarget as HTMLElement).parentNode?.parentNode?.querySelector('code');
if (code) {
navigator
.clipboard
.writeText((code as HTMLElement).innerText)
.then(() => {
ElMessage.success('复制成功');
})
.catch(() => {
ElMessage.error('复制失败');
});
}
};
const marked = new Marked(
markedHighlight({
async: false,
langPrefix: 'language-',
emptyLangClass: 'no-lang',
highlight: (code, language) => {
return hljs.highlightAuto(code, [language]).value;
},
})
);
const enhanceCodeBlock = (content: any) => {
// 直接在<pre>内插入复制按钮,确保唯一性和样式优先级
return content.replace(/<pre><code/g, `<pre><div class="enhance"><div class="copyCode">复制</div></div><code`);
};
const bindCopyFunction = (el: Element) => {
const codeBlocks = el.querySelectorAll('pre');
// console.log("codeBlocks = ", codeBlocks);
codeBlocks.forEach((codeBlock) => {
const enhance = codeBlock.querySelector('.enhance');
// console.log("enhance = ", enhance);
if (enhance) {
const copyCode = enhance.querySelector('.copyCode');
// console.log("copyCode = ", copyCode);
if (copyCode) {
copyCode.removeEventListener('click', doCopy);
copyCode.addEventListener('click', doCopy);
}
}
});
};
let globalMathIdx = 0;
// 替换数学公式的标识符,以防marked.js的渲染将数学公式改变,导致MathJax无法正常渲染数学公式
const processEscapes = (raw: string) => {
const mathMap = new Map();
// 获取时间戳
const timestamp = new Date().getTime();
// 匹配 $$...$$
raw = raw.replace(/\$\$([\s\S]+?)\$\$/g, (m) => {
const key = `MATH_BLOCK_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
mathMap.set(key, m);
return key;
});
// 匹配 \[...\]
raw = raw.replace(/\\\[([\s\S]+?)\\\]/g, (m) => {
const key = `MATH_BLOCK_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
mathMap.set(key, m);
return key;
});
// 匹配 $...$
raw = raw.replace(/\$(.+?)\$/g, (m) => {
const key = `MATH_INLINE_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
mathMap.set(key, m);
return key;
});
// 匹配 \\(...\\)
raw = raw.replace(/\\\((.+?)\\\)/g, (m) => {
const key = `MATH_INLINE_${timestamp}_${globalMathIdx++}__TRANSTRATION`;
mathMap.set(key, m);
return key;
});
return { raw, mathMap };
}
// 还原数学公式的标识符,在marked渲染后执行
// 将前面替换的数学公式标识符替换回来,然后再由MarkJax渲染数学公式
const restoreEscapes = (raw: string, mathMap: Map<string, string>) => {
mathMap.forEach((val, key) => {
raw = raw.replaceAll(key, val);
});
return raw;
}
const parseMarkdown = () => {
// 数学公式占位符处理
// 后面marked解析后再替换回来
const { raw, mathMap } = processEscapes(props.value);
htmlContent.value = raw;
nextTick(() => {
const el = document.querySelectorAll('.markdown-body');
const item = el[el.length - 1];
// marked 解析
let html: any = marked.parse(raw) || '';
// 还原数学公式
html = restoreEscapes(html, mathMap);
htmlContent.value = enhanceCodeBlock(html);
nextTick(() => {
if (el) {
if (window.MathJax && window.MathJax.typesetPromise) {
window.MathJax.typesetPromise([item]);
}
bindCopyFunction(item);
}
});
});
};
watch(
() => props.value,
() => {
parseMarkdown();
},
{ immediate: true }
);
onMounted(() => {
if (props.value) {
parseMarkdown();
}
});
</script>
<style lang="less">
.markdown-body {
padding: 0 10px;
box-sizing: border-box;
}
pre {
position: relative;
}
pre .enhance {
display: flex;
color: #247aaa;
padding: 5px 5px 0 0;
box-sizing: border-box;
font-size: 12px;
border-radius: 9px;
justify-content: flex-end;
align-items: flex-start;
position: absolute;
top: 0;
right: 0;
height: 36px;
z-index: 2;
background: transparent;
}
pre .enhance .copyCode {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: #fff;
border-radius: 6px;
padding: 2px 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
margin-top: 4px;
margin-right: 4px;
transition: color 0.2s;
font-weight: bold;
&:hover {
color: rgba(2, 120, 255, 0.84);
background: #f0f7ff;
}
i {
font-size: 16px;
margin-left: 5px;
}
}
.markdown-body code,
.markdown-body tt {
background-color: #ffe6e6;
color: #df3b3b;
}
</style>
三、测试与运行
前面已经把基本的配置完成了,接下来就应用到我的项目中,然后直接上效果图
这里就直接安装如下操作引入MarkDown组件,value的参数填写需要转换的html文本即可
<template>
<div>
<MarkDown :value="content"></MarkDown>
</div>
</template>
<script lang="ts" setup>
import {nextTick, onMounted, ref} from 'vue';
import MarkDown from "@/components/MarkDown.vue";
const content = ref('替换成需要解析的文本');
</script>
总结
通过这套方案,你可以构建出功能丰富、用户体验良好的Markdown展示界面,支持代码块高亮和LaTex数学公式渲染,并提供代码复制等功能。希望本片文章对你有所帮助,谢谢观看!如果有任何问题,欢迎随时向我询问。