大模型流式输出 Markdown?Vue3 指令封装:兼容公式、代码并解决渲染失效
大模型流式输出 Markdown?Vue3 指令封装:兼容公式、代码并解决渲染失效
在Vue3项目开发中,经常会遇到需要渲染大模型输出的Markdown内容的场景,其中可能包含复杂的数学公式、高亮代码块等元素。直接使用第三方组件可能存在定制化不足、公式渲染失效等问题。本文将介绍如何封装一个通用的Vue3指令,实现Markdown内容的高效渲染,同时重点解决数学公式不生效的核心问题。
本文核心内容:
-
Vue3自定义指令封装思路与实现步骤
-
集成highlight.js实现代码高亮
-
兼容多种数学公式语法(、、、$、(、)、[、])
-
深度剖析数学公式不生效的原因及解决方案
-
指令的全局注册与使用示例
一、封装背景与核心需求
在与大模型交互的场景中,大模型返回的内容通常是标准Markdown格式,包含:
-
基础Markdown语法(标题、列表、链接、加粗等)
-
多行/合并单元格表格
-
多种语言的代码块(如Vue、JavaScript、Python等)
-
数学公式(行内公式如a+b=ca+b=ca+b=c,块级公式如∑i=1ni=n(n+1)2\sum_{i=1}^n i=\frac{n(n+1)}{2}i=1∑ni=2n(n+1))
核心需求:通过Vue3指令的方式,实现“即插即用”的Markdown渲染能力,同时确保数学公式能够正确渲染,代码块高亮美观。
二、技术选型
根据需求,选择以下依赖包:
-
markdown-it:轻量、高性能的Markdown解析器,支持插件扩展 -
markdown-it-multimd-table:增强markdown-it的表格功能,支持合并单元格、多行单元格等 -
highlight.js:代码高亮库,支持多种编程语言 -
github-markdown-css:GitHub风格的Markdown样式,美观且通用 -
mathjax:专业的数学公式渲染库,支持LaTeX语法
安装依赖:
npm install markdown-it markdown-it-multimd-table highlight.js github-markdown-css mathjax --save
# 或
yarn add markdown-it markdown-it-multimd-table highlight.js github-markdown-css mathjax
# 或
pnpm install markdown-it markdown-it-multimd-table highlight.js github-markdown-css mathjax
三、Vue3指令完整封装实现
创建文件 directives/vRenderMarkdown.ts,封装核心逻辑:
import 'github-markdown-css/github-markdown-light.css'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css' // 代码高亮样式,可替换为其他样式
import MarkDownIt from 'markdown-it'
import markdownItMultimdTable from 'markdown-it-multimd-table'
// 定义匹配数学公式的正则表达式,用于将不同格式的数学公式替换为标记字符串
const MATH_PATTERNS = [
{
pattern: /\$\$(.*?)\$\$/gs, // 匹配块级公式 $$公式$$
replacement: 'MATH_DISPLAY_DOLLAR_OPEN$1MATH_DISPLAY_DOLLAR_CLOSE',
},
{
pattern: /\$(.*?)\$/gs, // 匹配行内公式 $公式$
replacement: 'MATH_INLINE_DOLLAR_OPEN$1MATH_INLINE_DOLLAR_CLOSE'
},
{
pattern: /\\\[([\s\S]*?)\\\]/g, // 匹配块级公式 \[公式\]
replacement: 'MATH_DISPLAY_BRACKET_OPEN$1MATH_DISPLAY_BRACKET_CLOSE',
},
{
pattern: /\\\(([\s\S]*?)\\\)/g, // 匹配行内公式 \(公式\)
replacement: 'MATH_INLINE_PAREN_OPEN$1MATH_INLINE_PAREN_CLOSE',
},
]
// 定义恢复数学公式的正则表达式,用于将标记字符串替换回原始的数学公式语法
const RESTORE_PATTERNS = [
{
pattern: /MATH_DISPLAY_DOLLAR_OPEN(.*?)MATH_DISPLAY_DOLLAR_CLOSE/gs,
replacement: '$$$1$$'
},
{
pattern: /MATH_INLINE_DOLLAR_OPEN(.*?)MATH_INLINE_DOLLAR_CLOSE/gs,
replacement: '$$1$'
},
{
pattern: /MATH_DISPLAY_BRACKET_OPEN(.*?)MATH_DISPLAY_BRACKET_CLOSE/g,
replacement: '\\[$1\\]'
},
{
pattern: /MATH_INLINE_PAREN_OPEN(.*?)MATH_INLINE_PAREN_CLOSE/g,
replacement: '\\($1\\)'
},
]
/**
* 保护数学公式,将特定数学公式语法替换为标记字符串
* 目的:避免Markdown解析器误处理公式中的特殊字符(如#、*、_等)
* @param content - 待处理的内容
* @returns 处理后的内容
*/
const protectMathExpressions = (content: string) => {
let processedContent = content
for (const { pattern, replacement } of MATH_PATTERNS) {
processedContent = processedContent.replace(pattern, replacement)
}
return processedContent
}
/**
* 恢复数学公式,将标记字符串替换回原始数学公式语法
* 目的:让MathJax能够识别并渲染公式
* @param content - 待处理的内容
* @returns 处理后的内容
*/
const restoreMathExpressions = (content: string) => {
let processedContent = content
for (const { pattern, replacement } of RESTORE_PATTERNS) {
processedContent = processedContent.replace(pattern, replacement)
}
return processedContent
}
/**
* 核心渲染函数
* @param el - 指令绑定的DOM元素
* @param bind - 指令绑定信息(包含传入的Markdown内容)
*/
const renderMarkdown = (el: any, bind: any) => {
const { value } = bind
// 内容为空时不渲染,避免空DOM操作
if (!value) return
// 初始化Markdown解析器,配置基础功能
const md = new MarkDownIt({
linkify: true, // 自动将URL格式的文本转换为链接
typographer: true, // 启用引号美化、破折号替换等语言中立功能
breaks: true, // 将段落内的\n转换为<br>
html: true, // 允许解析源码中的HTML标签
})
// 加载表格增强插件,支持合并单元格、多行单元格等
md.use(markdownItMultimdTable, {
multiline: true, // 启用多行单元格
rowspan: true, // 启用行合并
colspan: true, // 启用列合并
headerless: true, // 允许无表头表格
multibody: true, // 启用多个表格体
aotolabel: true, // 自动为表格添加label属性
})
// 配置代码高亮逻辑
md.set({
highlight: (str: string, lang: string) => {
let highlighted = str // 默认使用原始字符串
// 判断是否支持当前语言高亮,特殊处理vue(按html解析)
if ((lang && hljs.getLanguage(lang)) || lang === 'vue') {
try {
highlighted = hljs.highlight(str, {
language: lang === 'vue' ? 'html' : lang
}).value
} catch (error) {
// 高亮失败时,对HTML特殊字符进行转义,避免XSS
highlighted = md.utils.escapeHtml(str)
console.error('代码高亮失败:', error)
}
} else {
// 不支持的语言,仅转义HTML特殊字符
highlighted = md.utils.escapeHtml(str)
}
// 包裹代码块,方便自定义样式
return `<div class="code-block" data-lang="${lang || 'plaintext'}">${highlighted}</div>`
},
})
// 关键步骤1:保护数学公式,避免被Markdown解析器误处理
const protectedContent = protectMathExpressions(value)
// 解析Markdown为HTML
const html = md.render(protectedContent)
// 关键步骤2:恢复数学公式原始语法,让MathJax识别
const restoredHtml = restoreMathExpressions(html)
// 添加GitHub风格的Markdown样式类
el.classList.add('markdown-content', 'github-markdown')
// 渲染最终HTML
el.innerHTML = restoredHtml
// 关键步骤3:调用MathJax渲染数学公式
if (window.MathJax) {
window.MathJax.typesetPromise()
.then(() => {
console.log('数学公式渲染完成')
})
.catch((error: any) => {
console.error('数学公式渲染失败:', error)
})
} else {
console.warn('未引入MathJax,数学公式无法渲染')
}
}
// 定义Vue3指令
const vRenderMarkdown = {
// 元素挂载时执行渲染
mounted(el: any, bind: any) {
renderMarkdown(el, bind)
},
// 元素更新时重新渲染(处理内容变化场景)
updated(el: any, bind: any) {
// 避免重复渲染(当内容未变化时)
if (bind.value === bind.oldValue) return
renderMarkdown(el, bind)
},
// 元素卸载时清理(可选,根据需求添加)
unmounted(el: any) {
el.innerHTML = ''
el.classList.remove('markdown-content', 'github-markdown')
}
}
// 导出指令,供全局或局部注册使用
export default vRenderMarkdown
四、核心问题:数学公式不生效的原因与解决方案
在封装过程中,最容易遇到的问题就是数学公式渲染失效。下面从根源分析原因,并讲解我们封装中的解决方案。
4.1 公式不生效的核心原因
Markdown解析器(如markdown-it)会解析内容中的特殊字符(如$、*、_、[、]等),而数学公式的语法恰好依赖这些特殊字符。例如:
-
行内公式
$a*b=c$中的*会被Markdown解析为斜体标记,导致公式结构被破坏 -
块级公式
$$\sum_{i=1}^n i$$中的_会被解析为下划线标记,导致MathJax无法识别
简单来说:Markdown解析器与数学公式语法存在冲突,导致公式被误解析,最终无法正常渲染。
4.2 解决方案:公式保护-解析-恢复三步法
我们的核心思路是:在Markdown解析前,先将公式替换为临时标记(避免被误解析);解析完成后,再将临时标记恢复为原始公式语法,最后由MathJax渲染。
-
第一步:公式保护(protectMathExpressions)
使用正则表达式匹配所有类型的数学公式(行内:$...$、\(...\);块级:$$...$$、\[...\]),将其替换为自定义的临时标记(如MATH_INLINE_DOLLAR_OPEN...MATH_INLINE_DOLLAR_CLOSE)。 示例:$a+b=c$ → MATH_INLINE_DOLLAR_OPENa+b=cMATH_INLINE_DOLLAR_CLOSE 这样Markdown解析器就不会处理公式中的特殊字符了。 -
第二步:Markdown解析
对替换后的内容进行Markdown解析,生成HTML。此时临时标记不会被解析,保证了公式结构的完整性。 -
第三步:公式恢复(restoreMathExpressions)
使用正则表达式将临时标记替换回原始的公式语法。 示例:MATH_INLINE_DOLLAR_OPENa+b=cMATH_INLINE_DOLLAR_CLOSE → $a+b=c$ 最后调用MathJax的typesetPromise()方法,渲染恢复后的公式。注意:正则表达式中使用了
gs修饰符:g表示全局匹配,s表示让.匹配换行符(解决公式跨多行的场景)。
4.3 补充:MathJax的正确引入
即使完成了上述三步,若MathJax未正确引入,公式仍无法渲染。需在项目入口HTML文件(如public/index.html)的中引入MathJax:
<head>
<!-- 其他配置 -->
<script async src="https://cdn.bootcdn.net/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js"></script>
<!-- 配置MathJax,可选 -->
<script>
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']], // 行内公式分隔符
displayMath: [['$$', '$$'], ['\\[', '\\]']], // 块级公式分隔符
processEscapes: true // 允许使用\$转义$符号
}
};
</script>
</head>
五、指令的注册与使用
5.1 全局注册(推荐)
在main.ts中注册指令,全局可用:
import { createApp } from 'vue'
import App from './App.vue'
import vRenderMarkdown from './directives/vRenderMarkdown'
const app = createApp(App)
// 全局注册Markdown渲染指令
app.directive('render-markdown', vRenderMarkdown)
app.mount('#app')
5.2 局部注册(按需使用)
在单个组件中注册使用:
<script setup lang="ts">
import vRenderMarkdown from '@/directives/vRenderMarkdown'
</script>
<template>
<div v-render-markdown="markdownContent"></div>
</template>
5.3 使用示例
<script setup lang="ts">
// 模拟大模型返回的Markdown内容
const markdownContent = `# Vue3 Markdown渲染指令示例
## 1. 基础语法
- 列表项1
- 列表项2
- **加粗文本**
- *斜体文本*
## 2. 代码块
\`\`\`typescript
// 示例代码
const add = (a: number, b: number) => a + b
\`\`\`
## 3. 数学公式
### 行内公式
- 勾股定理:$a^2 + b^2 = c^2$
- 欧拉公式:$e^{i\pi} + 1 = 0$
### 块级公式
$$
\sum_{i=1}^n i = \frac{n(n+1)}{2}
$$
$$
\int_0^1 x^2 dx = \frac{1}{3}
$$
## 4. 表格(合并单元格)
| 姓名 | 年龄 | 性别 |
|------|------|------|
| 张三 | 20 | 男 |
| 李四 | 22 | 女 |
| 王五 | 25 | 男 |
`
</script>
<template>
<div class="markdown-container">
<div v-render-markdown="markdownContent"></div>
</div>
</template>
<style scoped>
.markdown-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
/* 自定义代码块样式 */
.code-block {
margin: 10px 0;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
}
</style>
六、进阶优化与注意事项
6.1 进阶优化
-
样式自定义:可替换github-markdown-css为其他样式(如vuepress风格、掘金风格),也可自定义.code-block等类名的样式。
-
性能优化:在updated钩子中判断内容是否变化,避免重复渲染;对于大量内容,可添加节流处理。
-
扩展功能:可通过markdown-it的插件扩展其他功能,如支持流程图(markdown-it-flowchart)、任务列表(markdown-it-task-lists)等。
6.2 注意事项
-
MathJax引入方式:建议使用CDN引入,避免打包体积过大;若需本地引入,需配置webpack/vite的相关规则。
-
公式语法规范:确保大模型返回的数学公式语法正确(如括号匹配、转义字符正确),否则MathJax无法正常渲染。
-
XSS防护:虽然markdown-it的escapeHtml方法会转义HTML特殊字符,但仍需注意用户输入的内容(若包含用户输入),建议添加XSS过滤库(如xss)。
-
样式冲突:若项目中已有全局样式,可能会与github-markdown-css冲突,可通过增加样式优先级(如使用scoped+deep)解决。
七、总结
本文通过Vue3自定义指令的方式,封装了一套完整的Markdown渲染方案,核心解决了大模型输出内容中数学公式、代码块、复杂表格的渲染问题。其中“公式保护-解析-恢复”三步法是解决数学公式不生效的关键,通过该方案可实现“即插即用”的Markdown渲染能力,大幅提升开发效率。
该指令可广泛应用于大模型交互、知识库、文档系统等场景,根据实际需求扩展插件和样式即可满足不同项目的定制化需求。
在Vue3项目开发中,经常会遇到需要渲染大模型输出的Markdown内容的场景,其中可能包含复杂的数学公式、高亮代码块等元素。直接使用第三方组件可能存在定制化不足、公式渲染失效等问题。本文将介绍如何封装一个通用的Vue3指令,实现Markdown内容的高效渲染,同时重点解决数学公式不生效的核心问题。
更多推荐



所有评论(0)