跳到主要内容

多主题和暗夜模式

亮色主题/暗色主题 在用户切换主题之后将用户的选择存储在 cookie 中 储存在 cookie 中的好处是保证 ssr 和 csr 拿到的结果是一样的

antd + css 变量实现暗夜模式

  • components
    • ThemeProvider
      • themeContext.ts
      • themeProvider.tsx
  • hooks
    • useTheme.ts
  • styles -global.css
// themeContext.ts

import React, { useContext } from "react";

export interface ThemeContextValue {
theme: string;
toggleTheme?: () => void;
}

export const ThemeContext =
React.createContext <
ThemeContextValue >
{
theme: "light",
};

export const useThemeContext = () => {
return useContext(ThemeContext);
};
// themeProvider.ts
import React from "react";
import { ThemeContext } from "@/components/ThemeProvider/themeContext";
import { ConfigProvider, theme } from "antd";
import useTheme from "@/hooks/useTheme";
import locale from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";

export interface ThemeProviderProps {
children: React.ReactNode;
}

export function ThemeProvider(props: ThemeProviderProps) {
const [value, toggleTheme] = useTheme();

return (
<ThemeContext.Provider value={{ theme: value, toggleTheme }}>
// 包括了antd本身的ConfigProvider
<ConfigProvider
locale={locale}
theme={{
algorithm:
value === "light" ? theme.defaultAlgorithm : theme.darkAlgorithm,
}}
>
{props.children}
</ConfigProvider>
</ThemeContext.Provider>
);
}
import { useEffect, useState } from "react";

const useTheme = (): [string, () => void] => {
const [theme, setTheme] = useState("light");

useEffect(() => {
const storeTheme = window.localStorage.getItem("theme");
const root = document.documentElement;
if (storeTheme) {
setTheme(storeTheme);
root.className = storeTheme;
} else {
// 更改根元素类名
root.className = "light";
}
}, []);

const toggleTheme = () => {
setTheme((prevTheme) => {
const value = prevTheme === "light" ? "dark" : "light";
window.localStorage.setItem("theme", value);
const root = document.documentElement;
root.className = value;
return value;
});
};

return [theme, toggleTheme];
};

export default useTheme;
/* global.css */
/* 亮色主题颜色定义 */
:root.light {
--color: #fc0000;
--background-color: #ffffff;
}

/* 暗色主题颜色定义 */
:root.dark {
--color: #d4ff00;
--background-color: #000000;
}

app.tsx

const App = ({ Component, pageProps }: AppProps) => (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);

export default App;

使用

一定要使用 useThemeContext 包裹的 useContext

这样拿到的才是 Provider 中唯一注入的 state

有人可能会问直接使用 useTheme 和 ConfigProvider 实现不行吗,为什么还要使用 react 的 Context 和 Provider?其实核心就一句话,Context 和 Provider 这哥们俩实现了组件间数据共享。解释一下就是当你在其他组件中(例如在 Header 组件)直接用 useTheme 切换主题时,_app.tsx 中的 ConfigProvider 是感知不到主题变化的,而改变 Context 是全局都可以感知到的,所以,这就是使用 Context 和 Provider 的原因

const { theme, toggleTheme } = useThemeContext();

<Button onClick={() => toggleTheme && toggleTheme()}>{theme}</Button>;

实现方法

Css in js

emotion 将 CSS 样式转换为 JavaScript 代码并动态注入到页面中

1.初始化两套色卡, key 值保持一致

export const BrightColors = {
TextPrimary:....
}

export const DarkColors = {
TextPrimary:....
}

2.通过 ThemeProvider (从 emotion/react 中引入)从顶层注入色卡

const comp = styled.div({theme})=>({
.....
color: theme.colors.TextPrimary
})

3.在书写 css 代码中只需从色卡中取相同的 key 就可以了

4.当用户切换颜色时,会触发上层 peovider 中的 hooks,从而改变主题

css 变量

在根元素下加上两种类名,都定义了 css 变量

:root .isDark {
--color-primary: rgba(1,1,1,1)
--color-primary-hover: rgba(var(--primary-600),1)
}

:root .isLight{
--color-primary: rgba(1,1,1,1)
--color-primary-hover: rgba(var(--primary-600),1)
}

给根节点切换类名,从而匹配到对应的颜色主题

// 或者利用属性选择器
.button {
background-color: var(--color-bg-0);
}
body {
--color-bg-0: #fff;
}
body[theme-mode="dark"] {
--color-bg-0: #000;
}
变量就是一些自定义的属性,遵循正常的的样式覆盖原则。如上所示,button的背景颜色由
--color-bg-0 CSS 变量决定,当给 body 设置
<body theme-mode="dark">
...
</body>
时, --color-bg-0: #000 会覆盖 --color-bg-0: #fff,此时 button 背景色为黑色。

或者用 js 精确修改

  • Js 修改 body 主题色
document.body.style.setProperty("--themeColor", "#ff0000");

优点

  1. 可以实现制定动态色值换肤

缺点

  1. 在构建时根据 css 变量生成对应的 css,换肤发生在运行时,并不能生成对应的 css,只是在已有 css 中调整
  2. 兼容(可以安装插件)

PostCss(后处理)

PostCSS

后处理并不是只能处理 css,也可以代替预处理器,直接处理 scss 关注点一:如何解析识别 css/scss/stylus 等样式文件中的【颜色字面量】,如:background: white 中的 white。 关注点二:计算识别的【颜色字面量】是否可以替换为 semi 主题包中提供的【颜色变量】如:var(--color-bg-0)。如果【颜色字面量】与【颜色变量】对应的颜色值相同或相近则认为可以替换。 对于第二点,计算是否有对应的颜色变量,可以使用 chorma-js。chorma-js 可以实现任意颜色格式之间的转换以及计算颜色之间的相似度,比如:chroma.distance(color1, color2)。

PostCSS 与 CSS 的关系,就相当于 Babel 与 JavaScript。PostCSS 可以将 CSS 解析为抽象语法树 (AST),并提供了 API 允许分析和转换 CSS 文件的内容。

不仅仅是 CSS,通过引入插件,PostCSS 可以解析任何形式的样式代码,比如 Sass, Less, Stylus 以及 JSX 中的样式代码。

因此借助 PostCSS 解析后的结果,针对以上三种形式的颜色值,识别算法为:

  • CSS 属性值中是否包含穷举的 颜色关键字;
  • CSS 属性值以 '#' 开头则认为是 RGB 16 进制形式;
  • CSS 属性值为函数形式且名称为 rgb, rgba, hsl, hsla 等则认为是函数形式。 由此,基于 PostCSS 我们可以完成样式的解析以及颜色的识别。如果要达到自动完成项目中样式文件的颜色值替换工作,只需要添加收集项目中的样式文件的逻辑,并封装为 CLI 工具方便使用即可。

PostCSS 已经能够满足我们的需求,然而,还有另一种实现方式,Stylelint

Stylelint

A mighty, modern linter that helps you avoid errors and enforce conventions in your styles. Stylelint 与 CSS 的关系,就相当于 ESLint 与 JavaScript 的关系。Stylelint 是针对样式文件的代码审查工具。Stylelint 底层基于 PostCSS,对样式文件进行解析分析,可以对 CSS/Sass/Stylus/Less 等样式文件进行审查。 比如 Stylelint 其中一个规则,color-named,不允许使用颜色关键字,效果如下:

当我们在代码中使用了类似 white 颜色关键字,Stylelint 会给出提示,借助编辑器插件,我们能够在编码过程中获得实时提示,非常方便。 每个规则在 Stylelint 中都是一个插件,插件的输入是样式文件通过 PostCSS 解析后的抽象语法树,插件可以基于 PostCSS API 遍历语法树,分析/修改样式代码。因此类似规则 color-named,我们也可以实现一个 Stylelint 插件,分析并识别样式文件中的【颜色字面量】,并给出提示,针对有对应的 semi 颜色变量的,支持自动替换(autofix)。具体可以查看官方的插件开发文档。

如何识别颜色值

CSS 中指定颜色值的方式有三类,分别为:

  • 颜色关键字,如:white, blue 等等;
  • RGB 16 进制形式,如:#FFF;
  • 函数形式,如:rgb(255, 255, 0) 或者 rgba(255, 0, 255, 0.5),hsl(0deg, 0%, 13%) 或者 hsla(0deg, 0%, 13%, 1) 或者 hwb(...) gray(...)

随不同系统变化

1.js 通过媒体查询,然后 onChange 事件 2.css 媒体查询

参考

https://juejin.cn/post/7313910763978358824?searchId=202404082305230F18F33DF14F52BA4A1C

https://juejin.cn/post/7003315163625422879?searchId=20240408231756EB29289C68ED42B60554#heading-3