李锋镝的博客

  • 首页
  • 时间轴
  • 插件
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
  • 留言
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. AI
  3. 正文

打造AI应用的高颜值答案展示:基于Vue3.5+MarkdownIt构建专业级富文本渲染组件

2025年12月3日 65点热度 0人点赞 0条评论

在AI聊天、智能问答类应用中,AI生成内容的呈现效果直接决定了用户的使用体验。纯文本的展示形式不仅会让代码块、表格等结构化内容变得杂乱难读,还会因缺乏交互能力降低用户操作效率。本文将从需求痛点出发,基于Vue3.5的组合式API,结合MarkdownIt解析引擎与Prism.js语法高亮工具,一步步构建一个集结构化渲染、专业语法高亮、便捷交互、高性能适配于一体的AI答案美化组件,同时补充多场景扩展方案与性能优化技巧,让AI回复既“赏心悦目”又“高效实用”。

一、需求洞察:AI答案展示的核心痛点与解决方向

当AI返回包含代码、表格、列表等复杂格式的内容时,传统纯文本展示会暴露出诸多体验短板,这些痛点也是我们构建美化组件的核心出发点:

  • 格式混乱:代码块无缩进、无颜色区分,大段代码挤成一团;表格无边框、无对齐,数据对比困难;多级列表层级模糊,阅读逻辑混乱。
  • 交互缺失:无法一键复制代码,需手动框选;长文本无分段,移动端阅读体验差;特殊内容(如公式、流程图)无法精准渲染。
  • 性能瓶颈:AI流式输出时,频繁的DOM更新导致页面卡顿;大体积内容渲染时,主线程阻塞引发交互延迟。
  • 兼容性弱:不同浏览器对特殊语法的解析差异大,部分Markdown扩展语法(如脚注、任务列表)无法识别。

针对这些痛点,我们的组件需要实现完整的Markdown语法支持、专业的代码高亮、便捷的交互功能、高性能的动态渲染四大核心目标,同时具备可扩展、可定制的特性,适配不同AI应用的品牌风格。

二、技术选型:为什么是Vue3.5+MarkdownIt+Prism.js?

为了平衡功能完整性、性能与开发效率,我们对技术栈进行了多维度对比,最终确定以下核心工具组合:

技术模块 选型方案 选型依据 替代方案对比
前端框架 Vue3.5(组合式API) 轻量高效的响应式系统,组合式API便于逻辑拆分与复用;支持nextTick、computed等特性,适配动态DOM渲染的时序控制;Vue3.5的watch选项flush:post可精准控制DOM更新时机 React:需额外引入状态管理库处理响应式,动态DOM事件绑定成本更高;Angular:体积大,学习成本高,不适合轻量级组件开发
Markdown解析 MarkdownIt 轻量级(核心体积仅16KB)、高性能,支持丰富的插件扩展(如GFM语法、脚注、任务列表);可自定义渲染规则,便于嵌入交互元素;社区生态成熟,文档完善 marked:扩展性弱,自定义渲染逻辑复杂;remark:学习曲线陡,适合复杂AST处理,轻量组件场景下冗余
语法高亮 Prism.js 模块化设计,支持按需加载语言包,减少打包体积;主题丰富且可自定义,语法识别精准;支持行号显示、代码高亮行等高级功能 highlight.js:默认加载全量语言包,体积较大;语法高亮精度略低于Prism.js,自定义主题成本高
UI交互支持 Element Plus 提供成熟的通知、弹窗组件,可快速实现“复制成功”反馈;组件样式统一,便于与自定义样式融合;支持响应式适配,降低移动端适配成本 Ant Design Vue:体积略大,部分组件风格与轻量化场景不符;原生CSS:需手动实现交互反馈,开发效率低

此外,我们还会引入copy-to-clipboard实现跨浏览器的代码复制、MutationObserver监听动态DOM变化、lodash-es的防抖节流工具优化高频操作,形成完整的技术闭环。

三、架构设计:分层解耦的组件体系

为了保证组件的可维护性与可扩展性,我们采用三层架构设计,将渲染、解析、交互逻辑完全分离,遵循单一职责原则:

  1. Markdown解析层:负责将原始Markdown文本转化为带样式标记的HTML字符串,核心是封装MarkdownIt的配置与插件,实现自定义渲染规则。
  2. 交互功能层:负责处理代码复制、表格响应式适配、长文本滚动等交互逻辑,通过DOM监听与事件委托实现动态内容的交互绑定。
  3. Vue组件层:作为组件的对外入口,负责接收数据、触发解析、管理样式与状态,同时处理响应式数据更新与生命周期钩子。

三层架构的核心优势在于逻辑解耦:解析层只需关注语法转化,交互层只需关注用户操作,组件层只需关注数据流转,任一模块的修改都不会影响其他模块,便于单元测试与功能扩展。

四、核心功能实现:从解析到交互的全流程拆解

(一)Markdown解析层:自定义渲染规则,实现语法高亮与结构美化

解析层的核心是markdownUtil.js工具文件,通过配置MarkdownIt并扩展其渲染逻辑,实现从Markdown文本到结构化HTML的转化,同时集成Prism.js完成代码高亮。

1. 基础配置与插件扩展

首先初始化MarkdownIt实例,开启基础语法支持,并引入插件扩展GFM(GitHub Flavored Markdown)语法,实现表格、任务列表、脚注等扩展功能:

// utils/markdownUtil.js
import MarkdownIt from 'markdown-it'
import prism from 'prismjs'
import 'prismjs/themes/prism-tomorrow.css' // 引入高亮主题
import markdownItTaskLists from 'markdown-it-task-lists' // 任务列表插件
import markdownItFootnote from 'markdown-it-footnote' // 脚注插件
import markdownItTableOfContents from 'markdown-it-table-of-contents' // 目录插件

// 初始化MarkdownIt实例
const md = new MarkdownIt({
  html: true, // 允许解析HTML标签
  linkify: true, // 自动识别链接并转化为a标签
  breaks: true, // 换行符转化为<br>
  typographer: true, // 启用排版优化(如替换引号为弯引号)
  highlight: (str, lang) => {
    // 重写代码块高亮逻辑
    if (!lang || !prism.languages[lang]) {
      // 不支持的语言,返回转义后的纯文本
      return `<pre class="code-block"><code>${md.utils.escapeHtml(str)}</code></pre>`
    }
    // 使用Prism.js进行语法高亮
    const highlightedCode = prism.highlight(str, prism.languages[lang], lang)
    // 注入代码头部(语言标识+复制按钮)
    return `<pre class="code-block language-${lang}">
      <div class="code-header">
        <span class="lang-label">${lang}</span>
        <button class="copy-btn" type="button">复制代码</button>
      </div>
      <code class="code-content">${highlightedCode}</code>
    </pre>`
  }
})

// 注册扩展插件
md.use(markdownItTaskLists, { enabled: true })
md.use(markdownItFootnote)
md.use(markdownItTableOfContents, {
  includeLevel: [1, 2, 3], // 仅生成h1-h3的目录
  containerClass: 'toc-container' // 目录容器类名
})

// 暴露渲染方法
export const renderMarkdown = (content) => {
  if (!content) return ''
  return md.render(content)
}

在这段代码中,highlight函数是核心:它会拦截MarkdownIt的代码块渲染流程,先通过Prism.js完成语法着色,再为代码块添加包含“语言标签”和“复制按钮”的头部结构,既实现了专业高亮,又为后续交互埋下伏笔。

2. 自定义表格渲染(优化移动端适配)

默认的Markdown表格在移动端会出现横向溢出问题,我们可以通过重写MarkdownIt的表格渲染规则,为表格添加响应式容器:

// 重写表格渲染规则
const defaultTableRender = md.renderer.rules.table_open
md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
  // 为表格外层包裹响应式容器
  return `<div class="table-responsive">${defaultTableRender(tokens, idx, options, env, self)}`
}
md.renderer.rules.table_close = (tokens, idx, options, env, self) => {
  return self.renderToken(tokens, idx, options) + '</div>'
}

配合CSS样式,即可实现移动端表格横向滚动,避免内容溢出:

.table-responsive {
  width: 100%;
  overflow-x: auto;
  margin: 16px 0;
  border-radius: 4px;
}
table {
  width: 100%;
  border-collapse: collapse;
}
th, td {
  padding: 8px 12px;
  border: 1px solid #e5e7eb;
  text-align: left;
}
th {
  background-color: #f9fafb;
  font-weight: 600;
}

(二)交互功能层:动态DOM监听与便捷操作实现

AI答案常为流式输出(内容逐步追加),传统的onMounted事件绑定会因DOM动态更新失效。我们通过MutationObserver监听DOM变化,结合事件委托实现交互功能的动态绑定。

1. 代码复制功能

首先封装复制工具函数,利用copy-to-clipboard实现跨浏览器复制,并通过Element Plus的ElMessage提供操作反馈:

// utils/interactiveUtil.js
import copy from 'copy-to-clipboard'
import { ElMessage } from 'element-plus'

// 代码复制逻辑
export const handleCodeCopy = (event) => {
  const target = event.target
  // 仅响应复制按钮的点击
  if (!target.classList.contains('copy-btn')) return

  // 找到对应的代码内容元素
  const codeContent = target.parentElement.nextElementSibling
  if (!codeContent) return

  // 执行复制
  const success = copy(codeContent.textContent)
  if (success) {
    ElMessage.success('代码复制成功')
  } else {
    ElMessage.error('代码复制失败,请手动复制')
  }
}

// 事件委托绑定(全局仅绑定一次)
export const bindCopyEvent = () => {
  document.removeEventListener('click', handleCodeCopy)
  document.addEventListener('click', handleCodeCopy)
}

// DOM变化监听
export const observeDomChange = () => {
  const observer = new MutationObserver((mutations) => {
    let hasNewCodeBlock = false
    mutations.forEach((mutation) => {
      // 检测是否有新的代码块节点添加
      if (mutation.type === 'childList' && mutation.addedNodes.length) {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === 1 && (node.classList.contains('code-block') || node.querySelector('.code-block'))) {
            hasNewCodeBlock = true
          }
        })
      }
    })
    // 有新代码块则重新绑定事件(实际通过事件委托无需重复绑定,此处为兼容特殊场景)
    if (hasNewCodeBlock) {
      setTimeout(bindCopyEvent, 100)
    }
  })

  // 监听body下的所有DOM变化
  observer.observe(document.body, {
    childList: true,
    subtree: true
  })

  return observer
}

事件委托的优势在于全局仅需绑定一次点击事件,无论DOM如何动态更新,都能响应复制按钮的点击,相比为每个按钮单独绑定事件,大幅减少了内存占用与CPU消耗。

2. 长文本与图片优化

对于AI返回的超长文本,我们可以添加虚拟滚动(通过vue-virtual-scroller实现),避免一次性渲染大量DOM;对于图片内容,添加懒加载与点击放大功能:

// 图片懒加载(结合IntersectionObserver)
export const initImageLazyLoad = () => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target
        img.src = img.dataset.src
        observer.unobserve(img)
      }
    })
  })

  document.querySelectorAll('img[data-src]').forEach((img) => {
    observer.observe(img)
  })
}

在Markdown解析时,将图片的src替换为data-src,即可实现懒加载:

// 重写图片渲染规则
md.renderer.rules.image = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  const src = token.attrGet('src')
  const alt = token.content
  // 替换为懒加载格式
  return `<img data-src="${src}" alt="${alt}" class="md-image" loading="lazy">`
}

(三)Vue组件层:响应式数据与生命周期管理

最终我们将解析层与交互层的能力封装为Vue组件AiAnswerRenderer.vue,通过组合式API实现数据驱动与生命周期管理:

<template>
  <div 
    class="ai-answer-container"
    v-html="renderedContent"
    ref="answerRef"
  ></div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { renderMarkdown } from '@/utils/markdownUtil'
import { bindCopyEvent, observeDomChange, initImageLazyLoad } from '@/utils/interactiveUtil'
import 'element-plus/es/components/message/style/css'

// 接收父组件传入的AI原始内容
const props = defineProps({
  content: {
    type: String,
    required: true,
    default: ''
  },
  theme: {
    type: String,
    default: 'default' // 支持主题切换
  }
})

const answerRef = ref(null)
let domObserver = null

// 缓存解析结果,仅当content变化时重新渲染
const renderedContent = computed(() => {
  return renderMarkdown(props.content)
})

// 绑定交互事件
const initInteractive = async () => {
  await nextTick() // 等待DOM渲染完成
  bindCopyEvent()
  initImageLazyLoad()
}

// 监听内容变化,重新初始化交互
watch(() => props.content, () => {
  initInteractive()
}, { flush: 'post' }) // DOM更新后执行

// 监听主题变化,切换样式
watch(() => props.theme, (newTheme) => {
  answerRef.value?.classList.remove('theme-dark', 'theme-light')
  answerRef.value?.classList.add(`theme-${newTheme}`)
})

onMounted(() => {
  initInteractive()
  // 启动DOM变化监听
  domObserver = observeDomChange()
})

onUnmounted(() => {
  // 销毁监听器,避免内存泄漏
  domObserver?.disconnect()
  document.removeEventListener('click', handleCodeCopy)
})
</script>

<style scoped>
.ai-answer-container {
  line-height: 1.6;
  font-size: 14px;
  color: #333;
  padding: 16px;
  border-radius: 8px;
  background-color: #fff;
}

/* 主题样式 */
.theme-dark {
  background-color: #1f2937;
  color: #f3f4f6;
}
.theme-dark .code-block {
  background-color: #2d3748;
}
</style>

组件中computed属性的使用,确保了只有当content真正变化时才会重新执行Markdown解析,避免了重复计算;watch的flush:post选项则保证了DOM更新完成后再执行交互初始化,解决了动态内容的时序问题。

五、性能优化:从解析到渲染的全链路提速

为了让组件在大体积内容、高频更新场景下依然流畅,我们需要从多个维度进行性能优化:

  1. 解析结果缓存:对相同的Markdown内容,缓存其解析后的HTML字符串,避免重复解析。可通过Map实现缓存:

    const renderCache = new Map()
    export const renderMarkdown = (content) => {
      if (!content) return ''
      if (renderCache.has(content)) {
        return renderCache.get(content)
      }
      const result = md.render(content)
      renderCache.set(content, result)
      // 缓存清理:超过100条时清空最早的缓存
      if (renderCache.size > 100) {
        const firstKey = renderCache.keys().next().value
        renderCache.delete(firstKey)
      }
      return result
    }
  2. Prism.js按需加载:默认加载全量语言包会增加体积,通过动态import实现语言包按需加载:

    // 动态加载Prism语言包
    const loadPrismLang = async (lang) => {
      if (prism.languages[lang]) return
      try {
        await import(prismjs/components/prism-${lang}.min.js)
      } catch (e) {
        console.warn(未找到${lang}对应的语法高亮包)
      }
    }
  3. 虚拟滚动适配长文本:当AI答案超过一定长度时,启用虚拟滚动,只渲染可视区域内的内容,减少DOM节点数量:

  4. 减少重绘重排:对代码块、表格等元素的样式修改,优先使用CSS类名切换而非直接操作style属性;避免在滚动、输入等高频事件中执行DOM操作。

六、场景扩展:适配更多AI内容类型

除了基础的Markdown内容,我们还可以为组件扩展更多AI专属的内容渲染能力:

  1. 公式渲染:集成katex或mathjax,实现AI返回的LaTeX公式渲染,满足学术类AI问答需求。
  2. 流程图/时序图:通过mermaid插件,解析Markdown中的流程图语法,将AI生成的流程描述转化为可视化图表。
  3. 多语言支持:为组件添加国际化配置,支持不同语言的交互提示(如“复制成功”的多语言文案)。
  4. 内容审核:对接内容安全接口,在渲染前过滤敏感信息,保障应用合规性。

七、总结与展望

通过分层架构设计与精细化的功能实现,我们构建的AI答案美化组件不仅解决了传统文本展示的体验痛点,还具备高性能、可扩展、可定制的特性。它既可以作为独立组件集成到各类AI应用中,也能通过扩展插件适配不同行业的专属需求。

未来,随着AI生成内容的形式愈发多样(如3D模型描述、交互式图表指令),组件还可进一步集成多模态渲染能力,实现“文本+视觉+交互”的一体化展示,为用户带来更沉浸式的AI内容体验。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/ren-gong-zhi-neng/4613

相关文章

  • AI原生数据库新标杆:seekdb深度解析,轻量架构与混合搜索的双重革命
  • 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环
  • Gemini 3 Pro 深度测评:多模态AI编程的跨代际突破,从一句话到完整应用的全链路革命
  • 6款核心MCP协议工具让AI深度融入业务,告别“纸上谈兵”
  • 揭秘大模型Token的诞生:从字节到子词的分词逻辑与底层算法
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: AI应用 MarkdownIt Vue3.5
最后更新:2025年12月3日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

位卑未敢忘忧国,事定犹须待阖棺。

那年今日(12月17日)

  • 1981年:德国足球运动员蒂姆·维泽出生
  • 1971年:印度和东巴基斯坦达成停火协议
  • 1909年:比利时国王利奥波德二世逝世
  • 1905年:狙击之王西蒙·海耶出生
  • 1902年:京师大学堂正式开学
  • 更多历史事件
最新 热点 随机
最新 热点 随机
AI原生数据库新标杆:seekdb深度解析,轻量架构与混合搜索的双重革命 做了一个WordPress文章热力图插件 Spring WebFlux底层原理深度剖析-从响应式流到事件循环的全链路拆解 Spring WebFlux深度解析:异步非阻塞架构与实战落地指南 规范驱动AI编程:用OpenSpec实现100%可控开发,从需求到代码的全流程闭环 WordPress网站换了个字体,差点儿把样式换崩了
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?使用WireGuard在Ubuntu 24.04系统搭建VPNWordPress实现用户评论等级排行榜插件Gemini 3 Pro 深度测评:多模态AI编程的跨代际突破,从一句话到完整应用的全链路革命WordPress网站换了个字体,差点儿把样式换崩了
使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章 项目中不用 redis 分布式锁,怎么防止用户重复提交? SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports JAVA线程池简析(JDK1.6) IDEA版本2020.*全局MAVEN配置 Gemini 3 深度解析:从像素级复刻到 AGI 雏形,多模态 AI 如何重构开发与创作?
标签聚合
JVM WordPress SQL 日常 K8s 架构 SpringBoot AI编程 MySQL ElasticSearch 多线程 分布式 数据库 AI JAVA docker 设计模式 Spring IDEA Redis
友情链接
  • Blogs·CN
  • Honesty
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3