React 中轻松实现暗黑模式(以及多种颜色主题)

Avatar of Abram Thau
Abram Thau

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

我正在为一家初创公司开发一个大型 React 应用程序,除了想要一些好的策略来保持我们的样式井井有条之外,我还想尝试一下这种“暗黑模式”。鉴于 React 周围有庞大的生态系统,你可能会认为有一个针对样式主题的通用解决方案,但稍微搜索一下就会发现并非如此。

市面上有很多不同的选择,但其中许多选择与非常具体的 CSS 策略相结合,例如使用 CSS 模块、某种形式的 CSS-in-JS 等等。我还发现了一些特定于某些框架(如 Gatsby)的工具,但不是通用的 React 项目。我想要的是一个易于设置和使用,无需跳过大量障碍的基本系统;速度快,易于让整个前端和全栈开发人员团队快速上手。

我最喜欢的现有解决方案围绕使用**CSS 变量和数据属性**展开,在 StackOverflow 答案中找到。但这还需要一些 useRef 内容,感觉很像黑客行为。正如每个电视购物节目所说的那样,一定有更好的方法!

幸运的是,确实有。通过将这种通用的 CSS 变量策略与美观的 useLocalStorage 钩子相结合,我们拥有了一个强大且易于使用的主题系统。我将逐步介绍设置和运行此系统的方法,从一个全新的 React 应用程序开始。如果你坚持到最后,我还会向你展示如何将其与 react-scoped-css 集成,这也是我最喜欢的在 React 中使用 CSS 的方法。

项目设置

让我们从 一个非常好的起点 开始:从头开始。

本指南假设你已经基本了解 CSS、JavaScript 和 React。

首先,确保你已经安装了最新版本的 Node 和 npm。然后导航到你想要存放项目的文件夹,在那里运行 git bash(或你喜欢的命令行工具),然后运行

npx create-react-app easy-react-themes --template typescript

easy-react-themes 替换为你的项目名称,如果你想在 JavaScript 中工作,可以随意省略 --template typescript。我碰巧喜欢 TypeScript,但这对本指南来说实际上没有区别,除了文件以 .ts/.tsx 结尾还是 .js/.jsx 结尾。

现在,我们将打开我们全新的项目,并在代码编辑器中打开它。在这个示例中,我使用的是 VS Code,如果你也是,那么你可以运行以下命令

cd easy-react-themes
code .
现在看起来不多,但我们会改变这一点!

接下来运行 npm start 启动开发服务器,并在新的浏览器窗口中生成以下内容

最后,使用以下命令安装 use-local-storage 包

npm i use-local-storage

这就是项目的初始设置!

代码设置

打开 App.tsx 文件,删除我们不需要的内容。

我们想要从…
…变成这样。

删除 App.css 中的所有内容

好!现在让我们创建主题!打开 index.css 文件,并在其中添加以下内容

:root {
  --background: white;
  --text-primary: black;
  --text-secondary: royalblue;
  --accent: purple;
}
[data-theme='dark'] {
  --background: black;
  --text-primary: white;
  --text-secondary: grey;
  --accent: darkred;
}

到目前为止,我们拥有以下内容

你看到我们刚才做了什么吗?如果你不熟悉 CSS 自定义属性(也称为 CSS 变量),它们允许我们定义一个值在样式表中的其他地方使用,模式为 --key: value。在本例中,我们只定义了一些颜色,并将它们应用到 :root 元素,以便我们可以在整个 React 项目中需要的地方使用它们。

第二部分,从 [data-theme='dark'] 开始,是事情变得有趣的地方。HTML(以及我们用于在 React 中创建 HTML 的 JSX)允许我们使用 data-* 属性为 HTML 元素设置完全任意的属性。在本例中,我们为应用程序的最外层 <div> 元素提供了一个 data-theme 属性,并在 lightdark 之间切换它的值。当它是 dark 时,CSS[data-theme='dark'] 部分会覆盖我们在 :root 中定义的变量,因此任何依赖这些变量的样式也会被切换。

让我们将其付诸实践。回到 App.tsx 中,让我们为 React 提供一种跟踪主题状态的方法。我们通常会使用 useState 来处理局部状态,或使用 Redux 来处理全局状态管理,但我们也希望用户的主题选择在他们离开应用程序并返回后保持不变。虽然我们可以使用 Redux 和 redux-persist,但这对于我们的需求来说过于复杂。

相反,我们将使用之前安装的 useLocalStorage 钩子。正如你所预期的那样,它为我们提供了一种将内容存储在本地存储中的方法,但它是一个 React 钩子,它维护着对 localStorage 操作的状态信息,让我们的生活变得轻松。

你们中的一些人可能在想:“哦,不,如果页面在我们的 JavaScript 检查 localStorage 之前渲染了,我们会遇到可怕的“错误主题闪烁”怎么办?” 但是你不必担心这里,因为我们的 React 应用程序完全是在客户端渲染的;初始 HTML 文件基本上是一个骨架,其中只有一个 <div>,React 将应用程序附加到其中。所有最终的 HTML 元素都是在检查 localStorage 之后 由 JavaScript 生成的。

所以,首先,使用以下命令在 App.tsx 的顶部导入钩子

import useLocalStorage from 'use-local-storage'

然后,在我们的 App 组件中,我们使用以下命令

const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const [theme, setTheme] = useLocalStorage('theme', defaultDark ? 'dark' : 'light');

这为我们做了一些事情。首先,我们检查用户是否在浏览器设置中设置了主题偏好。然后我们创建一个与 localStorage 绑定的状态 theme 变量,以及用于更新 themesetTheme 函数。useLocalStorage 会向 localStorage 添加一个 key:value 对,如果它还不存在,则默认值为 theme: "light",除非我们的 matchMedia 检查返回 true,在这种情况下,它是 theme: "dark"。这样,我们就能优雅地处理两种可能性:为返回的用户保留主题设置,或者在处理新用户时默认情况下尊重他们的浏览器设置。

接下来,我们将向 App 组件添加一些内容,以便我们有一些元素可以进行样式设置,以及一个按钮和一个函数来实际允许我们切换主题。

完整的 App.tsx 文件

秘密武器在第 14 行,我们向顶层的 <div> 添加了 data-theme={theme}。现在,通过切换 theme 的值,我们选择是否要使用 data-theme='dark' 部分中的变量覆盖 :root 中的 CSS 变量 index.css 文件。

我们需要做的最后一件事是添加一些使用我们之前创建的 CSS 变量的样式,然后它就会启动并运行!打开 App.css,并将此 CSS 放入其中

.App {
  color: var(--text-primary);
  background-color: var(--background);
  font-size: large;
  font-weight: bold;
  padding: 20px;
  height: calc(100vh - 40px);
  transition: all .5s;
}
button {
  color: var(--text-primary);
  background-color: var(--background);
  border: 2px var(--text-primary) solid;
  float: right;
  transition: all .5s;
}

现在,主 <div> 的背景和文本,以及 <button> 的背景、文本和轮廓都依赖于 CSS 变量。这意味着当主题更改时,所有依赖这些变量的内容也会更新。还要注意,我们向 App<button> 添加了 transition: all .5s,以便在颜色主题之间实现平滑过渡。

现在,回到运行该应用程序的浏览器,我们会看到以下内容

大功告成! 让我们再添加一个组件,只是为了展示如果我们正在构建一个真正的应用程序,该系统是如何工作的。我们将在 /src 中添加一个 /components 文件夹,在 /components 中放置一个 /square 文件夹,并添加一个 Square.tsxsquare.css,如下所示

让我们将其导入回 App.tsx 中,如下所示

现在我们有了以下结果

就这样!很明显,这是一个非常基本的情况,我们只使用默认(亮色)主题和次要(暗色)主题。但是如果你的应用程序需要,此系统可以用于实现多个主题选项。就我个人而言,我正在考虑在我的下一个项目中提供亮色、暗色、巧克力色和草莓色的选项——尽情发挥创意吧!

奖励:与 React Scoped CSS 集成

使用 React Scoped CSS 是我最喜欢的保持每个组件的 CSS 封装的方法,以防止名称冲突混乱和意外的样式继承。我以前的首选方法是 CSS 模块,但它有一个缺点,那就是它会让浏览器中的 DOM 看起来像是机器人写的所有类名… 因为情况的确如此。这种缺乏人类可读性使得调试比必要时更加烦人。React Scoped CSS 出现了。我们仍然可以使用 CSS(或 Sass)以我们一直以来的方式编写,并且输出看起来像是人类写的。

鉴于 React Scoped CSS 仓库 提供了完整且详细的安装说明,我在这里只对它们进行总结。

首先,根据他们的说明安装和配置 Create React App Configuration Override (CRACO)。Craco 是一种工具,它让我们可以覆盖捆绑到 create-react-app (CRA) 中的一些默认 webpack 配置。通常,如果你想在 CRA 项目中调整 webpack,你首先需要“弹出”项目,这是一个不可逆操作,它会让你完全负责通常为你处理的所有依赖项。你通常希望避免弹出,除非你确实非常了解自己在做什么,并且有充分的理由走这条路。相反,CRACO 让我们可以对 webpack 配置进行一些小的调整,而不会出现混乱。

完成后,安装 React Scoped CSS 包

npm i craco-plugin-scoped-css

(README 指令使用 yarn 进行安装而不是 npm,但两者都可以。) 现在已经安装好了,只需将 CSS 文件重命名,在 .css 之前添加 .scoped,如下所示

app.css -> app.scoped.css

并且我们需要确保我们在将该 CSS 导入组件时使用新名称

import './app.css'; -> import './app.scoped.css';

现在所有 CSS 都已封装,因此它们仅应用于导入它们的组件。它通过使用 data-* 属性来实现,就像我们的主题系统一样,因此当将范围化的 CSS 文件导入组件时,该组件的所有元素都将用一个属性标记,例如 data-v-46ef2374,并且该文件中的样式将被包装,以便它们仅应用于具有该精确数据属性的元素。

这很不错,但让它与这个主题系统一起工作的诀窍是我们明确地 **不希望** CSS 变量被封装;我们希望它们应用于整个项目。因此,我们只需不将 index.css 更改为具有范围化的 CSS 文件......换句话说,我们可以让该 CSS 文件保持原样。就是这样!现在我们有一个强大的主题系统与范围化的 CSS 协同工作——我们实现了梦想!

非常感谢您阅读本指南,如果您用它构建了一些很棒的东西,我很乐意知道!