从0到1构建一个渲染Markdown字符串的组件
- ✨ 流式输出:支持增量渲染 Markdown 内容,适用于大文本或流式数据场景
- 🎯 无闪屏渲染:每次更新内容时保持平滑过渡,提升用户体验
- 🎨 样式友好:内置美观的默认样式(数学公式转换,代码块语法高亮)
- 🚀 高性能:借助虚拟 dom 和 Vue 的diff算法增量渲染
- 🔗 原生Html:支持原生Html标签嵌入到Md字符串内
先上效果 演示demo
背景
相信很多大模型前端开发的小伙伴都已经处理过markdown实时解析翻译成html了,传统的方式类似使用Marked、markdown-it等组件全量渲染。但是全量渲染及其消耗性能,会造成大量的重排、重绘,导致页面抖动。
实现逻辑拆解
1、想要实现渲染Markdown字符串,必然涉及到如何把md字符串转成html;
2、如若支持流式渲染,可以借助虚拟dom和vue的diff算法;
3、处理数学公式展示可以借助unified生态的rehypeKatex、remarkMath;
4、处理代码语法高亮可以借助unified生态的rehypeHighlight;
5、原生Html标签如 <br> <a>等需要放行,不做转化处理;
6、支持提示语法如:::warning ::: error等,需要unified生态的remarkFlexibleContainers;
3、部分碎片md字符串如(输出一半的图片链接、表格、数学公式等)可以使用js正则匹配非法的格式做过滤,直到合法后再做解析渲染;
开箱即用模式
# 安装命令
npm install v3-markdown-stream
# 或
yarn add v3-markdown-stream
组件使用示例
<template>
<div>
<MarkdownRender :markInfo="markdownContent" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { MarkdownRender } from 'v3-markdown-stream';
import 'v3-markdown-stream/dist/v3-markdown-stream.css';
// 静态内容
const markdownContent = ref('# Hello World\n\nThis is a simple markdown example.')
</script>
组件概览
首先,让我们来看看这个组件的核心代码(都是站在各位巨人的肩膀上) 组件使用 Vue3 的 defineComponent 定义,接收一个必须的 markstr 属性,这是要解析的 Markdown 字符串。整个组件的设计非常简洁,就像一个「专注的翻译官」,只做一件事,但要做到极致!
import { h, defineComponent, computed } from "vue";
import { Fragment, jsxs, jsx } from "vue/jsx-runtime";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import remarkParse from "remark-parse";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import 'highlight.js/styles/github-dark.css';
import remarkMath from "remark-math";
import remarkRehype from "remark-rehype";
import rehypeRaw from 'rehype-raw';
import rehypeHighlight from 'rehype-highlight'
import remarkFlexibleContainers from 'remark-flexible-containers'
import remarkGfm from "remark-gfm";
import { VFile } from "vfile";
import { unified } from "unified";
//1、先将接收到的md字符串通过VFile转成file,因为unified需要file类型
const createFile = (markstr) => { // markdown字符串转file
const file = new VFile();
file.value = markstr;
return file;
};
// 2、解析器链的构建
// 这部分代码构建了一个「解析流水线」,就像工厂里的生产线一样,Markdown 文本会依次经过各个「加工环节」。这里使用 computed 确保解析器只在必要时重新创建,提高了性能。
// 使用unified的remarkParse将md字符串转成md语法树;
// 使用unified的remarkRehype将md语法树转成html语法树;
// 使用rehypeRaw放行原生html、rehypeKatex解析数学公式、rehypeHighlight代码语法高亮
let unifiedProcessor = computed(() => { // unified解析器工具链初始化
const processor = unified()
.use(remarkParse, { allowDangerousHtml: true})
.use(remarkFlexibleContainers)
.use(remarkRehype, { allowDangerousHtml: true})
.use(rehypeRaw)
.use(remarkGfm)
.use(rehypeKatex)
.use(remarkMath)
.use(rehypeHighlight);
return processor;
});
//3、使用hast-util-to-jsx-runtime将语法树转为虚拟dom
const generateVueNode = (tree) => { // 获取vue虚拟dom
const vueVnode = toJsxRuntime(tree, {
Fragment,
jsx: jsx,
jsxs: jsxs,
passNode: true,
});
return vueVnode;
};
//4、响应式渲染
// processor.parse(file) 将文件解析成 AST
// processor.runSync(...) 运行所有插件处理 AST
// 最后通过 h() 函数将生成的虚拟 DOM 渲染到页面上
const computedVNode = computed(() => {
const processor = unifiedProcessor.value;
const file = createFile(props.markstr);
let result = generateVueNode(processor.runSync(processor.parse(file), file));
return result;
});
return () => {
return h(computedVNode.value);
};
核心功能包解析
1. Vue 核心团队
- vue : 提供 h , defineComponent , computed 等核心 API,是整个组件的「骨架」
- vue/jsx-runtime : 提供 Fragment , jsxs , jsx ,让我们可以在 Vue 中优雅地使用 JSX 语法。
2. Unified 解析系统
- unified : 这是整个解析系统的「大脑」,负责协调各个插件的工作。想象一下,它就像是一个「指挥官」,指挥着(插件)同步执行;
- vfile : 提供文件处理功能,把 Markdown 字符串转换成统一的文件格式,;
官方四大生态
| 生态名 | 负责语言 | 典型 AST 规范 | 关键插件 | 备注 |
|---|---|---|---|---|
| remark | Markdown | mdast | remark-parse / remark-stringify / remark-gfm / remark-frontmatter / remark-math … | 社区最活跃,工具最多 |
| rehype | HTML | hast | rehype-parse / rehype-stringify / rehype-slug / rehype-autolink-headings / rehype-pretty-code … | 常与 remark 组合做“MD→HTML” |
| retext | 自然语言 | nlcst | retext-english / retext-smartypants / retext-spell / retext-readability … | 拼写检查、可读性评分等 |
| redot | Graphviz Dot | dost | redot-parse / redot-stringify | 小众但官方维护 |
一句话概括:unified 不是“又一个 Markdown 解析器”,而是让“任何结构化文本”都能被统一解析、转换、生成的基础设施;在它之上,remark、rehype、retext、redot 四大生态共同组成了 JavaScript 世界最完善、最活跃的内容处理工具链
3. Remark 家族 - Markdown 处理器
- remark-parse : 将 Markdown 文本解析成抽象语法树(AST),就像是「翻译官」把中文翻译成一种中间语言
- remark-math : 处理数学公式,让你的文档可以「高大上」地展示复杂数学表达式
- remark-rehype : 将 Markdown AST 转换成 HTML AST,相当于「转换器」把中间语言翻译成另一种中间语言
- remark-gfm : 支持 GitHub 风格的 Markdown 扩展功能,比如表格、任务列表等
- remark-flexible-containers : 提供灵活的容器功能,让你的内容布局更加多样化
4. Rehype 家族 - HTML
- rehype-raw : 保留原始 HTML,让你的 Markdown 中混合的 HTML 代码也能正常工作
- rehype-katex : 将数学公式渲染成漂亮的 HTML,让数学表达式样式友好
- rehype-highlight : 为代码块提供语法高亮,提高代码可读性
5. 样式支持
- katex.min.css : 数学公式的样式
- github-dark.css : 代码高亮的样式(按需求可替换成浅色模式)
6.hast-util-to-jsx-runtime介绍
hast-util-to-jsx-runtime 是一个把 hast(HTML AST)转成 JSX 运行时对象 的小工具,一句话:给它一棵 hast 树,它就能吐出 React / Preact / Solid / Vue / Svelte 等框架“认识”的 JSX 节点,无需再手写 React.createElement 或 h() 等繁琐代码。
核心 API —— toJsxRuntime(tree, options)
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
参数
-
tree:hast 根节点 -
options:必填,至少提供 JSX 运行时三件套jsx/jsxs:生产环境自动运行时函数Fragment:片段构造器- (开发模式再额外给
jsxDEV)
返回值
直接就是框架的“虚拟节点”,可扔进框架的渲染层即可。
技术亮点与设计精髓
- 响应式设计 : 利用 Vue3 的 computed ,实现了 Markdown 字符串变化时的自动重新解析和渲染
- 模块化插件链 : 采用统一的插件系统,各功能模块解耦,可以灵活地添加或移除功能
- 高性能优化 : 通过 computed 缓存解析器和虚拟 DOM,避免不必要的重复计算
- 丰富的功能支持 : 支持数学公式、代码高亮、GitHub 风格扩展等高级功能
- 错误处理机制 : 提供了 errorCaptured 钩子,捕获并记录解析过程中的错误
- 兼容处理了大模型流式输出的时候图片链接、表格字符串、数学公式等未完全返回的的渲染抖动
总结
这个 Vue3 Markdown 解析组件就像是一个「智能翻译官 + 高级排版师」,它不仅能准确地将 Markdown 转换成 HTML,还能让最终的展示效果既美观又功能丰富。通过巧妙地组合各种开源工具,它实现了一个功能完备、性能优良的 Markdown 解析渲染系统。
无论是构建博客、文档系统还是知识库,这个组件都能为你的项目增添强大的内容展示能力。希望这篇文章能帮助你理解这个组件的实现原理,也欢迎大家提出宝贵的改进建议!
最后,如果你觉得这个组件对你有帮助,不妨点个赞并分享给更多的开发者朋友,让我们一起让 Markdown 解析变得更简单、更强大!
GitHub源码仓库地址 如果觉得好用,欢迎给个Star ⭐️ 支持一下!