70行代码实现一个Markdown流式解析器

1,114 阅读7分钟

从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 规范关键插件备注
remarkMarkdownmdastremark-parse / remark-stringify / remark-gfm / remark-frontmatter / remark-math …社区最活跃,工具最多
rehypeHTMLhastrehype-parse / rehype-stringify / rehype-slug / rehype-autolink-headings / rehype-pretty-code …常与 remark 组合做“MD→HTML”
retext自然语言nlcstretext-english / retext-smartypants / retext-spell / retext-readability …拼写检查、可读性评分等
redotGraphviz Dotdostredot-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.createElementh() 等繁琐代码。

核心 API —— toJsxRuntime(tree, options)
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'

参数

  1. tree:hast 根节点

  2. options:必填,至少提供 JSX 运行时三件套

    • jsx / jsxs:生产环境自动运行时函数
    • Fragment:片段构造器
    • (开发模式再额外给 jsxDEV

返回值
直接就是框架的“虚拟节点”,可扔进框架的渲染层即可。

技术亮点与设计精髓

  1. 响应式设计 : 利用 Vue3 的 computed ,实现了 Markdown 字符串变化时的自动重新解析和渲染
  2. 模块化插件链 : 采用统一的插件系统,各功能模块解耦,可以灵活地添加或移除功能
  3. 高性能优化 : 通过 computed 缓存解析器和虚拟 DOM,避免不必要的重复计算
  4. 丰富的功能支持 : 支持数学公式、代码高亮、GitHub 风格扩展等高级功能
  5. 错误处理机制 : 提供了 errorCaptured 钩子,捕获并记录解析过程中的错误
  6. 兼容处理了大模型流式输出的时候图片链接、表格字符串、数学公式等未完全返回的的渲染抖动

总结

这个 Vue3 Markdown 解析组件就像是一个「智能翻译官 + 高级排版师」,它不仅能准确地将 Markdown 转换成 HTML,还能让最终的展示效果既美观又功能丰富。通过巧妙地组合各种开源工具,它实现了一个功能完备、性能优良的 Markdown 解析渲染系统。

无论是构建博客、文档系统还是知识库,这个组件都能为你的项目增添强大的内容展示能力。希望这篇文章能帮助你理解这个组件的实现原理,也欢迎大家提出宝贵的改进建议!

最后,如果你觉得这个组件对你有帮助,不妨点个赞并分享给更多的开发者朋友,让我们一起让 Markdown 解析变得更简单、更强大!

GitHub源码仓库地址 如果觉得好用,欢迎给个Star ⭐️ 支持一下!