先说一个尴尬场景

团队里的 AI 应用分了两摊:Web 主站用 React,文档和后台用 Vue/Nuxt,还有个老项目是 Vue 2。Chat 界面要做流式 Markdown 渲染,选型时发现:
- react-markdown / Streamdown 只能在 React 里用
- 我的老方案是 Vue 专用的
- Vue 2 的项目几乎没人管
要么写三套渲染逻辑,要么让三个项目"各自美丽",页面行为越来越不像一家人。
破局:一个内核,五层壳
我的做法是把渲染内核和框架层彻底拆开:
- 内核:stream-markdown-parser 负责流式解析------在 markdown-it-ts 基础上,加了对"未闭合 fence、半截表格、写到一半的行内语法"的中间态处理。这一层与框架无关。
- 框架层 :Vue 3 / React / Svelte 5 / Angular / Vue 2 各出一层薄封装,暴露同一个
MarkdownRender组件和同一套 props(content / final / mode...)。
好处很直接:
- 行为一致:同一个 Markdown,五个框架渲染结果完全一致,评审时只看一份逻辑
- 维护集中:解析器的坑只修一次,五个包同步发版
- 团队自由:新项目可以选任何框架,不用为渲染层改变技术选型
2.x 稳定了什么
- 流式代码块拆成独立包 stream-diffs:代码逐 token 到达时用 diff 增量补全,不整块重建
- 低抖动更新:增量渲染 + 批量提交,流的每一帧都是稳定状态
- SSR 安全:Next.js / Nuxt 服务端渲染开箱可用
- 重块渐进渲染:Mermaid 图、KaTeX 公式语法稳定后才升级渲染,支持 Web Worker
一份代码,两个框架
vue
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
defineProps<{ content: string; isDone: boolean }>()
</script>
<template>
<MarkdownRender mode="chat" :content="content" :final="isDone" />
</template>
tsx
import MarkdownRender from 'markstream-react'
import 'markstream-react/index.css'
export default function ChatMessage({ content, isDone }) {
return <MarkdownRender content={content} final={isDone} />
}
数据验证(截至 2026-08-25)
- GitHub 2961 ⭐ / 178 forks
- markstream-vue npm 月下载量 10.7 万
- 五个框架包全部可用,文档含每个框架的安装/SSR 指南
什么时候别用全家桶
诚实说:如果你只是渲染短静态 Markdown,marked / markdown-it 更轻。全家桶是为「Markdown 还在变化时用户就在读」的场景设计的------AI 聊天、LLM token 流、SSE 应用。场景匹配再上,别为了全家桶而全家桶。
我是作者,欢迎吐槽架构取舍。