feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,749 @@
|
||||
# React 组件与 API
|
||||
|
||||
markstream-react 提供与 markstream-vue 相同强大的组件,但专为 React 构建。所有组件都支持 React 18+ 并包含完整的 TypeScript 支持。
|
||||
|
||||
根入口、`markstream-react/next`、`markstream-react/server` 现在都会产出独立声明文件。像 `NodeRendererProps`、`NodeRendererCodeBlockProps`、`NodeComponentProps`、`RenderContext`、`RenderNodeFn`、`CustomComponentMap`、`CodeBlockMonacoOptions`、`MarkdownCodeBlockNodeProps`、`ListItemNodeProps`、`HtmlPreviewFrameProps`、`TooltipProps`、`TooltipPlacement`、`LinkNodeStyleProps` 这类共享渲染器与组件类型,都可以直接从你实际使用的入口导入。
|
||||
## 主组件:MarkdownRender
|
||||
|
||||
在 React 中渲染 markdown 内容的主要组件。
|
||||
|
||||
### Props
|
||||
|
||||
`MarkdownRender` 使用 `markstream-react` 的 `NodeRendererProps`。
|
||||
|
||||
#### 核心 props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 描述 |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `string` | - | 要渲染的 Markdown 内容 |
|
||||
| `nodes` | `BaseNode[]` | - | 预解析的 AST 节点(通常为 `ParsedNode[]`) |
|
||||
| `customId` | `string` | - | 作用域标识,用于 `setCustomComponents` 与样式隔离 |
|
||||
| `final` | `boolean` | `false` | 标记输入结束,停止输出 streaming `loading` 节点 |
|
||||
| `parseOptions` | `ParseOptions` | - | 解析选项与 token hooks(仅在传入 `content` 时生效) |
|
||||
| `customHtmlTags` | `readonly string[]` | - | 作为自定义节点输出的 HTML-like 标签(如 `thinking`) |
|
||||
| `customMarkdownIt` | `(md: MarkdownIt) => MarkdownIt` | - | 自定义 MarkdownIt 实例 |
|
||||
| `debugPerformance` | `boolean` | `false` | 输出解析/渲染耗时与虚拟化统计(仅 dev) |
|
||||
| `isDark` | `boolean` | `false` | 暗色主题标记,转发给重型节点并在根容器加 `.dark` |
|
||||
| `indexKey` | `number \| string` | - | 列表渲染时的 key 前缀 |
|
||||
| `typewriter` | `boolean` | `true` | 非代码节点进入动画 |
|
||||
| `showTooltips` | `boolean` | `true` | 全局控制 `LinkNode` 与代码块节点 tooltip |
|
||||
|
||||
#### 流式与重节点开关
|
||||
|
||||
| 属性 | 默认值 | 描述 |
|
||||
|------|---------|-------------|
|
||||
| `renderCodeBlocksAsPre` | `false` | 将 `code_block` 渲染为 `<pre><code>`(Mermaid/D2/Infographic 也会随之回退) |
|
||||
| `codeBlockStream` | `true` | 随内容到达流式更新代码块 |
|
||||
| `viewportPriority` | `true` | 将 Monaco/Mermaid/D2/KaTeX 等重型工作延迟到接近视口时 |
|
||||
| `deferNodesUntilVisible` | `true` | 重型节点先占位,接近可视区再渲染(仅非虚拟化模式) |
|
||||
|
||||
#### 性能(虚拟化与批次渲染)
|
||||
|
||||
| 属性 | 默认值 | 描述 |
|
||||
|------|---------|-------------|
|
||||
| `maxLiveNodes` | `320` | DOM 最大保留节点数(设为 `0` 关闭虚拟化) |
|
||||
| `liveNodeBuffer` | `60` | 视窗前后 overscan 缓冲 |
|
||||
| `batchRendering` | `true` | 在关闭虚拟化时启用批次渲染 |
|
||||
| `initialRenderBatchSize` | `40` | 批次渲染前先渲染的节点数量 |
|
||||
| `renderBatchSize` | `80` | 每个批次渲染的节点数量 |
|
||||
| `renderBatchDelay` | `16` | 每次批次前的额外延迟(ms) |
|
||||
| `renderBatchBudgetMs` | `6` | 自适应批次缩小前的预算(ms) |
|
||||
| `renderBatchIdleTimeoutMs` | `120` | `requestIdleCallback` 超时(ms) |
|
||||
|
||||
#### 代码块全局配置
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|------|------|-------------|
|
||||
| `codeBlockDarkTheme` | `CodeBlockMonacoTheme` | 转发到每个 `CodeBlockNode` 的 Monaco 深色主题 |
|
||||
| `codeBlockLightTheme` | `CodeBlockMonacoTheme` | 转发到每个 `CodeBlockNode` 的 Monaco 浅色主题 |
|
||||
| `codeBlockMonacoOptions` | `CodeBlockMonacoOptions` | 转发到 `stream-monaco` 的选项,包括 `diffHunkActionsOnHover`、`diffHunkHoverHideDelayMs`、`onDiffHunkAction` 这类 diff 悬浮操作配置 |
|
||||
| `codeBlockMinWidth` | `string \| number` | 转发到 `CodeBlockNode` 的最小宽度 |
|
||||
| `codeBlockMaxWidth` | `string \| number` | 转发到 `CodeBlockNode` 的最大宽度 |
|
||||
| `codeBlockProps` | `NodeRendererCodeBlockProps` | 额外转发到每个代码块渲染器(`CodeBlockNode` / `MarkdownCodeBlockNode`)的 props |
|
||||
| `mermaidProps` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 Mermaid 围栏和自定义 `mermaid` 渲染器的 props |
|
||||
| `d2Props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 D2 围栏和自定义 `d2` 渲染器的 props |
|
||||
| `infographicProps` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | 额外转发到 infographic 围栏和自定义 `infographic` 渲染器的 props |
|
||||
| `themes` | `CodeBlockMonacoTheme[]` | 转发到 `stream-monaco` 的主题列表 |
|
||||
|
||||
#### 重型渲染器 props 透传
|
||||
|
||||
`NodeRenderer` 可以直接给重型图表节点传调优参数:
|
||||
|
||||
```tsx
|
||||
<MarkdownRender
|
||||
content={markdown}
|
||||
viewportPriority
|
||||
mermaidProps={{
|
||||
showHeader: false,
|
||||
renderDebounceMs: 180,
|
||||
previewPollDelayMs: 500,
|
||||
}}
|
||||
d2Props={{ progressiveIntervalMs: 500 }}
|
||||
infographicProps={{ showHeader: false }}
|
||||
/>
|
||||
```
|
||||
|
||||
流式建议:
|
||||
- 保持 `viewportPriority` 开启,避免离屏 Mermaid / Monaco / D2 在文字仍在流式更新时继续做后台工作。
|
||||
- 高频 SSE 更推荐直接传 `nodes`,而不是每个 chunk 都重跑整篇 `content` 解析。
|
||||
- Mermaid 常用调优项包括:`renderDebounceMs`、`contentStableDelayMs`、`previewPollDelayMs`、`previewPollMaxDelayMs`、`previewPollMaxAttempts`。
|
||||
|
||||
`NodeRendererCodeBlockProps` 会跟随公开的 `CodeBlockNode` props 结构(去掉 `node`),所以像 `showHeader`、`showFontSizeButtons`、`showTooltips` 这类字段都能直接获得补全,而不需要退回 `any`。
|
||||
|
||||
```tsx
|
||||
import type { NodeRendererCodeBlockProps } from 'markstream-react'
|
||||
|
||||
const codeBlockProps: NodeRendererCodeBlockProps = {
|
||||
showHeader: false,
|
||||
showFontSizeButtons: false,
|
||||
showTooltips: false,
|
||||
}
|
||||
```
|
||||
|
||||
#### 事件
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|------|------|-------------|
|
||||
| `onCopy` | `(code: string) => void` | 代码块复制事件 |
|
||||
| `onHandleArtifactClick` | `(payload: any) => void` | 预览/制品点击事件 |
|
||||
| `onClick` | `(event: React.MouseEvent<HTMLDivElement>) => void` | 根容器点击事件 |
|
||||
| `onMouseOver` | `(event: React.MouseEvent<HTMLElement>) => void` | 根容器鼠标悬停事件 |
|
||||
| `onMouseOut` | `(event: React.MouseEvent<HTMLElement>) => void` | 根容器鼠标移出事件 |
|
||||
|
||||
### 使用
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const markdown = `# Hello React!
|
||||
|
||||
这是 markstream-react。`
|
||||
|
||||
return (
|
||||
<MarkdownRender
|
||||
customId="docs"
|
||||
content={markdown}
|
||||
maxLiveNodes={150}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 代码块组件
|
||||
|
||||
### MarkdownCodeBlockNode
|
||||
|
||||
使用 Shiki 的轻量级代码高亮。
|
||||
|
||||
```tsx
|
||||
import { MarkdownCodeBlockNode } from 'markstream-react'
|
||||
|
||||
function CodeBlock() {
|
||||
const codeNode = {
|
||||
type: 'code_block',
|
||||
language: 'javascript',
|
||||
code: 'const hello = "world"',
|
||||
raw: 'const hello = "world"'
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
alert('代码已复制!')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MarkdownCodeBlockNode
|
||||
node={codeNode}
|
||||
showCopyButton={true}
|
||||
showCollapseButton={false}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### CodeBlockNode
|
||||
|
||||
功能丰富的 Monaco 驱动代码块。
|
||||
|
||||
```tsx
|
||||
import { CodeBlockNode } from 'markstream-react'
|
||||
|
||||
function MonacoCodeBlock() {
|
||||
const codeNode = {
|
||||
type: 'code_block',
|
||||
language: 'typescript',
|
||||
code: 'const greeting: string = "Hello"',
|
||||
raw: 'const greeting: string = "Hello"'
|
||||
}
|
||||
|
||||
const handleCopy = (code: string) => {
|
||||
console.log('代码已复制:', code)
|
||||
}
|
||||
|
||||
const handlePreviewCode = (artifact: any) => {
|
||||
console.log('预览代码:', artifact)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<CodeBlockNode
|
||||
node={codeNode}
|
||||
monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
|
||||
stream={true}
|
||||
showCollapseButton={false}
|
||||
onCopy={handleCopy}
|
||||
onPreviewCode={handlePreviewCode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 数学组件
|
||||
|
||||
### MathBlockNode
|
||||
|
||||
使用 KaTeX 渲染块级数学公式。
|
||||
|
||||
```tsx
|
||||
import { MathBlockNode } from 'markstream-react'
|
||||
|
||||
function MathBlock() {
|
||||
const mathNode = {
|
||||
type: 'math_block',
|
||||
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
||||
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MathBlockNode node={mathNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### MathInlineNode
|
||||
|
||||
渲染行内数学公式。
|
||||
|
||||
```tsx
|
||||
import { MathInlineNode } from 'markstream-react'
|
||||
|
||||
function MathInline() {
|
||||
const inlineMathNode = {
|
||||
type: 'math_inline',
|
||||
content: 'E = mc^2',
|
||||
raw: 'E = mc^2'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<p>
|
||||
公式如下:
|
||||
{' '}
|
||||
<MathInlineNode node={inlineMathNode} />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Mermaid 图表
|
||||
|
||||
### MermaidBlockNode
|
||||
|
||||
渐进式 Mermaid 图表渲染。
|
||||
|
||||
#### 快速要点
|
||||
- **Tooltip 控制**:`showTooltips` 默认 `true`;设为 `false` 可关闭头部按钮 tooltip。
|
||||
|
||||
```tsx
|
||||
import { MermaidBlockNode } from 'markstream-react'
|
||||
|
||||
function MermaidDiagram() {
|
||||
const mermaidNode = {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: `graph TD
|
||||
A[开始] --> B{能用吗?}
|
||||
B -->|是| C[太好了!]`,
|
||||
raw: ''
|
||||
}
|
||||
|
||||
const handleExport = (ev: any) => {
|
||||
console.log('Mermaid SVG:', ev.svgString)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<MermaidBlockNode
|
||||
node={mermaidNode}
|
||||
isStrict={true}
|
||||
onExport={handleExport}
|
||||
showTooltips={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
事件说明:
|
||||
- `onCopy(code: string)` 直接收到源码字符串(React 版本没有 `MermaidBlockEvent` 包装)。
|
||||
- `onExport` / `onOpenModal` / `onToggleMode` 接收 `MermaidBlockEvent`,可用 `ev.preventDefault()` 阻止默认行为。
|
||||
- `onToggleMode` 签名:`(target: 'source' | 'preview', ev)`。
|
||||
- 如果是在 `MarkdownRender` / `NodeRenderer` 中统一使用,优先通过 `mermaidProps` 传这些参数。
|
||||
|
||||
## D2 图表
|
||||
|
||||
### D2BlockNode
|
||||
|
||||
渐进式 D2 图表渲染,失败时保留上次成功的预览。
|
||||
|
||||
```tsx
|
||||
import { D2BlockNode } from 'markstream-react'
|
||||
|
||||
function D2Diagram() {
|
||||
const d2Node = {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: `direction: right
|
||||
Client -> API: request
|
||||
API -> DB: query`,
|
||||
raw: ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markstream-react">
|
||||
<D2BlockNode
|
||||
node={d2Node}
|
||||
progressiveIntervalMs={600}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 其他节点组件
|
||||
|
||||
### HeadingNode
|
||||
|
||||
```tsx
|
||||
import { HeadingNode } from 'markstream-react'
|
||||
|
||||
function CustomHeading() {
|
||||
const headingNode = {
|
||||
type: 'heading',
|
||||
level: 1
|
||||
}
|
||||
|
||||
return <HeadingNode node={headingNode}>Hello World</HeadingNode>
|
||||
}
|
||||
```
|
||||
|
||||
### ParagraphNode
|
||||
|
||||
```tsx
|
||||
import { ParagraphNode } from 'markstream-react'
|
||||
|
||||
function CustomParagraph() {
|
||||
const paragraphNode = {
|
||||
type: 'paragraph'
|
||||
}
|
||||
|
||||
return (
|
||||
<ParagraphNode node={paragraphNode}>
|
||||
这是一个
|
||||
{' '}
|
||||
<strong>粗体</strong>
|
||||
{' '}
|
||||
单词。
|
||||
</ParagraphNode>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### ListNode
|
||||
|
||||
```tsx
|
||||
import { ListNode, renderNode } from 'markstream-react'
|
||||
|
||||
function CustomList() {
|
||||
const listNode = {
|
||||
type: 'list',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
children: [
|
||||
{ type: 'paragraph', children: [{ type: 'text', content: '项目 1' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list_item',
|
||||
children: [
|
||||
{ type: 'paragraph', children: [{ type: 'text', content: '项目 2' }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const ctx = { events: {} }
|
||||
|
||||
return <ListNode node={listNode} ctx={ctx} renderNode={renderNode} />
|
||||
}
|
||||
```
|
||||
|
||||
### LinkNode
|
||||
|
||||
```tsx
|
||||
import { LinkNode } from 'markstream-react'
|
||||
|
||||
function CustomLink() {
|
||||
const linkNode = {
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
title: '示例',
|
||||
text: '点击我'
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkNode
|
||||
node={linkNode}
|
||||
color="#e11d48"
|
||||
underlineHeight={3}
|
||||
showTooltip={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### ImageNode
|
||||
|
||||
```tsx
|
||||
import { ImageNode } from 'markstream-react'
|
||||
|
||||
function CustomImage() {
|
||||
const imageNode = {
|
||||
type: 'image',
|
||||
src: 'https://example.com/image.jpg',
|
||||
alt: '示例图片',
|
||||
title: '示例',
|
||||
raw: ''
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('图片被点击!')
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
console.log('图片已加载!')
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageNode
|
||||
node={imageNode}
|
||||
onClick={handleClick}
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 工具函数
|
||||
|
||||
### getMarkdown
|
||||
|
||||
获取配置好的 markdown-it 实例。
|
||||
|
||||
```tsx
|
||||
import { getMarkdown } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown('my-msg-id', {
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
})
|
||||
|
||||
const tokens = md.parse('# Hello World')
|
||||
```
|
||||
|
||||
### parseMarkdownToStructure
|
||||
|
||||
将 markdown 字符串解析为 AST 结构。
|
||||
|
||||
```tsx
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
|
||||
const md = getMarkdown()
|
||||
const nodes = parseMarkdownToStructure('# 标题\n\n这里的内容...', md)
|
||||
|
||||
// 与 MarkdownRender 一起使用
|
||||
// <MarkdownRender nodes={nodes} />
|
||||
```
|
||||
|
||||
### 可选 Worker(Mermaid / KaTeX)
|
||||
|
||||
安装后会自动加载 Mermaid/KaTeX。如需线程外解析/渲染,可注入 Worker:
|
||||
|
||||
```tsx
|
||||
import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
|
||||
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
|
||||
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'
|
||||
|
||||
setMermaidWorker(new MermaidWorker())
|
||||
setKaTeXWorker(new KatexWorker())
|
||||
```
|
||||
|
||||
## 自定义组件 API
|
||||
|
||||
### Props 接口
|
||||
|
||||
所有自定义节点组件都接收这些 props:
|
||||
|
||||
```tsx
|
||||
import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'
|
||||
|
||||
interface NodeComponentProps<TNode = unknown> {
|
||||
node: TNode // 解析后的节点数据
|
||||
ctx?: RenderContext // 渲染上下文(主题、事件、开关)
|
||||
renderNode?: RenderNodeFn // 子节点渲染助手
|
||||
indexKey?: React.Key // 节点的唯一键
|
||||
customId?: string // 作用域标识符
|
||||
isDark?: boolean
|
||||
typewriter?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
`CustomComponentMap` 就是 `setCustomComponents(...)` 接收的映射类型。
|
||||
|
||||
### 示例自定义组件
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
interface CustomParagraphProps {
|
||||
node: {
|
||||
type: string
|
||||
children: Array<{
|
||||
type: string
|
||||
content?: string
|
||||
children?: any[]
|
||||
}>
|
||||
}
|
||||
indexKey?: number | string
|
||||
customId?: string
|
||||
}
|
||||
|
||||
function CustomParagraph({ node, indexKey, customId }: CustomParagraphProps) {
|
||||
return (
|
||||
<p
|
||||
className={`custom-paragraph custom-paragraph-${indexKey}`}
|
||||
data-custom-id={customId}
|
||||
data-node-type={node.type}
|
||||
>
|
||||
{node.children.map((child, i) => (
|
||||
<span key={i}>{child.content || ''}</span>
|
||||
))}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用
|
||||
function App() {
|
||||
const paragraphNode = {
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'text', content: '自定义段落内容' }
|
||||
]
|
||||
}
|
||||
|
||||
return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
|
||||
}
|
||||
```
|
||||
|
||||
## Context + 自定义组件
|
||||
|
||||
可以在自定义节点组件内部使用 React Context,同时通过 `setCustomComponents` 注册组件:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const ThemeContext = createContext<'light' | 'dark'>('light')
|
||||
|
||||
function CustomHeading({ node, customId }: any) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const level = node.level || 1
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Tag className={`custom-heading ${theme}`} data-custom-id={customId}>
|
||||
{node.children?.map((child: any, i: number) => (
|
||||
<span key={i}>{child.content || ''}</span>
|
||||
))}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
setCustomComponents('docs', { heading: CustomHeading })
|
||||
|
||||
function App() {
|
||||
const markdown = `# 自定义标题
|
||||
|
||||
这使用了自定义标题组件。
|
||||
`
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value="dark">
|
||||
<MarkdownRender customId="docs" content={markdown} />
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 流式传输支持
|
||||
|
||||
markstream-react 支持流式 markdown 内容:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function StreamingDemo() {
|
||||
const [content, setContent] = useState('')
|
||||
const fullContent = `# 流式传输演示
|
||||
|
||||
此内容正在**逐步**流式传输。
|
||||
`
|
||||
|
||||
useEffect(() => {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i < fullContent.length) {
|
||||
setContent(prev => prev + fullContent[i])
|
||||
i++
|
||||
}
|
||||
else {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 30)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <MarkdownRender content={content} />
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript 支持
|
||||
|
||||
markstream-react 包含完整的 TypeScript 类型定义:
|
||||
|
||||
```tsx
|
||||
import type { NodeComponentProps, NodeRendererProps } from 'markstream-react'
|
||||
import type { ParsedNode } from 'stream-markdown-parser'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
|
||||
function App() {
|
||||
const markdown = '# Hello TypeScript!'
|
||||
const nodes: ParsedNode[] = []
|
||||
|
||||
return <MarkdownRender content={markdown} nodes={nodes} />
|
||||
}
|
||||
```
|
||||
|
||||
代码块相关的 props 类型(`CodeBlockNodeProps` / `MermaidBlockNodeProps` / `D2BlockNodeProps` / `InfographicBlockNodeProps` / `PreCodeNodeProps`)统一使用 `stream-markdown-parser` 的 `CodeBlockNode`(用 `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` 区分渲染器)。
|
||||
|
||||
## Next.js 最佳实践
|
||||
|
||||
优先使用专门的 Next SSR 双入口,而不是 `mounted` guard 或 `ssr: false`。
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/next'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello Next.js!" final />
|
||||
}
|
||||
```
|
||||
|
||||
如果你需要纯服务端渲染路径:
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react/server'
|
||||
|
||||
export default function MarkdownPage() {
|
||||
return <MarkdownRender content="# Hello!" final />
|
||||
}
|
||||
```
|
||||
|
||||
完整入口模型见 [React Next SSR](/zh/guide/react-next-ssr)。
|
||||
|
||||
## Hooks 集成
|
||||
|
||||
你可以轻松地与 React hooks 集成:
|
||||
|
||||
```tsx
|
||||
import type { ChangeEvent } from 'react'
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
function MarkdownEditor() {
|
||||
const [content, setContent] = useState('# 编辑我!')
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
const memoizedContent = useMemo(() => content, [content])
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea value={content} onChange={handleChange} />
|
||||
<MarkdownRender
|
||||
content={memoizedContent}
|
||||
customId={`editor-${theme}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```tsx
|
||||
import MarkdownRender from 'markstream-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function SafeMarkdown({ content }: { content: string }) {
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
渲染 markdown 时出错:
|
||||
{error.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return <MarkdownRender content={content} />
|
||||
}
|
||||
catch (err) {
|
||||
setError(err as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看 [React 快速开始](/zh/guide/react-quick-start) 获取设置示例
|
||||
- 探索 [Vue 3 组件](/zh/guide/components) 获取更多组件示例(API 类似)
|
||||
- 查看 [使用与 API](/zh/guide/usage) 获取高级模式
|
||||
Reference in New Issue
Block a user