通常多主题的方案应用的场景就是常见的换肤功能,CSS 预编译工具 Sass/Less 等流行的当下,去定制多套主题变量似乎并不是什么难事。另外在工程化的现今,如何结合基础组件的主题能力和工程构建工具来降低多主题应用的成本呢,下面将通过不同场景和思路来一探多主题的实现方案。
CSS 样式控制
要实现多主题供切换,脑中的第一反应就是准备多套 CSS ,步骤上也相对简单:
- CSS 中定义不同主题下样式内容
- 通过 JS 控制切换
body
标签上的类名
一个简单的 demo:
.theme-green .container {
background-color: green;
}
.theme-red .container {
background-color: red;
}
当然实际的项目开发中自然不能这样一句句的重复写 CSS,在工程化的现今可以借助 CSS 预编译器的能力来降低样式开发和维护的成本,以 Sass 为例:
// 定义不同主题的变量
$themes: (
theme-green: (
bg-color: green;
),
theme-red: (
bg-color: red;
),
);
// 循环输出上述定义主题的样式
@mixin theme($properties, $key) {
@each $theme in map-keys($themes) {
.#{$theme} & {
@each $property in $properties {
#{$property}: map-variable-get($themes, $theme, $key);
}
}
}
}
// 获取主题下对应变量的内容
@function map-variable-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
// 具体样式时使用,最终编译结果同上述简单demo
.container {
@include theme(background-color, bg-color);
}
方案很简单,将多主题的样式都放在来同一处代码中,非常适合主题切换的功能,但带来的缺陷也是非常的明显:
- 虽然可以通过上述 Sass 预编译能力,简化了多主题带来的样式复杂度,但最终样式文件会统一打包,生成多套css 内容,样式文件的大小将呈倍数增长
- 样式内容上可能会存在冗余
编译多份 CSS 文件
为了解决上述提到的问题,其实可以通过工程工具的方式将主题样式进行区分和切换,然后再通过多个主题样式文件间切换来实现多主题,实现步骤上如下:
- 创建主题文件,内容中包含了主题切换相关的变量定义
- 工程工具根据不同的配置内容,编译生成不同主题的 CSS 文件
- 通过替换页面加载指定
css
文件实现主题切换
创建主题文件
还是以 Sass 为例,在源码目录下创建 theme 文件夹以便于管理主题文件:
|—src
| |—theme
| | |—theme-1.scss
| | |—theme-2.scss
主题文件中包含了样式所需的主题变量:
// color
$primary-color: #5584FF;
// size
$font-size: 12px;
在对应样式文件中使用主题变量:
工程工具将根据配置信息决定 sass 文件中注入的主题变量,在使用变量时无需显示地 import 对应主题文件
// import 主题文件有工程工具完成,直接在 scss 中使用主题定义变量
.button-bg-color {
background-color: $primary-color;
}
编译多套主题
解决编译多套主题问题,本质上在于能够结合工程输出多份 CSS 文件,借助 webpack 的能力可以对样式主题进行注入,已经 CSS 文件的打包:
const themeFiles = ['theme-1', 'theme-2'];
module.exports = themeFiles.map((themeFile) => {
return {
...
module: {
loaders: [
...
{
test: /.scss$/,
use: [
// 省略默认loader
...
{
// 注入主题 scss,以便在 scss 文件中直接使用主题变量
loader: 'sass-loader',
options: { data: `@import "./src/theme/${themeFile}.scss";`)}
}
]
}
],
plugin: [
...
new miniCssExtractPlugin({
filename: `${themeFile}.css`
}),
],
}
};
});
在 webpack3 中,可以借助 extract-text-webpack-plugin 的能力开启多个实例,来构建不同配置的 scss 规则,但是在 webpack4 开始推荐使用mini-css-extract-plugin,而它并不支持多实例的构建方式,只能通过多份不同配置的 webpack 进行构建 - 即利用 multiple configurations 的能力构建。
多份 CSS 解决了单文件打包多主题存在的冗余和体积的问题,但这个方案仍会给开发带来一些限制和问题:
- 多份主题,可能意味着多份配置多次构建,除 CSS 文件外,其他内容存在冗余构建
- 所有 CSS 样式只能已单文件形式打包,不能存在 css splitChunks,splitChunks 会在 js 脚本中注入加载 css chunk 代码,从而导致多次构建 js 内容不一致的情况
动态多主题方案
项目开发过程中,除了项目本身依赖的样式之外,大部分开发过程中还会依赖基础组件库。而这部分样式也是需要考虑主题的统一切换。比如 Fusion 组件,它可以通过 Fusion 平台的主题配置能力,可视化定制组件的主题样式,并生成对应的主题包(npm 包)。
结合组件的主题包,多主题的切换的需求可以归结为主题包的相互间切换,而最终的效果希望能够达到以下几点:
- 主题包样式同时作用于项目样式和基础组件样式
- 配置简化,不引入多主题重复的样式内容
- 工程上提供开发和构建时一致的主题切换体验,不增加额外构建流程
基于上面方案的尝试,项目中希望能有更加优雅便利的方式来实现多主题,那 ice-scripts
作为飞冰(ICE)团队提供的 React 链路的工程工具给出来怎样的解决方案?
ice-scripts
结合 Fusion 基础组件的主题包配置能力,通过工程构建的手段实现多主题动态切换:
- 提取主题包中的 scss 变量
- 将 scss 变量具体内容转换为 css 变量
- 将新生成的 scss 变量值注入到对应的 scss 文件中进行编译(包括项目 scss 样式和 Fusion 基础组件)
- 提供主题切换方法,实现不同主题包间的切换
CSS 变量
在开始详细方案的介绍之前,我们需要准备些预备知识,了解 CSS 变量的基础用法。
一提到样式的变量,多数人的第一反应都是 LESS、SASS 等预编译语言,但实际上原生的 CSS 也已经在大多数浏览器中支持变量。
在 CSS 中原生的变量定义语法是 --*
,变量的使用语法是 var(--*)
,声明变量的时候,变量名前面要加两根连词线 --,各种值都可以放入 CSS 变量:
:root {
--bg-color: #E5E8ED;
--height: 100px;
--position: center;
}
使用变量时,通过 var()函数读取变量:
.use-style {
background-color: var(--bg-color);
height: var(--height);
text-align: var(--position);
}
这便是动态切换主题最基础的依赖,接下来要做的就是借助工程工具来简化主题变量的编译和提取。
主题变量提取和变换
ice-scripts
中支持通过配置主题包的方式来加载多份主题文件,对应配置文件设置:
module.exports = {
plugins: [
['ice-plugin-fusion', {
// 通过数组方式配置多主题包
themePackage: [{
name: '@icedesign/theme',
// 设置默认加载主题,如果不进行设置,默认以最后添加主题包作为默认主题
default: true,
// 设置自定义主题颜色,可以在 scss 文件中直接使用该变量,比如: .bg-color { background: $custom-color; }
themeConfig: {
'custom-color': '#000',
},
}, {
name: '@alifd/theme-ice-purple',
themeConfig: {
'custom-color': '#fff',
},
}],
}]
]
}
主题包通过 npm 包的形式引入,比如 @icedesign/theme 中 scss 变量内容均设置在 variables.scss 文件中,除了主题包中的定义工程配置上同时也支持了自定义的 scss 变量(通过 themeConfig
指定 ),工程上通过正则规则 /$[w-]+?:.+?;/g
将主题包变量内容提取,结合自定义变量按指定的规则生成新的 scss 变量和 css 变量。
变量注入和编译
完成主题变量的提取和变换之后,需要借助 webpack loader 的能力将新生成的 scss 变量和对应的主题文件注入到对应的 scss 文件中进行编译(通过 scss 变量优先级规则实现覆盖)。
通过启动多主题方案构建先后内容的对比,可以发现注入变换后生成的 CSS 内容中均是以使用 CSS 变量的形式出现。
主题切换方法
新生成的 css 内容在没有定义任何 css 变量时,主题内容并不会生效,工程上可以简化切换的逻辑,直接将默认将主题切换的方法 __changeTheme__
挂在到 window 下,通过指定对应主题包在 head 中插入指定的 css 变量内容,以达到动态切换主题的效果:
<style>
:root {
--color-brand1-1: #E2EDFF;
--color-brand1-2: #FFFAAA;
}
</style>
项目中需要切换主题时,只需要调用__changeTheme__('@icedesign/theme')
即可以完成主题的切换。
总结
多主题的实现,依据架构的复杂度,可以很简单也可能会比较复杂,项目中可以借助工程上的能力去屏蔽一些复杂的处理逻辑,提升项目开发过程中的体验。这也正是体现工程化带来的一大优势。
动态多主题的方案,结合了 Fusion 主题配置、CSS 变量和工程构建三者的能力共同实现主题间优雅简便的切换,欢迎大家试用反馈~
相关链接
ice-scripts Github地址
主题配置文档