@markdown-stream/vue3
v0.1.28
Published
[@markdown-stream/core](../core) 的 Vue 3 组件层。
Readme
@markdown-stream/vue3
@markdown-stream/core 的 Vue 3 组件层。
安装
npm install @markdown-stream/vue3 @markdown-stream/core vue引入样式(推荐):
import '@markdown-stream/vue3/style.css'Props 参考
| Prop | 类型 | 说明 |
|------|------|------|
| content | string \| AsyncIterable<string> | 统一入口:字符串一次性渲染,AsyncIterable 流式渲染(推荐) |
| components | CustomTokenDefinition[] \| Partial<MarkdownTokenComponentMap> | 自定义 token 定义或渲染组件映射 |
| tokenTypes | TokenTypeDefinition[] | 传给核心库的自定义 token 类型 |
| cursor | boolean | 流式输出时显示打字光标,默认 false |
| debug | boolean | 在 console 打印 token 状态变化,默认 false |
快速上手:统一 content prop
无需区分数据来源,直接传 content——字符串一次性渲染,AsyncIterable 流式渲染:
<script setup lang="ts">
import { MarkdownStream } from '@markdown-stream/vue3'
// 一次性字符串
const markdownString = '# Hello\n\nThis is **markdown**.'
// 或流式 AsyncIterable(来自 AI / SSE 等)
async function* aiStream() {
yield '# Title\n\n'
yield 'Streaming content...'
}
</script>
<template>
<!-- 传字符串 -->
<MarkdownStream :content="markdownString" />
<!-- 传 AsyncIterable -->
<MarkdownStream :content="aiStream()" />
</template>自定义 Token 与组件
通过 components 数组同时定义「如何解析」和「如何渲染」,组件直接传入
interface CustomTokenDefinition {
name: string // token 类型名
component?: Component // 渲染组件,token.state 在组件内部自行处理
openRegex?: string | RegExp // 有此字段时自动注册解析规则
closeRegex?: string | RegExp // 配合 openRegex 匹配 open/close token 对
contentRegex?: RegExp // 匹配行内文本片段,捕获组内容作为 token.content 传入组件
}覆盖已有 token 的渲染
只传 name + component,不改变解析逻辑:
<template>
<MarkdownStream
:content="markdown"
:components="[
{ name: 'fence', component: MyCodeBlock },
{ name: 'heading', component: MyHeading },
]"
/>
</template>Fence 模式:匹配代码围栏的语言标识符
添加 openRegex,匹配 ``` 后的 info 字段:
```warning
这是一段警告内容。
```<template>
<MarkdownStream
:content="markdown"
:components="[
{ name: 'warning', openRegex: /^warning$/, component: WarningBlock },
]"
/>
</template>WarningBlock.vue 通过 token.content 获取围栏内文本:
<script setup lang="ts">
import type { StatefulToken } from '@markdown-stream/core'
defineProps<{ token: StatefulToken }>()
</script>
<template>
<aside class="warning" :data-state="token.state">{{ token.content }}</aside>
</template>JSON 模式:解析 JSON 并展示图片
添加 openRegex: /^json$/,匹配 ```json 代码围栏,组件内解析 JSON 并展示图片:
```json
{
"img": "//example.com/image.jpg",
"name": "示例图片"
}
```<script setup lang="ts">
import { computed } from 'vue'
import type { StatefulToken } from '@markdown-stream/core'
const props = defineProps<{ token: StatefulToken }>()
const parsedJson = computed(() => {
try {
return JSON.parse(props.token.content || '')
} catch { return null }
})
const imgUrl = computed(() => {
if (!parsedJson.value) return null
const url = parsedJson.value.img || parsedJson.value.image || null
// 处理 // 开头的相对协议 URL
return url?.startsWith('//') ? 'https:' + url : url
})
</script>
<template>
<div class="json-block">
<!-- 图片预览 -->
<div v-if="imgUrl" class="json-image">
<img :src="imgUrl" :alt="parsedJson?.name" />
</div>
<!-- JSON 内容 -->
<pre v-if="parsedJson"><code>{{ JSON.stringify(parsedJson, null, 2) }}</code></pre>
<!-- 加载中 -->
<div v-else class="json-loading">解析中...</div>
</div>
</template>使用方式:
<template>
<MarkdownStream
:content="markdown"
:components="[
{ name: 'json', openRegex: /^json$/, component: JsonBlock },
]"
/>
</template>内联 Token 拦截
除了代码块等块级 token,你还可以拦截行内(inline)语法,替换默认渲染或定义全新的行内样式。
覆盖已有内联 Token
直接用 name 指向已有内联 token(如 strong、em、code_inline),传入自定义 component,不改变任何解析逻辑:
| 内置名称 | 对应 Markdown 语法 |
|---|---|
| strong | **加粗** 或 __加粗__ |
| em | *斜体* 或 _斜体_ |
| code_inline | `行内代码` |
<template>
<MarkdownStream
:content="markdown"
:components="[
{ name: 'strong', component: MyHighlight },
]"
/>
</template>自定义组件通过 token.children[0].content 取得被包裹的文本:
<!-- MyHighlight.vue -->
<script setup lang="ts">
import type { StatefulToken } from '@markdown-stream/core'
defineProps<{ token: StatefulToken }>()
</script>
<template>
<span style="background:#fef08a; padding:2px 4px; border-radius:3px; color:#854d0e;">
{{ token.children?.[0]?.content }}
</span>
</template>效果:**重要文字** 将渲染为黄色高亮而不是加粗。
contentRegex:自定义行内语法
使用 contentRegex 可以匹配任意行内文本片段,无需改动 Markdown 解析器。组件通过 token.content 获取正则捕获后的内容。
示例:将 pink文字pink 渲染为粉色高亮
Markdown 原文:
这段话里有一个 pink重要提示pink 需要特别关注。注册配置:
<template>
<MarkdownStream
:content="markdown"
:components="[
{
name: 'pink_highlight',
contentRegex: /^pink(.+)pink$/,
component: PinkHighlight,
},
]"
/>
</template>PinkHighlight.vue 直接读取 token.content(已提取捕获组内容):
<!-- PinkHighlight.vue -->
<script setup lang="ts">
import type { StatefulToken } from '@markdown-stream/core'
defineProps<{ token: StatefulToken }>()
</script>
<template>
<span style="background:#fce7f3; padding:2px 4px; border-radius:3px; color:#be185d; font-weight:500;">
{{ token.content }}
</span>
</template>同时使用多种内联拦截
<template>
<MarkdownStream
:content="markdown"
:components="[
{ name: 'strong', component: YellowHighlight },
{ name: 'em', component: BlueItalic },
{ name: 'code_inline', component: StyledCode },
{ name: 'pink_highlight', contentRegex: /^pink(.+)pink$/, component: PinkHighlight },
]"
/>
</template>注意:
contentRegex匹配 inline 内容时,会将所有文本节点拼接后整体匹配。正则捕获组(第一个括号)的内容会作为token.content传给组件。即使内容中包含 URL(被 linkify 自动转换为链接),匹配仍然正常工作。
内置组件
SfcRendererPending
Vue SFC 生成过程中的骨架屏占位组件,用于 fence token 的 streaming 阶段。
显示一张带波浪动画的卡片,包含头像、标题行、副标题行及横幅区块,视觉上模拟「内容即将出现」的加载状态。可在自定义组件内部根据 token.state 决定是否展示。
<script setup lang="ts">
import type { StatefulToken } from '@markdown-stream/core'
import { SfcRendererPending } from '@markdown-stream/vue3'
defineProps<{ token: StatefulToken }>()
</script>
<template>
<!-- streaming 阶段显示骨架屏,done 阶段渲染实际内容 -->
<SfcRendererPending v-if="token.state === 'streaming'" />
<div v-else>{{ token.content }}</div>
</template>VueSfcFenceRenderer
用于渲染 ```vue 代码围栏的内置组件,适合 AI 流式生成 Vue SFC 后的「完成态」展示。
功能:
- 非
vue语言的围栏退化为普通<pre><code>块。 vue围栏提供 Preview / Code 双标签页:- Preview:将 SFC 源码注入 Blob URL iframe,通过
vue3-sfc-loader在沙箱中即时编译并运行,iframe 高度随内容自适应。 - Code:以等宽字体展示原始源码。
- Preview:将 SFC 源码注入 Blob URL iframe,通过
- 右上角 Copy 按钮,点击后 2 秒内显示复制成功状态。
<script setup lang="ts">
import { MarkdownStream, VueSfcFenceRenderer } from '@markdown-stream/vue3'
</script>
<template>
<MarkdownStream
:content="aiStream()"
:components="[
{ name: 'fence', component: VueSfcFenceRenderer },
]"
/>
</template>沙箱说明:
vue@3、vue3-sfc-loader及@tailwindcss/browser已内联打包至组件内,无需任何外部网络请求。sandbox 属性设置为allow-scripts allow-same-origin allow-modals allow-popups allow-forms。
流式计时字段
所有顶层 block token 的 meta 中会自动注入以下两个计时字段,由内部 diff 阶段写入,无需手动设置:
| 字段 | 类型 | 写入时机 |
|------|------|----------|
| streamStartTime | number(毫秒时间戳) | token 首次出现时记录,后续更新中保持不变 |
| streamDoneTime | number(毫秒时间戳) | token 状态变为 done 时记录,streaming 阶段不存在此字段 |
典型用法——在自定义组件中计算 token 从出现到完成的耗时:
<script setup>
import { computed } from 'vue'
const props = defineProps(['token'])
const elapsed = computed(() => {
const start = props.token.meta?.streamStartTime
const done = props.token.meta?.streamDoneTime ?? Date.now()
return done - start // 毫秒
})
</script>
<template>
<div>
<slot />
<span v-if="token.state === 'done'">生成耗时 {{ elapsed }} ms</span>
</div>
</template>这两个字段不参与内容相等性比较,不会因时间戳变化触发多余的重渲染。
useMarkdownStream 组合式 API
底层组合式函数,适合需要自行控制渲染逻辑的场景。
import { useMarkdownStream } from '@markdown-stream/vue3'
const {
tokens, // Ref<StatefulToken[]> — 当前完整 token 树
isStreaming, // Ref<boolean>
error, // Ref<unknown>
parse, // (markdown: string) => void — 一次性解析
write, // (chunk: string) => void — 追加输入
consume, // (stream: AsyncIterable<string>) => Promise<void>
reset, // () => void
cancel, // () => void — 取消当前流
} = useMarkdownStream({ debug: false })<script setup lang="ts">
import { useMarkdownStream } from '@markdown-stream/vue3'
import type { StatefulToken } from '@markdown-stream/core'
const { tokens, isStreaming, consume } = useMarkdownStream()
async function* aiStream() { /* ... */ }
consume(aiStream())
</script>
<template>
<div v-for="token in tokens" :key="token.id" :data-state="token.state">
<!-- 自定义渲染 -->
</div>
</template>