
Storybook用 Markdown 块与 ?raw 原始导入在 Docs 页面渲染 .md 文件【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook在 Storybook 的 Docs 页面auto-docs 或 MDX 文档中经常会想把项目里的 README、CHANGELOG 等纯.md文件原样嵌入展示而不是把内容复制到 MDX 里手动维护。本文以官方仓库中storybook/addon-docs自带的示例文件 Markdown-content.md 为骨架完整讲解?raw导入的用法与原理、Markdown块的 props 与底层实现基于markdown-to-jsx的代码块/链接/标题覆盖渲染以及为什么不能把.md文件直接导入 MDX 渲染帮助你在自己的项目中正确复用这一文档能力。示例文件本体Markdown-content.md 与它的角色先看 Markdown-content.md 的完整内容全文仅十余行# This is an .md file it has been imported using import content from ./Markdown-content.md?raw Notice the ?raw at the end above, it is necessary to work. A full example: md import { Markdown } from storybook/addon-docs/blocks; import content from ./Markdown-content.md?raw; Markdown{content}/Markdown这个文件本身是一个“自指”示例它的内容就是演示如何导入它自己。它在仓库中并非孤立存在而是被 storybook/addon-docs 官方的 Story 引用——[Markdown.stories.tsx](https://link.gitcode.com/i/224496eafab5cdab33ccd9b449e07051) 第一行 import 就是 ts import mdContent from ../examples/Markdown-content.md?raw;并用于ImportedMDFile这个故事/** * The Markdown component wont know the difference between getting a raw string and something * imported from a .md file. So this story doesnt actually test the component, but rather the * import at the top of the CSF file */ export const ImportedMDFile { name: Imported .md file, args: { children: mdContent }, };注意这段源码注释Markdown.stories.tsx点明了一个关键事实Markdown组件本身并不关心 children 是“手写字符串”还是“从.md文件导入的字符串”真正需要验证的是 CSF 文件顶部的?raw导入方式是否工作正常。也就是说“能导入”和“能渲染”是两个环节前者依赖打包器的?raw支持后者依赖Markdown块的解析实现。核心用法?raw 导入 块继承示例文件给出的完整写法在 MDX 文档页或 story 的 docs 中import { Markdown } from storybook/addon-docs/blocks; import content from ./Markdown-content.md?raw; # A header Markdown{content}/Markdown官方文档 doc-block-markdown.mdx 用 README 场景给出了 DO / DONT 对照// DONT do this, will error import ReadMe from ./README.md; // DO this, will work import ReadMe from ./README.md?raw; import { Markdown } from storybook/addon-docs/blocks; Markdown{ReadMe}/Markdown?raw后缀是整条链路能工作的关键。它把文件内容“原样”导入为一个字符串而不经过编译求值。这一点在 Vite builder 的文档 vite.mdx 中有明确说明- import readme from ./readme.md; import readme from ./readme.md?raw;并且该文档指出Storybook 的 Webpack builder 同样理解?raw所以在 Vite 与 Webpack 之间迁移时这一写法可以通用对于 Vite 本身不处理的其他文件类型也可以同样追加?raw导出字符串。TypeScript 用户可能担心*.md?raw这种模块没有类型声明会报错。官方仓库内自带了模块声明作为佐证见 typings.d.tsdeclare module *.md?raw;在实际项目中Vite 的 client 类型vite/client已提供*?raw通配声明因此通常无需自行声明但在独立于 Vite 类型的工程配置中可以参照上述声明补一条保证类型检查通过。除 MDX 文档页外该模式也用于把 changelog 等文件嵌入自定义文档页官方代码片段 storybook-custom-docs-markdown.md 展示了一个Changelog.mdx的完整页面写法import { Meta, Markdown } from storybook/addon-docs/blocks; import Readme from ../../Changelog.md?raw; Meta titleChangelog / # Changelog Markdown{Readme}/MarkdownMarkdown 块的 props 与实现细节Markdown块从storybook/addon-docs/blocks导出blocks 入口 中的export * from ./Markdown。官方文档声明它接收两类 propschildrenstring类型提供待解析和展示的 markdown 字符串options透传给底层markdown-to-jsx库的选项。从源码 Markdown.tsx 可以看到更完整的实现事实// mirror props from markdown-to-jsx type MarkdownProps typeof PureMarkdown extends React.ComponentTypeinfer Props ? Props : never; const MarkdownImpl (props: MarkdownProps) { if (!props.children) { return null; } if (typeof props.children ! string) { throw new Error(/* 提示 children 必须是单个字符串…… */); } return ( PureMarkdown {...props} options{{ forceBlock: true, overrides: { code: CodeOrSourceMdx, a: AnchorMdx, ...HeadersMdx, ...props?.options?.overrides, }, ...props?.options, }} / ); };这里有三个值得注意的实现细节children必须是字符串如果传入的不是字符串会直接抛出错误错误信息本身还给出了正反示例——无效的写法是多行 JSX children有效的写法是用模板字符串包裹{# Some heading ...}。这解释了为什么在 MDX 中写Markdown{content}/Markdown时content必须是一个?raw导入来的字符串而不能把 markdown 文本直接作为块级子节点书写。forceBlock: true强制以块级模式解析 markdown保证多行内容的渲染行为符合预期。props 的options会覆盖默认行为overrides的合并顺序是先内置覆盖code/a/headers再展开用户的props.options.overrides最后展开props.options即用户传入的选项具有最高优先级可以对渲染行为做精细定制。另外MarkdownProps类型通过infer Props直接从markdown-to-jsx的组件类型推断而来源码注释写明这是“mirror props”模式因此Markdown块实际支持的 props 面与markdown-to-jsx保持一致。源码深潜Markdown 内容如何被渲染storybook/addon-docs的 package.json 依赖列表声明了渲染引擎markdown-to-jsx: ^7.7.2。Markdown块并非用 MDX2 二次编译 markdown而是用markdown-to-jsx将字符串解析为 React 元素并通过overrides替换了三类元素对应源码 mdx.tsx代码区分行内 code 与代码块。CodeOrSourceMdxmdx.tsx的判断逻辑是没有className且内容不含换行的渲染为行内Code否则解析className形如lang-jsx取语言标识渲染为完整的Source块——即带上语言标识、语法高亮与复制按钮的 docs 代码块无语言标识时回退为text。链接三种 href 三种处理。AnchorMdxmdx.tsx按href分类处理空 href、target_blank或http(s)://外链原样渲染a以#开头的页内锚点渲染AnchorInPage点击时document.getElementById命中后通过context.channel.emit(NAVIGATE_URL, hash)在 Storybook 内部导航其他相对路径指向 Storybook 其他页面拦截左键单击Cmd/Ctrl/Shift/Alt 点击及非左键保持浏览器默认行为改走 manager 侧 iframe 的 base URL 导航避免 preview iframe 的相对路径把链接解析错。标题自动生成可复制的锚点。HeaderMdx/HeadersMdxmdx.tsx覆盖h1h6如果标题带了remark-slug插件生成的id会渲染为带“复制标题 URL 到地址栏”按钮LinkIcon的标题组件如果没有id则退化为普通标题元素保证在没有 slug 插件时依然可用。最后整个实现被包在withMdxComponentOverride(Markdown, MarkdownImpl)中Markdown.tsx使Markdown块像其他 docs 块一样支持在文档上下文中做组件覆盖参见 component-overrides.mdx。为什么不直接导入 .md 文件到 MDX 里渲染官方文档 doc-block-markdown.mdx 的 “Why not import markdown directly?” 一节解释了必须经过Markdown块而非{ReadMe}直接插值的原因核心是纯 markdown 与 MDX2 存在细微但致命的语义差异MDX2 更严格会把 markdown 合法内容当作 JSX 表达式求值{ this is valid in a plain markdown file, but MDX2 will try to evaluate this as an expression }类 JSX 标签会被 MDX2 当作组件This is also valid, but MDX2 thinks this is a JSX component /MDX2 会把跨行文本包进p等标签导致渲染结构与纯 markdown 不一致div Some text /div在纯 markdown 中原样保留在 MDX2 中会编译为divpSome text/p/div。这与官方 story 中的注释可以相互印证Markdown.stories.tsx{ brackets, valid MD but invalid MDX - works here } Looks like a JSX tag/ !-- above is valid MD but invalid in markdown-to-jsx, so it will not be rendered -- Looks like a JSX tag / The above is only visible because it is wrapped in backticks即{ ... }与类 JSX 标签在 MDX 编译期就会出问题而经过Markdown块走markdown-to-jsx解析时前者作为 markdown 文本能正常渲染后者则不会被渲染成真实组件——这正是“字符串经由专用解析器渲染”与“字符串进入 MDX 编译管线”的行为差异。适用前提与验证方法Builder 前提?raw需要打包器支持。Vite builder 原生支持?raw即 Vite 的 import asset as string 能力Webpack builder 同样理解?raw见 vite.mdx 的说明两种主流构建方案下该写法均可用类型前提确保 TS 能识别*.md?raw模块Vite 项目由vite/client类型提供必要时参照 typings.d.ts 补充声明;运行时验证导入后可断言typeof content string。若渲染时报 “The Markdown block only accepts children as a single string”说明 children 不是字符串——通常是忘记用模板字符串包裹或导入时缺少?raw导致拿到的是模块对象官方对照 story仓库中 Markdown.stories.tsx 提供了三个 story 可直接参照——Markdown长文本文本 行内代码 代码块 各级标题 链接的全量覆盖用例、Imported .md file验证?raw导入环节、Text纯文本段落可作为你自定义用法时的回归参照。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考