序言
在现代Web开发中,Markdown支持已经成为许多应用不可或缺的功能,尤其是在文档编写、技术博客、知识管理等领域,特别是最近大火的deepseek大语言模型,其UI界面更需要支持Markdown语法的解析。而为了提升用户体验,我们通常还需要集成代码高亮和数学公式的显示功能。今天,我们将通过一个Vue3组件,一次性搞定这些需求。
一、功能概述
本篇将实现以下功能:
- 动态渲染Markdown内容
- 代码块高亮(全自动支持多种语言)
- LaTex数学公式渲染
- 一键复制代码
二、核心代码解析
1、安装依赖
首先,我们先给项目安装以下依赖,后面两个AntDesign Vue是为了弹出提示用的,最后的less是我用的比较习惯,动手能力强的读者可以换成Scss或者原生css。
npm i marked
npm i marked-highlight
npm i github-markdown-css
npm i highlight.js
npm i ant-design-vue
npm i @ant-design/icons-vue
npm i less -D
2、调整mathjax的引用方式
因为mathjax比较古老,不支持import方式引入,所以接下来找到node_modules
下的mathjax
目录,把它拷贝到public
下面
既然已经拷贝过去了,那么我们就可以在index.html
里引用它了
<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>项目的标题</title>
<!-- 重点是下面这行 -->
<script src="/mathjax/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
3、写一个公式渲染工具
在src/util目录下面加上Mathjax.js文件,这样当需要渲染的时候调用它就可以了
let isMathjaxConfig = false;
const initMathjaxConfig = () => {
if (!window.MathJax) {
return;
}
window.MathJax = {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
], // ⾏内公式选择符
displayMath: [
["$$", "$$"],
["\\[", "\\]"],
], // 段内公式选择符
},
options: {
skipHtmlTags: [
"script",
"noscript",
"style",
"textarea",
"pre",
"code",
"a",
], // 避开某些标签
ignoreHtmlClass: "tex2jax_ignore",
processHtmlClass: "tex2jax_process",
},
};
isMathjaxConfig = true;
};
const TypeSet = async function (elementId) {
if (!window.MathJax) {
return;
}
window.MathJax.startup.promise = window.MathJax.startup.promise
.then(() => {
return window.MathJax.typesetPromise();
})
.catch((err) => console.log("Typeset failed: " + err.message));
return window.MathJax.startup.promise;
};
export default {
isMathjaxConfig,
initMathjaxConfig,
TypeSet,
};
4、核心渲染组件
接下来我们写一个Markdown.vue,把这些功能都写出来。
<template>
<div v-html="htmlContent" class="markdown-body"></div>
</template>
<script>
import {defineComponent, nextTick, onMounted, ref, watch} from 'vue';
import {Marked} from 'marked';
import {markedHighlight} from 'marked-highlight';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
// 不喜欢Ant Design Vue,可以不用它
import {message} from 'ant-design-vue';
import 'github-markdown-css/github-markdown-light.css';
import mathJax from "@/util/MathJax";
let doCopy = function (e) {
const code = this.parentNode.parentNode.querySelector('code');
navigator.clipboard
.writeText(code.innerText)
.then(() => {
// 用Ant Design Vue 进行提示,不喜欢可以换掉
message.success('复制成功');
})
.catch((error) => {
// 用Ant Design Vue 进行提示,不喜欢可以换掉
message.error('复制失败');
});
}
export default defineComponent({
name: 'DynamicMarkdown',
props: {
value: {
type: String,
required: true,
},
},
setup(props) {
const htmlContent = ref('');
// 初始化 marked 实例
const marked = new Marked(
markedHighlight({
async: false,
langPrefix: 'language-',
emptyLangClass: 'no-lang',
highlight: (code, language) => {
// 代码高亮自动识别语言的关键就在这里
return hljs.highlightAuto(code, [language]).value;
},
})
);
// 增强代码块功能
const enhanceCodeBlock = (content) => {
return content.replace(/<pre><code/g, '<pre><div class="enhance"><div class="copyCode">复制</div></div><code');
};
// 复制功能绑定
const bindCopyFunction = (el) => {
const codeBlocks = el.querySelectorAll('pre');
codeBlocks.forEach((codeBlock) => {
const enhance = codeBlock.querySelector('.enhance');
if (enhance) {
const copyCode = enhance.querySelector('.copyCode');
if (copyCode) {
copyCode.removeEventListener('click', doCopy);
copyCode.addEventListener('click', doCopy);
}
}
});
};
// 解析 Markdown 内容
const parseMarkdown = () => {
htmlContent.value = props.value
// 这里是关键,很多人就折在这了,时序上一定是先转公式,再取出来转Markdown,最后转代码
nextTick(()=>{
// 先转公式
if (mathJax) {
if (mathJax.isMathjaxConfig) {
mathJax.initMathjaxConfig()
}
mathJax.TypeSet()
}
nextTick(()=>{
htmlContent.value=document.querySelector('.markdown-body').innerHTML
htmlContent.value = enhanceCodeBlock(marked.parse(htmlContent.value));
nextTick(() => {
const el = document.querySelector('.markdown-body');
if (el) {
bindCopyFunction(el);
}
});
})
})
};
// 监听 value 变化
watch(
() => props.value,
(newValue, oldValue) => {
if (newValue) {
parseMarkdown();
}
},
{immediate: true}
);
onMounted(() => {
if (props.value) {
parseMarkdown();
}
});
return {
htmlContent,
};
},
});
</script>
<style lang="less">
.markdown-body {
padding: 20px;
box-sizing: border-box;
}
pre {
position: relative;
}
pre .enhance {
display: flex;
color: #247aaa;
padding: 10px;
box-sizing: border-box;
font-size: 12px;
border-radius: 9px;
justify-content: flex-end;
//background-color: #202020;
position: absolute;
top: 0;
right: 0;
.copyCode {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
color: rgba(2, 120, 255, 0.84);
}
i {
font-size: 16px;
margin-left: 5px;
}
}
}
.markdown-body code, .markdown-body tt{
background-color: #ffe6e6;
color: #df3b3b;
}
</style>
5、调用方示例
为了方便调试,左侧屏幕可以输入markdown,右侧可以实时显示出效果
<template>
<div style="display: flex;height: 100%">
<textarea v-model="value" style="width: 30%;flex-shrink: 0;height: 100%"></textarea>
<Markdown v-model:value="value" style="width: 100%;overflow-y: scroll"></Markdown>
</div>
</template>
<script>
import Markdown from '@/components/Markdown.vue'
export default {
name: 'MarkdownView',
components: {
Markdown
},
data() {
return {
value: ""
}
}
};
</script>
三、运行效果
运行效果如下,很完美的还原了公式,并高亮了代码
四、深夜模式改造
如果白底黑子看的很刺眼,也可以很方便的改为深夜模式,改这两行即可:
然后我们就可以得到一个如五彩斑斓黑一样的界面了:
总结
通过以上实现,我们成功地在Vue3中集成了Markdown渲染、代码高亮和数学公式支持。这个组件不仅可以满足基本的Markdown需求,还提供了额外的代码复制按钮。