大模型流式输出 Markdown?Vue3 指令封装:兼容公式、代码并解决渲染失效

在Vue3项目开发中,经常会遇到需要渲染大模型输出的Markdown内容的场景,其中可能包含复杂的数学公式、高亮代码块等元素。直接使用第三方组件可能存在定制化不足、公式渲染失效等问题。本文将介绍如何封装一个通用的Vue3指令,实现Markdown内容的高效渲染,同时重点解决数学公式不生效的核心问题。

本文核心内容:

  • Vue3自定义指令封装思路与实现步骤

  • 集成highlight.js实现代码高亮

  • 兼容多种数学公式语法(、、$、(、)、[、])

  • 深度剖析数学公式不生效的原因及解决方案

  • 指令的全局注册与使用示例

一、封装背景与核心需求

在与大模型交互的场景中,大模型返回的内容通常是标准Markdown格式,包含:

  1. 基础Markdown语法(标题、列表、链接、加粗等)

  2. 多行/合并单元格表格

  3. 多种语言的代码块(如Vue、JavaScript、Python等)

  4. 数学公式(行内公式如a+b=ca+b=ca+b=c,块级公式如∑i=1ni=n(n+1)2\sum_{i=1}^n i=\frac{n(n+1)}{2}i=1ni=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渲染。

  1. 第一步:公式保护(protectMathExpressions)

     使用正则表达式匹配所有类型的数学公式(行内:$...$、\(...\);块级:$$...$$、\[...\]),将其替换为自定义的临时标记(如MATH_INLINE_DOLLAR_OPEN...MATH_INLINE_DOLLAR_CLOSE)。
    
     示例:$a+b=c$ → MATH_INLINE_DOLLAR_OPENa+b=cMATH_INLINE_DOLLAR_CLOSE
    
     这样Markdown解析器就不会处理公式中的特殊字符了。
    
  2. 第二步:Markdown解析

     对替换后的内容进行Markdown解析,生成HTML。此时临时标记不会被解析,保证了公式结构的完整性。
    
  3. 第三步:公式恢复(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内容的高效渲染,同时重点解决数学公式不生效的核心问题。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐