手撸原生 CSS 变量,给 Vant2 实现动态主题与暗黑模式
在需求开发中如果需要大规模替换样式,例如:将主题颜色从蓝色改为橙色或绿色,亦或是需要添加日间、夜间主题模式,以供用户动态切换。这个时候将其一个个覆盖起来也许并不是一个好主意。
目前前端实现定制主题有三种主流方式:
- 通过 sass-loader 或 less-loader 覆盖 less 或 sass 变量,重新编译;
- 通过 CSS 变量设置;
- CSS-in-JS
巧的是,本人接手的老项目用的是 Vant2.0,只支持第一种方案,而目前 Vant4.0 已支持第二种方案,所以我们刚好借此看看方案一是如何操作的,并且让 Vant2.0 也能支持方案二。
方案一:通过 Less 变量
组件库会使用 Less 或 SCSS 对样式进行预处理,并内置一些样式变量,所以通过替换样式变量即可定制自己需要的主题。
查找 less 变量文件
其实不光 Vant,Element、Antd(5.0 以上除外) 等主流组件库都有自己默认的样式变量,我们去它的配置文件中就能看到:
第一步要做的就是从文件中找到自己想要变更的颜色,这里我们尝试将按钮的默认绿色改为红色,主要针对以下三种变量:
{
'button-primary-color': '#fff';
'button-primary-background-color': '#E4004F';
'button-primary-border-color': '#E4004F';
}
配置 less-loader
Less 提供 modifyVars,该方法可以在不重新加载 less 文件的情况下,重新编译 less 文件,达到修改运行时中 less 变量的目的。
我们在 webapck 文件中修改 less-loader
的配置:
module.exports = {
rules: [
{
test: /\.less$/,
use: [
// ...其他 loader 配置
{
loader: 'less-loader',
options: {
// 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
'button-primary-color': '#fff';
'button-primary-background-color': '#E4004F';
'button-primary-border-color': '#E4004F';
},
},
},
},
],
},
],
};
重启项目编译后,会发现绿色变成了我们想要的红色。
如果要修改的颜色较多,也可以单独写在一个文件里:
// src/style/theme/vant-vars.less
@button-primary-color: #fff;
@button-primary-background-color: #E4004F;
@button-primary-border-color: #E4004F;
然后将之前的属性值换成文件路径,名字随便取(比如就叫 'hack')。
{
loader: 'less-loader',
options: {
lessOptions: {
modifyVars: { hack: 'true; @import "@/style/theme/vant-vars.less"' },
},
},
},
值得一提的是,这种写法其实在 Less 官方文档里并不存在,但是却出现在了 Vant2.0 的官方文档中,有意思的是路径中的 true
也可以忽略不写,但是路径前的 ;
却不能省略,否则会报错。
究其原因,这种写法应该是利用了 modifyVars
的特性,通过创建一个假的变量 "hack",在编译的样式中任意注入导入的主题,有兴趣的同学可以查看 less 的解析器
优缺点
这种方案比较简单,对于需要大批更换主题色的需求,此方案已绰绰有余,只需每次通过修改 SCSS 文件并重新编译就能实现。
但也正因如此,这种方案只能做到一次性修改,如果想继续覆盖,只能再重新编译,而且每次只能固定展示一种颜色,无法做到动态切换。
方案二:通过 CSS 变量
CSS 变量是一个非常有用的功能,几乎所有浏览器都支持。 (IE:啊这?)
这意味着你可以动态的改变组件内的个别变量,以便更好地自定义组件样式,而不需要修改 SCSS 文件重新编译。
打开 Vant4.0 的官网,找到 HTML 根标签,可以看到已声明的 CSS 变量(或者查看源码样式文件):
先来介绍下其中的 :root
和 CSS 变量。
:root
:root
是 CSS 伪类元素,用来匹配文档树的根元素,对 HTML 来说,:root
就代表着 <html>
元素,但是它的优先级要更高一些,所以在声明全局的 CSS 变量时,:root
就会很有用。
你甚至可以连写两个 root,就像这样:
/* 添加这段样式后,Primary Button 会变成红色 */
:root:root {
--van-button-primary-background: red;
}
这是因为 Vant 中的主题变量也是在 :root
下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。通过 :root:root
可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。
CSS Variables 和 var() 函数
CSS 变量是由开发者自己定义的,属性名必须带有前缀 --
。比如 --main-color: black
,表示的是带有值为 black
的自定义属性,可以通过 var() 函数在全文档范围内复用。
CSS 变量受级联的约束,并从其父级继承其值,每一个CSS 变量可以多次出现,并且变量的值将会借助级联算法和自定义属性值运算出来。
:root {
--text-color: #488cff;
}
p {
color: var(--text-color);
}
在上述示例中,如果我想修改 <p> 标签内字体的颜色,只需要修改变量 --text-color
就可以了,比如刚刚提到的连写 root 提高优先级。
:root:root {
--text-color: pink;
}
如果你只想自定义一个特定的组件,只需为这些组件单独添加内联样式。
<p style="--text-color: red">蓝屏的钙</p>
出于性能原因,对于单个特定组件,更加推荐在类名下添加自定义 css 变量,而不是在全局的 :root
下。
p {
--text-color: blue;
color: var(--text-color)
}
这三种方法都能达到覆盖 CSS 变量的目的。
step 1:配置 CSS 变量
接下来,我们着手来实现自己的 CSS 变量。首先新建一个 css-vars.css
文件:
// src/style/theme/css-vars.css
:root {
/* Color Palette */
--g-white-1: #fff;
--g-red-1: #e4004f;
/* Component Colors */
--g-primary-color: var(--g-red-1);
}
这里面定义了两种变量 —— “基础变量” 和 “组件变量”。其中,组件变量继承基础变量。这样的好处是既可以直接修改基础变量影响所有相关组件,也可以有针对地修改某个组件变量而不造成全局变量污染。
step 2:替换 Vant2.0 组件
Vant2.0 组件并没有使用 CSS 变量,所以我们要将组件中的颜色统统替换成自己的 CSS 变量,新建一个 vant.scss
文件:
// src/style/vant.scss
.van-button--primary {
color: var(--g-white-1) !important;
background-color: var(--g-primary-color) !important;
}
在 main.js
入口文件中导入这两个文件,并重启服务。
// main.js
import './style/theme/css-vars.css';
import './style/vant.scss';
可以看到,按钮的颜色已经变成了我们自己的变量了!通过这种方式,我们可以将更多的组件加进来,设置成自己喜欢的颜色。
step 3:setProperty 动态控制 CSS 变量
其实在介绍 CSS 变量时,已经提及了一种控制 CSS 变量的方法,就是在类名下直接自定义 CSS 变量进行覆盖。你可以使用一个布尔值去控制类名的添加与移除,来达到这个目的。
还有一种方式就是通过 JS 控制 CSS 变量,使用 setProperty
方法直接设置一个新的值。
<template>
<van-button block @click="changeColor('blue')"> 变蓝 </van-button>
<van-button block @click="changeColor('green')"> 变绿 </van-button>
<van-button id="login-btn" type="primary" block> 登录</van-button>
</template>
export default {
methods: {
changeColor(color) {
const dom = document.getElementById('login-btn');
dom.style.setProperty('--g-primary-color', color);
}
}
}
这里演示的是对单个组件的针对性修改,打开控制台你会发现 --g-primary-color
变量只在这个按钮身上发生了变化,而全局的变量依旧是红色,这样我们就可以将颜色动态切换的精度控制在单个组件内。
如果你想修改所有 primary 按钮组件的颜色,那就直接给 HTML(documentElement)修改即可。
// app.vue
document.documentElement.style.setProperty('--g-primary-color', color);
大到动态控制全局组件样式,小到自定义单个组件样式,CSS 变量都能灵活胜任,还是很香的。
添加暗黑主题模式
了解了 CSS 变量的使用,我们接下来给项目提供日间和暗黑(夜间)两种主题,供用户动态切换。
所谓模式,就是一套配色。比如日间就是白底 + 黑字,但是到了夜间就得黑底 + 白字。所以 background-color
和 color
这俩属性就构成了一套最简单的配色,用户在一键点击时,就需要同时更换这两个属性。
目前主流的做法就是单独写一套暗黑配色,通过类名 dark
直接挂到 HTML 元素上,所以切换主题就是在切换 dark
类名。
step 1:准备 drak 配色
先准备一套暗黑风格的配色(颜色你们自己定义,或者直接问 UI 要),覆盖默认的 CSS 变量:
// src/style/theme/dark.scss
html.dark {
--g-background: var(--g-black-2);
--g-text-title-color: var(--g-white-2);
--g-input-background: var(--g-black-3);
--g-danger-color: #ff4d4f;
--g-text-gray-color: var(--g-gray-2);
--g-text-primary-color: var(--g-gray-2);
// 覆盖用到的 Vant2 组件的默认样式
.van-popup>button.van-share-sheet__cancel::before {
background-color: var(--g-black-1)
}
}
在 main.js
入口文件中添加。
// main.js
import './style/theme/css-vars.css';
import './style/vant.scss';
import './style/theme/dark.scss';
step 2:classList 切换 dark 类名
处于安全考虑,推荐使用 document.documentElement.classList 的内置方法给 HTML 动态添加和移除 dark
类名,而不是直接通过捕获 HTML 修改其 className
,因为 HTML 上可能还有其他的类名,如果直接使用 className
就会将之前的类名全部覆盖,这一点需要注意。
darkThemeChange(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
},
Antd 的 CSS-in-JS
最后一种方案就是 CSS-in-JS
, Antd5.0 版本就是采用的这种方式。不过本人能力有限,还没完全吃透这个技术方案,就不深入分析了,这里就简单和大家聊聊,等在下研究透了再来和大家分享😬
什么是 CSS-in-JS
在 5.0 版本的 Ant Design 中,我们提供了一套全新的定制主题方案。不同于 4.x 版本的 less 和 CSS 变量,有了 CSS-in-JS 的加持后,动态主题的能力也得到了加强。
- 支持动态切换主题;
- 支持同时存在多个主题;
- 支持针对某个/某些组件修改主题变量;
- ....
看上去很高大上有木有?其实 CSS-in-JS 本身只是一种操作方式,比如在 React 中,我们就是在用 JavaScript 写 HTML 和 CSS。
const style = {
color: 'red',
fontSize: '46px'
}
上述方式就是 React 对 CSS 的一种简单封装。但是由于 CSS 的封装非常弱,或者说功能不够强大,于是就出现了一系列的第三方库,用来加强 React 的 CSS 操作。它们统称为 CSS in JS
。意思就是使用 JS 语言去写 CSS。比如 styled
的写法,就是由 styled-components 首创:
import styled from 'styled-component';
import { List } from 'xxx';
// 创建样式组件
const StyledList = styled(List)`
border: 1px solid ${(p) => p.theme.colorPrimary};
border-radius: 2px;
box-shadow: 0 8px 20px ${(p) => p.theme.colorShadow};
`;
const App: FC = ({ list }) => {
return (
// 引用组件
<StyledList dataSource={list} />
);
};
主流 CSSinJS 库基本都支持这种写法。
Ant Design Style
由于 CSS in JS 的写法过多,所以我们需要给出一种最佳实践的写法,能兼容 V5 的 Token System、自定义主题、较低的研发心智和良好的扩展性。
在 Antd 看来,社区目前主流的几款 CSS in JS 多多少少都存在一些弊端,于是便推出了自己的最佳解决方案 —— Ant Design Style。
antd-style 内置了 @emotion/styled
作为 styled 语法的样式引擎,选择了 emotion
作为 css 语法的样式引擎,有兴趣的小伙伴可以看看它的设计理念与实施策略。
antd-style 通过在容器组件 ThemeProvider
上修改 apperance props
,即可实现主题切换,这是也是动态主题最简单的使用方式。
import { ThemeProvider } from 'antd-style';
export default () => {
return (
// 自动变为暗色模式
<ThemeProvider apperance={'dark'}>
<App />
</ThemeProvider>
);
};
总结
文本主要分享如何定制主题,以及定制主题的两种主流方案。
第一种方案通过配置 less-loader 修改 less 变量以达到修改主题色的目的,但是无法做到动态切换;
第二种方案就是使用原生 CSS 变量。通过修改 CSS 变量,我们实现了动态修改颜色,并且针对不同的修改对象,可以控制修改的覆盖范围,大到全局组件,小到单个组件,都能精准控制。
随后,我们给出了目前主流的暗黑(夜间)模式解决方案,即通过 HTML 切换类名的方式去匹配一整套的暗黑配色。
最后,我们简单聊了聊 CSS-in-JS 技术方案,关于这部分内容,打算后续深入了解后再和大家分享,敬请期待哈~😃
参考资料
转载自:https://juejin.cn/post/7262998095172878397