从零搭建一个自定义明暗主题系统

1 年前
/
15

最近准备用 React + Antd + UnoCSS 开发一个和 NestJS Admin 配套的系统,想加个自定义主题功能,效果如下图,也可以点击 这里这里 体验。

theme.gif

theme.gif

一、需求

  • 用户可以自定义主题颜色,需要实时响应;
  • 用户可以切换明暗模式,需要实时改变背景和文字颜色;
  • 当用户切换系统主题时,网页需要作出响应;
  • 主题颜色和明暗模式需要缓存至 localStorage

二、准备工作

这里使用的是 pnpm,用 npm 或 yarn 等包管理工具的记得替换命令。

1. 创建项目

首先,拉取 vite 模板:

pnpm create vite my-theme --template react-ts

清空 src 目录:

image.png

image.png

启动项目:

pnpm run dev

2. 安装 Antd

pnpm add antd
image.png

image.png

3. 安装并配置 UnoCSS

开始之前,先推荐两个 VSCode 插件:

  • UnoCSS

    unocss-ext.png

    unocss-ext.png

    这个插件会读取 uno.config.ts ,提供了类名的提示以及预览:

    image.png

    image.png
  • Iconify IntelliSense

    icon-ext.png

    icon-ext.png

    这个插件提供了图标名称的提示和预览功能:

    image.png

    image.png

1) 安装并引入

因为后续会用的 CSS 图标,这里顺带安装一下图标库。(体积很大,70M,你想要的 SVG 图标 这里 都有)

pnpm add unocss @iconify/json -D

配置 vite.config.ts

main.tsx 中引入:

2) 配置文件

在项目根目录创建 uno.config.ts 配置文件:

更多配置选项,请阅读 UnoCSS 文档

关于图标的使用方法和配置,请看 这里

3) 样式重置

pnpm add @unocss/reset

main.tsx 中引入:

4) 测试

App.tsx 中随便写点代码:

image.png

image.png

三、需求实现

1. 自定义主题颜色

1) 组件引入并绑定状态

image.png

image.png

2) 和 Antd 组件同步

新版本的 Antd 采用了 CSS-in-JS 方案以及 梯度变量演变 算法,只需要提供一个基础变量 colorPrimary ,主题相关的其它配色就能推算出来,比如按钮点击的波纹颜色等等。

所以,我们只需要将 primaryColro 通过 ConfigProvider 提供给 Antd 就可以了:

antd.gif

antd.gif

3) 和其他颜色同步

这里使用 CSS 变量的方案来保持颜色同步:

  • 给根元素添加一个 CSS 变量 --primary-color;
  • 给 UnoCSS 添加一个颜色 primary: 'var(--primary-color)'
  • 添加一个副作用,让 primaryColro--primary-color 保持同步。
primary-color.gif

primary-color.gif

2. 明暗模块切换

安装 classnames 方便组装类名:

pnpm add classnames

1) 封装切换组件

先给图标按钮加个 shortcut 组合类:

创建组件 ToggleTheme.tsx

2) 引入组件

image.png

image.png

3) 绑定 dark 类

目前常用的黑暗模式方案是给根元素添加一个 dark 类,然后在代码中通过 dark:text-yellow 指定黑暗模式下的样式:

使用 useEffect 同步 dark 类:

dark-text.gif

dark-text.gif

4) 使用 CSS 变量同步颜色

新建 main.css

引入 main.tsx

效果如下:

bg-dark.gif

bg-dark.gif

5) 同步 Antd

Antd 暴露的 theme 提供了几种颜色算法,我们需要用到这两种:

  • defaultAlgorithm 默认算法
  • darkAlgorithm 黑暗模式的算法

我们需要根据 modeConfgProvider 提供不同的算法:

效果如下:(注意看 zzz 按钮的背景颜色)

dark-antd.gif

dark-antd.gif

3. 监听系统主题

刚刚我们实现了手动切换明暗模式,现在来实现根据当前的系统主题使用对应的模式。

1) 获取并监听系统主题

CSS 提供了媒体查询 prefers-color-scheme: dark 用来监听系统明暗模式,如果我们想读取,需要调用 window.matchMedia,该方法需要传入一个查询字符串,并返回一个 MediaQueryList对象:

  • matches 布尔值
  • addEventListener 添加监听事件处理函数

为了更好的逻辑封装和复用,创建一个自定义 hook usePreferredDark.ts,返回系统是否处于黑暗模式:

测试:

效果如下:

Kapture 2024-01-29 at 16.19.04.gif

Kapture 2024-01-29 at 16.19.04.gif

2) 结合 mode

监听系统主题我们实现了,现在需要把 preferredDarkmode 结合起来判断当前网页是否处于黑暗模式,封装一个自定义 hook useDark.ts,如果是黑暗模式,返回 true:

逻辑解释:

  • 因为 mode 是用户选择的,所以它优先级最高,如果 mode === 'dark',直接短路返回 true;
  • 如果 mode === 'light',返回 false
  • 如果 mode === 'auto',返回当前系统是否处于黑暗模式

测试:

效果如下:

Kapture 2024-01-29 at 16.36.32.gif

Kapture 2024-01-29 at 16.36.32.gif

最后修改 mode 的初始值为 auto

4. 缓存至 localStorage

这个实现起来很简单,直接使用 ahooks 提供的 useLocalStorageState 即可。

1) 安装 ahooks

pnpm add ahooks

2) 替换 useState

效果如下:

Kapture 2024-01-29 at 16.48.44.gif

Kapture 2024-01-29 at 16.48.44.gif

3) 背景闪烁

刷新页面的时候,明显可以感觉到背景颜色闪烁了一下。这是因为根元素的 dark 类是通过 JS 设置的,我们的代码会在 html 创建之后执行。

解决方案:在 index.htmlhead 中插入一段脚本:

四、总结技术要点

  • window.matchMedia API
  • Antd ConfigProvider
  • useLocalStorageState
  • CSS 变量

完整代码见 GitHub

  • Loading...
  • Loading...
  • Loading...
  • Loading...