需求背景
在今年的项目中,根据客户的需要,需要在原本亮色的主题下,适配暗色主题,并提供可拓展的空间。当前需要改造的是基于新一代架构的配电房系统和综合能源系统,技术栈为Vue2,使用的组件库为bootstrap Vue,以及少量的element UI和 AntD Vue组件,涉及的组件库较多,改造较复杂。
方案研究
方案1:使用CSS3的CSS变量
特性。在不同的作用域下定义浅色和暗黑两套不同的CSS变量
,通过JS控制html根部的class属性来切换作用域,以达到切换主题色目的。经兼容性评估,该CSS变量
特性兼容至chrome49+,即2017年后的主流浏览器均已支持该特性,可用于生产实践中。当前主流的组件库都开始采用这种方式和来动态更新主题,但较老的组件库因为使用了大量的less或scss颜色函数预处理,不支持CSS变量
编译,若直接替换定义的颜色为CSS变量
会导致在编译阶段报错,因此只能手动去书写多套样式,需要大量工作量。
方案2:预设多份less或sass变量文件,使用webpack构建提前将所有的样式编译出总的多份css文件,通过切换 css 文件到达目的,但是需要对项目的所有的 less或sass 的引用模式作调整,对构建环境也需很大的调整,样式与 js 完全分离,不能友好地对组件库的的 less 或 sass 按需编译。
实践方案
经过网上方案的进一步研究,已经有@zougt/some-loader-utils
和@zougt/theme-css-extract-webpack-plugin
两个工具插件,结合方案1和方案2,利用插件来辅助编译出两套css主题代码。实践过程大体分为以下几个步骤:
3.1 定义CSS变量,替换自定义组件和页面中的固定的颜色值
由于项目主要使用scss为css预编译语言,我们主要以scss语法来定义css变量,采用之前的方案1,我们创建一个variable.scss
文件,分别定义浅色和深色作用域下的变量:
:root {
--theme-color: #1b85ff; //主题色,用于按钮、文字、链接、图标等
--warning-color: #f9ac30; //警告提示色,用于警告、提醒等
// …
}
.dark {
--theme-color: #6cecff; // 主题色,用于按钮、文字、链接、图标等
--warning-color: #fcd451; // 警告提示色,用于警告、提醒等
// …
}
我们对整个项目里所有原有固定写死的色彩CSS属性逐一检查,将其替换为CSS变量。例如,Vbox组件中,CSS将原本固定写死的title颜色#1b85ff
,修改为动态的var(--theme-color)
,那么在默认作用域下颜色为#1b85ff
,.dark
作用域下为#6cecff
。
.title {
color: var(--theme-color);
}
3.2 使用JS动态切换全局样式
此时,若根html DOM上无额外类名,浅色主题生效,整体页面风格为浅色主题;若在根html DOM上添加类名.dark
,此时暗色CSS变量便会生效,整体页面风格会切换为暗色主题。
我们可以使用JS来控制这个类名的存在,并使用vuex来管理和存储持久化当前的主题模式状态。当点击切换主题按钮时,我们修改vuex中存储的主题模式并切换DOM上的类名,伪代码如下:
function changeTheme() {
// 反转Vuex里存放的主题状态值
if (store.state.system.theme === 'dark') {
store.commit(MUTATION.SET_THEME, 'light')
} else {
store.commit(MUTATION.SET_THEME, 'dark')
}
// 在html DOM 上添加主题色类名
document.documentElement.className = store.state.system.theme
}
3.3 BootstrapVue、AntD组件库的适配
完成了自定义组件的适配,我们还需要对第三方组件库进行适配,BootstrapVue、AntD原生均不支持主题色动态切换,但它们都使用了scss预处理器去定义了主题颜色,因此我们可以通过定义两套变量,在打包过程中生成两套不同作用域下的CSS编译产物,实现主题色切换。
这里我们使用@zougt/some-loader-utils
这个插件来帮助我们生成编译产物,使用@zougt/theme-css-extract-webpack-plugin
来分离出独立的主题 css 文件,以节省加载时间:
插件的配置方法如下,编辑vue.config.js配置文件
const { getSass } = require('@zougt/some-loader-utils')
const ThemeCssExtractWebpackPlugin = require('@zougt/theme-css-extract-webpack-plugin')
// 定义两套主题
const multipleScopeVars = [
{
scopeName: 'light',
path: path.resolve(__dirname, 'src/assets/styles/theme/light.scss'), // 浅色主题变量文件
},
{
scopeName: 'dark',
path: path.resolve(__dirname, 'src/assets/styles/theme/dark.scss'), // 深色主题变量文件
},
]
module.exports = defineConfig({
chainWebpack: config => {
// 使用插件,在打包时将两种主题颜色分别打包进不同的css文件,以实现按需加载主题文件,节约首屏加载时间
config.plugin('ThemeCssExtractWebpackPlugin').use(ThemeCssExtractWebpackPlugin, [
{
multipleScopeVars,
extract,
outputDir: extractCssOutputDir,
},
])
},
css: {
loaderOptions: {
sass: {
// 使用插件,来预处理主题变量文件
implementation: getSass({
getMultipleScopeVars: () => multipleScopeVars,
}),
},
}
}
})
我们的编译插件就配置好了,剩下的就是需要逐一调整颜色变量。这一步需要一些耐心,我们需要阅读组件库源码,找到编译后颜色在源码中所对应的scss变量,并分别在dark.scss和light.scss文件中将其替换掉。例如,我们想修改Bootstrap Vue中 Button组件的字体颜色,通过阅读源码发现,字体颜色使用了$body-color
这个变量,那么我们在主题配色文件中分别将这个变量覆盖定义:
.btn {
//...
color: $body-color; // 找到了源码中按钮颜色的来源,是这个$body-color变量定义的
// ...
}
在浅色主题的light.scss文件中,我们添加一行代码,将这个颜色定义为黑色:
$body-color: #212529;
在深色主题的dark.scss文件中,我们添加一行代码,将这个颜色定义为白色:
$body-color: #fff;
这样,当我们编译后查看打包产物,会发现打包出两份不同作用域下对按钮字体颜色的定义:
.light .btn {
color: #212529;
}
.dark .btn{
color: #fff;
}
这样,和3.2步骤中实现主题切换同理,只需控制根组件html DOM树上的class 为light 或是dark,就可以实现主题色的切换。
3.4 ElementUI 组件库的适配
当前使用的BootstrapVue
组件库和ElementUI
使用了scss 预处理方案,而AntdV
使用了less预处理方案,也就意味着我们项目还需要安装less 预处理器,引入AntdV
的less文件而不是原先的css,使用编译方式生成AntdV
组件样式,这样我们获得了进一步对主题色修改的空间。
由于@zougt/some-loader-utils插件的局限性,无法同时预处理scss和less,无法像3.3步骤那样对less分别定义两套变量文件进行覆盖,但实际发现ElementUI
并未使用太多颜色函数,所以我们直接将less变量定义覆盖为CSS变量即可,实践方案类似于3.1和3.3步骤的结合:
@import '~ant-design-vue/dist/antd.less'; // 引入官方提供的 less 样式入口文件
// 修改antd的主题变量
@text-color: var(--subtitle-color);
@tree-showline-icon-color: var(--subtitle-color);
// ...
实践总结
在完成多主题色切换的实践过程中,我们采用了CSS变量和覆盖SCSS或Less变量编译生成两套css文件两种方案的结合,并借助一些插件工具来简化我们手动编码的过程。