代码混淆
本节将介绍 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
目录中生成一个新组件,示例如下:
- 弹出
- 包裹
import React from 'react';
export default function SomeComponent(props) {
// You can fully customize this implementation
// including changing the JSX, CSS and React hooks
return (
<div className="some-class">
<h1>Some Component</h1>
<p>Some component implementation details</p>
</div>
);
}
import React from 'react';
import SomeComponent from '@theme-original/SomeComponent';
export default function SomeComponentWrapper(props) {
// You can enhance the original component,
// including adding extra props or JSX elements around it
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 [theme name] [component name] -- --eject
yarn swizzle [theme name] [component name] --eject
pnpm run swizzle [theme name] [component name] --eject
bun run swizzle [theme name] [component name] --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>This is my custom site footer</h1>
<p>And it is very different from the original</p>
</footer>
);
}
弹出一个不安全的组件有时会导致复制大量的内部代码,而这些代码现在需要你自行维护。这会使 Docusaurus 升级变得更加困难,因为如果接收到的属性或使用的内部主题 API 发生变化,你将需要迁移你的自定义内容。
尽可能优先选择包裹 (wrapping):需要维护的代码量更少。
为了在 Docusaurus 升级后保持弹出的组件最新,请重新运行弹出命令并使用 git diff
比较更改。还建议在文件顶部写一段简短的注释,说明你所做的更改,以便在重新弹出后更容易地重新应用你的更改。
包裹 (Wrapping)
包裹主题组件是在原始主题组件周围创建一个包裹器的过程,你可以对其进行增强。
要包裹主题组件,请使用交互式混淆 CLI,或使用 --wrap
选项:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle [theme name] [component name] -- --wrap
yarn swizzle [theme name] [component name] --wrap
pnpm run swizzle [theme name] [component name] --wrap
bun run swizzle [theme name] [component name] --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>Extra section</h2>
<p>This is an extra section that appears above the original footer</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 允许你混淆它们,但这可能存在风险。
为什么有风险?
主题作者(包括我们)可能需要随着时间更新他们的主题:改变组件属性、名称、文件系统位置、类型等。例如,考虑一个接收两个属性 name
和 age
的组件,但在重构后,它现在接收一个包含上述两个属性的 person
属性。你的组件仍然期望这两个属性,它将渲染 undefined
而不是预期的内容。
此外,内部组件可能会直接消失。如果一个组件名为 Sidebar
后来重命名为 DocSidebar
,你混淆的组件将完全被忽略。
标记为不安全的主题组件在主题次要版本之间可能会以不向后兼容的方式更改。 当升级主题(或 Docusaurus)时,你的自定义内容可能表现异常,甚至可能破坏你的网站。
对于每个主题组件,混淆 CLI 将指示主题作者声明的3 个不同的安全级别:
- 安全 (Safe):此组件可以安全地进行混淆,其公共 API 被认为是稳定的,并且在主题主要版本内不应发生破坏性更改。
- 不安全 (Unsafe):此组件是主题的实现细节,混淆不安全,在主题次要版本内可能会发生破坏性更改。
- 禁用 (Forbidden):混淆 CLI 将阻止你混淆此组件,因为它根本不适合混淆。
有些组件可能适合包裹,但不适合弹出。
不要太害怕混淆不安全的组件:请记住,破坏性更改可能会发生,你可能需要在次要版本升级时手动升级你的自定义内容。
如果你有混淆不安全组件的强烈用例,请在此报告,我们将共同寻找解决方案使其变得安全。
我应该混淆哪个组件?
要精确地混淆哪个组件以达到预期效果并不总是很清楚。提供大部分主题组件的 @docusaurus/theme-classic
大约有100 个组件!
要打印所有 @docusaurus/theme-classic
组件的概览:
- npm
- Yarn
- pnpm
- Bun
npm run swizzle @docusaurus/theme-classic -- --list
yarn swizzle @docusaurus/theme-classic --list
pnpm run swizzle @docusaurus/theme-classic --list
bun run swizzle @docusaurus/theme-classic --list
你可以按照以下步骤定位要混淆的适当组件:
- 组件描述。 有些组件提供了简短的描述,这是找到正确组件的好方法。
- 组件名称。 官方主题组件的命名是语义化的,所以你应该能够从名称推断其功能。混淆 CLI 允许你输入组件名称的一部分来缩小可用选择范围。例如,如果你运行
yarn swizzle @docusaurus/theme-classic
,并输入Doc
,则只会列出与文档相关的组件。 - 从更高级别的组件开始。 组件形成一个树形结构,其中一些组件导入其他组件。每个路由都将与一个顶级组件相关联,该组件将由路由渲染(其中大部分列在内容插件中的路由中)。例如,所有博客文章页面都将
@theme/BlogPostPage
作为最顶层的组件。你可以从混淆这个组件开始,然后沿着组件树向下查找,以定位你所针对的组件。找到正确的组件后,不要忘记通过删除文件来取消混淆其余部分,这样你就不会维护过多的组件。 - 阅读主题源代码并明智地使用搜索。
我需要混淆吗?
代码混淆最终意味着你必须维护一些与 Docusaurus 内部 API 交互的额外 React 代码。如果可以的话,在自定义你的网站时,考虑以下替代方案:
- 使用 CSS。 CSS 规则和选择器通常可以帮助你实现相当程度的自定义。更多详细信息请参考样式和布局。
- 使用翻译。 这听起来可能令人惊讶,但翻译最终只是一种自定义文本标签的方式。例如,如果你的网站默认语言是
en
,你仍然可以运行yarn write-translations -l en
并编辑生成的code.json
。更多详细信息请参考国际化教程。
越小越好。 如果代码混淆不可避免,尽量只混淆相关部分并尽可能少地维护自己的代码。混淆一个小组件通常意味着升级时破坏性更改的风险较小。
包裹 (Wrapping) 也是比弹出 (ejecting) 更安全的替代方案。
使用 <Root>
包裹你的网站
<Root>
组件在 React 树的最顶层渲染,位于主题 <Layout>
之上,并且从不卸载。它是添加不应在导航之间重新初始化(用户认证状态、购物车状态等)的有状态逻辑的完美位置。
通过在 src/theme/Root.js
创建文件来手动混淆它:
import React from 'react';
// Default implementation, that you can customize
export default function Root({children}) {
return <>{children}</>;
}
使用此组件来渲染 React 上下文提供者。