基于Ant Design和CSS变量实现前端动态主题 您所在的位置:网站首页 xboxone动态主题 基于Ant Design和CSS变量实现前端动态主题

基于Ant Design和CSS变量实现前端动态主题

#基于Ant Design和CSS变量实现前端动态主题| 来源: 网络整理| 查看: 265

正文

快到年末了,最近总结了下今年做的一个大型后台项目重构工作。

整理过程中,感觉其中动态主题切换的功能有不少可以值得反思和挖掘的地方,于是在团队内进行了一次技术分享,也在这里用文本形式进行一个记录,便于日后温故知新。

注:本文基于V5版本的ant-design编写。

本文中的实践过程代码示例位于:

github.com/victorsonge…

在线代码示例位于(需科学上网):

ant-theme-demo.vercel.app

Untitled.png

前端主题方案演变

前端主题最常见的应用就是在浅色与深色模式之间切换主题。当然,前端作为最能折腾的群体,其实很早就有主题切换的例子了,比如早年间的QQ空间,还有各种信息门户网站,其实都会提供给用户在各种主题色之间选择的功能。

我们可以大致将前端主题方案演变的发展历史划分为静态和动态两个时期。

静态主题时期

静态时期持续的时间很长,时至今日仍有不少网站和组件库使用静态主题方案。

静态方案的核心思路有两种:

其一,将不同主题的样式划分至对应的class下,在运行时基于用户选择,切换DOM上的class名,命中不同的主题CSS;

其二,是预先编写(或通过脚手架在编译阶段生成)所有可供选择主题样式文件,然后在运行时去动态加载当前主题对应的样式文件。

目前采用静态主题方案的前端组件库代表是Element UI,提供了多种方式来帮开发者获取不同主题的样式文件,通常只要根据情况去按需引入对应主题的样式文件,即可实现主题变更。

动态主题时期

时间节点来到2023年末的今天,使用动态主题切换方案的网站已经越来越多了。

动态主题的核心思路也有两种:

其一,通过CSS-in-JS库动态更新绑定在前端组件上的style;

其二,基于CSS变量实现,关键逻辑在于运行时动态修改CSS变量的值,这样在开发阶段只要编写一份样式文件,理论上运行时可以映射出无数份实际的页面主题。

目前采用动态主题方案的前端组件库代表是ant-design,v5版本的ant-design内部是通过一套CSS-in-JS方案来处理动态主题的。

配置ant-design组件主题

已经了解如何修改ant-design组件主题的读者,可以跳过本节内容,直接跳转下一节CSS变量动态同步自定义组件与ant-design组件主题样式。

ant主题基础概念

token:在 5.0 版本中,ant- design把影响主题的最小元素称为 Design Token。通过修改 Design Token,我们可以呈现出各种各样的主题或者组件。通过在 ConfigProvider 中传入 theme 属性,可以配置主题。

theme:传给ant- design的组件的props,通过 theme 中的 token 属性,可以修改一些主题变量。部分主题变量会引起其他主题变量的变化,我们把这些主题变量称为 Seed Token。

theme对象的结构如下:

属性说明类型默认值token用于修改 Design TokenAliasToken-inherit继承上层 ConfigProvider 中配置的主题。booleantruealgorithm用于修改 Seed Token 到 Map Token 的算法(token: SeedToken) => MapToken  ((token: SeedToken) => MapToken)[]defaultAlgorithmcomponents用于修改各个组件的 Component Token 以及覆盖该组件消费的 Alias TokenComponentsConfig- 调配theme config json

ant官方提供了一个十分好用的在线主题编辑器:

ant.design/theme-edito…

Untitled 1.png

这里建议仅对品牌色、基础背景色等基础变量(Seed Token)进行修改,修改了这几个基础项之后,其他的高阶配置项也会基于ant- design的算法自动更新为相匹配的值。

虽然ant开放了很多细节的颜色配置项,但是在没有专业设计基础的情况下,随意修改某些细节配置——

容易造成整体色彩效果不和谐; 可能导致某些场景下,出现难以阅读的背景与文本颜色组合 传入theme config

调配完成后,点击页面右上角的“主题配置”,可以得到一个theme config JSON。

Untitled 2.png

将这份JSON作为props.theme传入ant-design的组件即可。

需要注意的是,我们在代码中使用这段配置时,需要手动将algorithm的值替换为相应的ant-design导出的算法对象。

代码示例如下:

import { Button, ConfigProvider, Space, theme } from 'antd'; import React from 'react'; const App: React.FC = () => ( Primary Default ); export default App; ant-design组件主题动态修改

由于ant-design组件等主题完全由运行时传入的theme JSON决定,开发者可以通过任意方式维护这份JSON,或者维护多份主题的JSON,通过全局状态(如Context或者Redux等状态管理库),甚至通过后端接口获取一个主题的JSON,拿到之后动态更新的theme属性即可。

代码示例:

import React, { createContext, useState } from 'react'; import { ConfigProvider, Button } from 'antd'; const ThemeContext = createContext(); const themes = { default: { primaryColor: '#1890ff', secondaryColor: '#f0f2f5', }, dark: { primaryColor: '#333', secondaryColor: '#000', }, }; const App = () => { const [theme, setTheme] = useState(themes.default); const changeTheme = (selectedTheme) => { setTheme(themes[selectedTheme]); }; return ( changeTheme('default')}>Default Theme changeTheme('dark')}>Dark Theme {/* 其他ant-design组件的使用 */} ); }; export default App;

在这个示例中,我们首先创建了一个ThemeContext,用于存储当前主题的状态。然后,我们定义了两个预置的主题:default和dark,每个主题都有不同的颜色设置。

在App组件中,我们使用useState来管理当前主题的状态,并通过setTheme函数来实现主题切换。通过点击按钮,我们可以切换到不同的主题。

在ConfigProvider组件中,我们将当前主题作为theme属性传递进去,从而实现动态修改ant-design组件的主题。

CSS变量动态同步自定义组件与ant-design组件主题样式 theme无法覆盖自定义组件样式的问题

实践过程中,我们会发现一个问题。

theme配置的影响范围只包括ant-design组件库中的组件,我们自定义的组件,或者我们自行编写的样式文件是无法直接使用当前主题的样式的。

ant官方给我们提供的解决方案是通过theme.useToken这个hook来动态获取当前下的所有token,然后通过jsx直接绑定在jsx的style属性上,形式如下:

import React from 'react'; import { Button, theme } from 'antd'; const { useToken } = theme; const App: React.FC = () => { const { token } = useToken(); return ( 使用 Design Token ); };

这种解决方案很明显只适用于极少量的样式修改,如果项目中有大面积的自定义样式,这样处理会导致后期难以维护。

为了能优雅地消费这份动态的token,我们自然地能想到两种主流的动态样式方案:

CSS-in-JS CSS变量 CSS-in-JS

Css-in-Js不是一个特定的库或者语言,而是对一系列在Js中编写CSS的方案的统称。常见的Css-in-Js的库有:

Styled Components:这是一个非常受欢迎的 CSS-in-JS 库,它提供了一种将 CSS 样式直接嵌入到组件中的方式。它使用标签模板字符串语法,使得编写和管理组件样式非常方便。 Emotion:Emotion 是另一个流行的 CSS-in-JS 库,它提供了类似于 Styled Components 的功能,但具有更高的性能和更好的开发者体验。它支持多种语法风格,包括对象样式和模板字符串。 CSS Modules:CSS Modules 不是一个纯粹的 CSS-in-JS 库,而是一种在 JavaScript 中使用 CSS 模块化的方法。它允许您将样式文件与组件绑定,以确保样式的局部作用域,并避免全局样式冲突。 styled-jsx:这是 Next.js 官方推荐的 CSS-in-JS 解决方案。它使用类似 CSS 的语法,并通过使用 Babel 插件将其转换为有效的 JavaScript。 Aphrodite:Aphrodite 是一个轻量级的 CSS-in-JS 库,它提供了一种简单的方式来定义和管理组件级别的样式。

ant-design v5内部实现动态主题的核心能力也是基于Css-in-Js方案,不过它并没有使用上述几种方案。

考虑到每次渲染都需要重新序列化style字符串,需要有一个合适的hash缓存方案来避免重复序列化造成的性能损耗。

ant-design官方称其CSS-in-JS方案为“组件级别的 CSS-in-JS”,并且解释了为什么这套方案仅适用于组件库场景,而不适用于应用级项目。详情可以参考官方的这篇博客:

ant.design/docs/blog/c…

CSS变量简介

考虑到目前团队没有成形的CSS-in-JS的方案,而且整体编码风格更倾向于在样式文件(.css或.less)中处理页面样式,最终决定基于CSS变量实现ant组件库与自定义组件间的样式同步。

CSS变量的使用很简单,其实就是把原先CSS的值改为用 var(—-xxx)这种形式替换就行。—-xxx可以在任意时期定义,或者修改,变更后会实时反映在页面样式上。

一个简单的代码示例:

DOCTYPE html> :root { --main-color: red; } #app { --main-color: blue; } h1 { color: var(--main-color); } This is a heading

在这个例子中,我们定义了一个名为--main-color的CSS变量,并将其设置为红色。然后,在#app元素内部,我们重新定义了同名的CSS变量,并将其设置为蓝色。

由于CSS变量具有层级作用域,所以在h1元素中使用var(--main-color)时,它会先查找最近的祖先元素中是否有同名的CSS变量,如果找到则使用祖先元素中的值。因此,h1标签的文本颜色将是蓝色,而不是红色。

如果你想通过JavaScript实时修改CSS变量的值,可以使用setProperty方法。例如,你可以添加以下JavaScript代码来实现动态修改CSS变量:

const app = document.getElementById('app'); app.style.setProperty('--main-color', 'green');

这段代码将会把--main-color的值修改为绿色。这样,页面上的标题文本颜色会立即改变为绿色。

通过CSS变量同步ant-design组件与自定义组件的样式

这里的核心是要实现一个自定义的组件,并包裹所有需要同步样式的DOM元素。

import React from "react"; import { Card, ConfigProvider, ThemeConfig, theme } from "antd"; import AntdComponentSample from "@/app/components/AntdComponentSample"; import CssVariableSetter from "@/app/components/CssVariableSetter"; import "./styles.css"; const levelTwoTheme: ThemeConfig = { algorithm: theme.defaultAlgorithm, inherit: false, token: { colorPrimary: "#EAB75A", colorBgBase: "#C8EADC", }, }; const LevelTwo: React.FunctionComponent = () => { return ( {/* 用于同步样式的核心组件,放置于ConfigProvider内部,并包裹所有需要同步样式的元素 */} LevelTwo ); }; export default LevelTwo; 与useCssVariables的核心逻辑

这个组件的核心是渲染传入其中的children,并运行一个自定义hook useCssVariables。

CssVariableSetter源码:

import useCssVariables from "./useCssVariables"; import React, { FC } from "react"; const CssVariableSetter: FC = ({ isGlobal, children }) => { // 执行自定义hook进行CSS变量的生成与写入,并获取此层级对应的容器id // 本次生成的CSS变量只会影响该容器下传入的children中的元素样式 const { cssVarContainerID } = useCssVariables(isGlobal); // 如果声明此次同步是全局生效的,则可以不需要生成一个实际的容器div,用fragment包裹即可 if (isGlobal) { return {children}; } return ( { if (el) { el.id = cssVarContainerID; } }} > {children} ); }; export default CssVariableSetter;

useCssVariables将当前所处的下的ant design token映射为CSS变量字符串,写入一个对应的style标签。

useCssVariables源码:

import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { theme } from "antd"; import { generate } from "@ant-design/colors"; // 可以基于项目需要的规则,过滤出自己想要用的token const filterTokenKey = (tokenName: string) => { return ( tokenName.startsWith("color") || tokenName.startsWith("control") || tokenName === "boxShadow" ); }; // 生成随机字符串 function generateRandomString(length: number) { let result = ""; const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const charactersLength = characters.length; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } const useCssVariables = (isGlobal?: boolean) => { const [cssVarContainerID] = useState( `css-var-container-${generateRandomString(6)}` ); const styleEleId = useMemo(() => { return isGlobal ? "style-is-global" : `style-${cssVarContainerID}`; }, [cssVarContainerID, isGlobal]); // 获取所处最近的下的所有的design tokens const { token } = theme.useToken(); const setCssVariables = useCallback(() => { console.log('setCssVariables token= /n', token); let styleEle = document.getElementById(styleEleId) as HTMLStyleElement; const isAlreadyHaveStyleEle = Boolean(styleEle); if (!isAlreadyHaveStyleEle) { // 如果当前的themeContainer没有对应head中的style标签 // 则新建一个,并加入head styleEle = document.createElement("style"); styleEle.id = styleEleId; styleEle.setAttribute("type", "text/css"); document.head.append(styleEle); } // 创建css变量字符串 let cssVariablesString = ""; // 遍历antd的token并为每一个值添加对应的css变量 const colorTokenArr = Object.keys(token) .filter(filterTokenKey) .map((tokenName) => ({ tokenName, tokenValue: token[tokenName as keyof typeof token], })); colorTokenArr.forEach(({ tokenName, tokenValue }) => { cssVariablesString = cssVariablesString.concat( `--${tokenName}: ${tokenValue};` ); // 如果是全局的configProvider,则还需要添加对应的--global-xxxx // 使整个项目的任何位置都能引用到此css变量 if (isGlobal) { cssVariablesString = cssVariablesString.concat( `--global-${tokenName}: ${tokenValue};` ); } }); // 中性色 // 只需要写入全局的style中即可 // 所有configProvider下用到的变量都是一致的 if (isGlobal) { const colorGroupNeutral = generate("#c7c7c7"); colorGroupNeutral.forEach((color, index) => { cssVariablesString = cssVariablesString.concat( `--neutral-color-${index}: ${color};` ); }); } // 定义这些css变量生效的作用域(css选择器) const styleRootSelector = isGlobal ? ":root" : `#${cssVarContainerID}`; // 将选择器与css变量字符串拼接 cssVariablesString = `${styleRootSelector} { ${cssVariablesString} }`; // 将完整的css变量(包括选择器)内容写入对应的style标签 styleEle.innerHTML = cssVariablesString; }, [cssVarContainerID, isGlobal, styleEleId, token]); const clearCssVariables = useCallback(() => { let styleEle = document.getElementById(styleEleId) as HTMLStyleElement; styleEle.remove?.(); }, [styleEleId]); useEffect(() => { setCssVariables(); // 组件销毁时,清除对应的style标签 return () => { clearCssVariables(); }; }, [clearCssVariables, setCssVariables]); return { cssVarContainerID, }; }; export default useCssVariables;

这样一来,页面的下就会塞入一个形如下图的标签:

Untitled 3.png

同时,任何位于 #css-var-container-0TBXHm 容器下的(也就是上文中下的)元素,都能消费这些token :

Untitled 4.png

总结

至此,我们已经完成了基于ant-design和CSS变量实现前端动态主题的完整实践过程。

完整的实践流程可以总结如下:

通过ant-design主题编辑器调配theme config JSON 传入theme config 将需要同步样式的自定义组件元素放置于组件下 theme.useToken()获取当前下所有的token, 将token映射为CSS变量,写入下的标签


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有