组件交换(Swizzling)
在本节中,我们将介绍如何在 Docusaurus 中进行布局自定义。
似曾相识...?
本节类似于样式和布局,但这次我们将自定义 React 组件本身,而不仅仅是它们的外观。我们将讨论 Docusaurus 的一个核心概念:组件交换(swizzling),它允许进行更深层次的站点自定义。
实际上,组件交换允许用您自己的实现替换主题组件,并且有两种模式:
- 弹出(Ejecting):创建原始主题组件的副本,您可以完全自定义
- 包装(Wrapping):在原始主题组件周围创建包装器,您可以对其进行增强
为什么叫做组件交换(swizzling)?
这个名称来自 Objective-C 和 Swift-UI:方法交换(method swizzling)是改变现有选择器(方法)实现的过程。
对于 Docusaurus,组件交换意味着提供一个优先于主题提供的组件的替代组件。
您可以将其视为 React 组件的猴子补丁(Monkey Patching),使您能够覆盖默认实现。Gatsby 有一个类似的概念,称为主题遮蔽(theme shadowing)。
要深入理解这一点,您必须了解主题组件的解析方式。
组件交换过程
概述
Docusaurus 提供了一个方便的交互式 CLI 来交换组件。您通常只需要记住以下命令:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle
yarn swizzle
pnpm run swizzle
bun run swizzle
它将在您的 src/theme
目录中生成一个新组件,看起来像这个示例:
- Ejecting
- Wrapping
import React from 'react';
export default function SomeComponent(props) {
// 您可以完全自定义此实现
// 包括更改 JSX、CSS 和 React 钩子
return (
<div className="some-class">
<h1>某个组件</h1>
<p>某个组件的实现细节</p>
</div>
);
}
import React from 'react';
import SomeComponent from '@theme-original/SomeComponent';
export default function SomeComponentWrapper(props) {
// 您可以增强原始组件,
// 包括添加额外的 props 或 JSX 元素
return (
<>
<SomeComponent {...props} />
</>
);
}
要获取所有可交换的主题和组件的概览,请运行:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle -- --list
yarn swizzle --list
pnpm run swizzle --list
bun run swizzle --list
使用 --help
查看所有可用的 CLI 选项,或参考交换 CLI 文档。
如果您决定之前交换的组件不再需要,可以简单地从 src/theme
目录中删除其文件。删除组件后,请确保重新启动开发服务器,以确保更改正确反映。
交换组件后,重新启动开发服务器,以便 Docusaurus 知道新组件的存在。
确保了解哪些组件是安全交换的。某些组件是主题的内部实现细节。
docusaurus swizzle
只是帮助您交换组件的自动化方式。您也可以手动创建 src/theme/SomeComponent.js
文件,Docusaurus 将解析它。这个命令背后没有内部魔法!
弹出(Ejecting)
弹出主题组件是创建原始主题组件的副本的过程,您可以完全自定义和覆盖。
要弹出主题组件,请交互式地使用交换 CLI,或使用 --eject
选项:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle [主题名称] [组件名称] -- --eject
yarn swizzle [主题名称] [组件名称] --eject
pnpm run swizzle [主题名称] [组件名称] --eject
bun run swizzle [主题名称] [组件名称] --eject
一个示例:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle @docusaurus/theme-classic Footer -- --eject
yarn swizzle @docusaurus/theme-classic Footer --eject
pnpm run swizzle @docusaurus/theme-classic Footer --eject
bun run swizzle @docusaurus/theme-classic Footer --eject
这将把当前 <Footer />
组件的实现复制到您站点的 src/theme
目录。Docusaurus 现在将使用这个 <Footer>
组件副本,而不是原始组件。您现在可以自由地完全重新实现 <Footer>
组件。
import React from 'react';
export default function Footer(props) {
return (
<footer>
<h1>这是我的自定义站点页脚</h1>
<p>它与原始页脚非常不同</p>
</footer>
);
}
要在 Docusaurus 升级后保持弹出组件最新,请重新运行弹出命令并使用 git diff
比较更改。建议在文件顶部写一个简短的注释,解释您做了哪些更改,以便在重新弹出后更容易重新应用您的更改。
包装(Wrapping)
包装主题组件是在原始主题组件周围创建包装器的过程,您可以对其进行增强。
要包装主题组件,请交互式地使用交换 CLI,或使用 --wrap
选项:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle [主题名称] [组件名称] -- --wrap
yarn swizzle [主题名称] [组件名称] --wrap
pnpm run swizzle [主题名称] [组件名称] --wrap
bun run swizzle [主题名称] [组件名称] --wrap
一个示例:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle @docusaurus/theme-classic Footer -- --wrap
yarn swizzle @docusaurus/theme-classic Footer --wrap
pnpm run swizzle @docusaurus/theme-classic Footer --wrap
bun run swizzle @docusaurus/theme-classic Footer --wrap
这将在您站点的 src/theme
目录中创建一个包装器。Docusaurus 现在将使用 <FooterWrapper>
组件,而不是原始组件。您现在可以在原始组件周围添加自定义内容。
import React from 'react';
import Footer from '@theme-original/Footer';
export default function FooterWrapper(props) {
return (
<>
<section>
<h2>额外部分</h2>
<p>这是出现在原始页脚上方的额外部分</p>
</section>
<Footer {...props} />
</>
);
}
什么是 @theme-original
?
Docusaurus 使用主题别名来解析要使用的主题组件。新创建的包装器获取 @theme/SomeComponent
别名。@theme-original/SomeComponent
允许导入被包装器遮蔽的原始组件,而不会创建一个包装器导入自身的无限导入循环。
包装主题是一种在不弹出现有组件的情况下在其周围添加额外组件的好方法。例如,您可以轻松地在每个博客文章下添加自定义评论系统:
import React from 'react';
import BlogPostItem from '@theme-original/BlogPostItem';
import MyCustomCommentSystem from '@site/src/MyCustomCommentSystem';
export default function BlogPostItemWrapper(props) {
return (
<>
<BlogPostItem {...props} />
<MyCustomCommentSystem />
</>
);
}
什么是安全交换?
能力越大,责任越大
某些主题组件是主题的内部实现细节。Docusaurus 允许您交换它们,但这可能存在风险。
为什么存在风险?
主题作者(包括我们)可能需要随时更新他们的主题:更改组件 props、名称、文件系统位置、类型等。例如,考虑一个接收两个 props name
和 age
的组件,但在重构后,它现在接收一个包含上述两个属性的 person
prop。您的组件仍然期望这两个 props,将渲染 undefined
。
此外,内部组件可能会简单地消失。如果一个组件名为 Sidebar
,后来重命名为 DocSidebar
,您的交换组件将被完全忽略。
标记为不安全的主题组件可能在主题次要版本之间以不向后兼容的方式发生变化。 在升级主题(或 Docusaurus)时,您的自定义可能会意外行为,甚至破坏您的站点。
对于每个主题组件,交换 CLI 将指示主题作者声明的3个不同的安全级别:
- 安全:此组件可以安全交换,其公共 API 被认为是稳定的,在主题主要版本内不应发生破坏性更改
- 不安全:此组件是主题实现细节,不安全交换,并且可能在主题次要版本内发生破坏性更改
在交换 CLI 中,您将看到每个组件的安全级别。
安全级别
安全
安全组件是主题的公共 API 的一部分。它们具有稳定的接口,并且在主题的主要版本之间不会发生破坏性更改。
示例:Layout
、CodeBlock
、DocSidebar
不安全
不安全组件是主题的内部实现细节。它们的接口可能会在主题的次要版本之间发生变化。
示例:DocBreadcrumbs
、DocVersionBadge
禁止
禁止交换的组件是主题的核心内部组件,不应被交换。
示例:Root
、App
如何选择
- 如果可能,始终选择安全组件
- 仅在绝对必要时才选择不安全组件
- 永远不要尝试交换禁止组件
最佳实践
- 最小化自定义:尽可能少地自定义组件
- 优先使用包装:相比弹出,包装更安全、更容易维护
- 保持更新:在升级 Docusaurus 或主题时,检查并更新您的自定义组件
- 记录更改:在交换的组件中添加注释,解释您的自定义内容
- 考虑替代方案:在某些情况下,使用插件或主题配置可能比直接交换组件更好
结论
组件交换是一个强大的功能,允许您深入自定义 Docusaurus 站点。但是,它需要谨慎使用。始终优先考虑主题的默认实现,并且只在绝对必要时才进行交换。
记住:能力越大,责任越大。
我应该交换哪个组件?
常见的交换场景
添加额外内容
- 场景:在现有组件周围添加额外的 HTML、组件或功能
- 推荐方法:包装
- 示例:
- 在博客文章下添加评论系统
- 在页脚添加额外的链接或徽标
- 在侧边栏添加额外的导航项目
完全重新设计组件
- 场景:需要对组件进行根本性的视觉或功能更改
- 推荐方法:弹出
- 示例:
- 重新设计页脚布局
- 为侧边栏添加全新的交互模式
- 为代码块创建自定义样式和功能
添加交互性
不建议交换的情况
- 主题配置已足够:如果主题选项可以满足您的需求,请不要交换组件
- 轻微的样式调整:对于简单的样式更改,使用 CSS 覆盖或自定义 CSS
- 复杂的内部组件:避免交换深度嵌套或高度依赖内部实现的组件
寻求帮助
如果您不确定是否应该交换组件,可以:
- 查看主题文档
- 在 Docusaurus 讨论区提问
- 检查现有的社区插件和主题
结束语
组件交换是一个强大的工具,但应该谨慎使用。始终考虑:
- 是否有更简单的定制方法
- 自定义的长期维护成本
- 对站点性能和可维护性的影响
记住,最好的代码是不需要编写的代码!
我真的需要交换吗?
在决定交换组件之前,请考虑以下替代方案:
主题配置
Docusaurus 主题通常提供丰富的配置选项。在交换组件之前,请检查 docusaurus.config.js
中的主题配置。
示例:
- 自定义颜色和样式
- 添加徽标和版权信息
- 配置导航栏和侧边栏
CSS 自定义
对于大多数样式更改,使用 CSS 是最简单和最可维护的方法。
技术:
- 全局 CSS
- CSS 模块
- CSS-in-JS
- Sass/SCSS
插件和集成
许多功能可以通过插件实现,无需直接交换组件。
常见场景:
- 分析集成
- 搜索功能
- 评论系统
- 性能优化
自定义 React 组件
有时,创建自定义 React 组件并在站点中使用比交换现有组件更简单。
优势:
- 完全控制实现
- 更好的代码组织
- 更容易测试和维护
何时交换
仅在以下情况下考虑交换:
- 配置选项不足
- CSS 无法实现所需效果
- 插件不能提供必要的功能
- 自定义组件过于复杂
结论
组件交换是一个强大但复杂的功能。始终遵循"最小侵入"原则:
- 首先尝试配置和 CSS
- 然后考虑插件
- 最后,仅在绝对必要时才交换组件
记住:简单性胜于复杂性。
使用 <Root>
包装您的站点
在某些情况下,您可能需要在整个应用程序级别添加提供程序、上下文或全局样式。这可以通过交换 <Root>
组件来实现。
<Root>
是一个禁止交换的组件。这意味着您应该非常小心地使用它,并且在升级时可能需要手动迁移更改。
示例:添加全局提供程序
import React from 'react';
import { ThemeProvider } from 'your-theming-library';
import { YourGlobalContextProvider } from './YourGlobalContext';
export default function Root({ children }) {
return (
<ThemeProvider>
<YourGlobalContextProvider>
{children}
</YourGlobalContextProvider>
</ThemeProvider>
);
}
常见用例
- 状态管理:添加全局状态提供程序(如 Redux、MobX)
- 主题切换:实现跨应用程序的主题上下文
- 国际化:添加全局语言或本地化提供程序
- 性能监控:集成性能跟踪工具
- 全局样式和重置
注意事项
- 仅在绝对必要时使用
<Root>
交换 - 保持实现尽可能简单
- 避免在此组件中添加复杂的渲染逻辑
- 确保不会显著影响性能
替代方案
在某些情况下,可以考虑以下替代方案:
- 使用 Docusaurus 插件
- 在
docusaurus.config.js
中配置 - 创建自定义 React 组件
- 使用主题钩子和上下文
结束语
组件交换是一个强大的功能,但应该谨慎使用。始终权衡复杂性、可维护性和性能。
记住:最好的代码是不需要编写的代码。