我所知道的动态换肤方案有两种,一种是通过使用 less.modifyVars 修改 less 变量实现,一种是使用 var css 实现,由于 var css 很多浏览器都不支持。
而且我们项目的主要场景在于移动端,所以能选择的就只有通过 less 实现的方案了。
原理简述
原理其实很好理解,将包含 less 变量的 css 类提取出来,通过修改变量的值重新生成新的 css 类,再添加到 dom 中。 如在项目代码中,编写样式如下:
@abcd-efg : #f3574c;
.center {
color: @abcd-efg;
font-size: 26px;
height: 50px;
}
在框架中编译之后,会产生这样的 css ,(如 umi.css):
.center {
color: #f3574c;
font-size: 26px;
height: 50px;
}
正常不需要动态换肤的场景下,这就是我们最终需要的 css 样式。 如果需要动态换肤,那我们就可以保留下这些 less 变量,重新生成一个 less 文件(假设命名是 alita.less)。
.center {
color: #f3574c;
font-size: 26px;
height: 50px;
}
@abcd-efg : #f3574c;
.center {
color: @abcd-efg;
}
然后 less.js 会将这个 less 文件转化成 css 文件,放到 dom 上。
最终我们部署的 html 文件大致如下:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/umi.css" />
</head>
<body>
<link rel="stylesheet/less" type="text/css" href="/alita.less" />
<script src="less.js"></script>
</body>
</html>
当用户访问页面时,在浏览器端,less.js 将 dom 中的 less 文件编译成 css 样式,html 变化大致如下:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/umi.css" />
<!-- 这里解析之后是
.center {
color: #f3574c;
font-size: 26px;
height: 50px;
} -->
</head>
<body>
<link rel="stylesheet/less" type="text/css" href="/alita.less" />
<style type="text/css" id="less:alita">
/* 这里就是上面的 less 文件编译而成 */
.center {
color: #f3574c;
}
</style>
<script src="less.js"></script>
</body>
</html>
当我们在项目中使用 less.modifyVars 修改变量会触发 less 文件重新编译。
window.less.modifyVars({
'abcd-efg': '#0000FF'
})
<style type="text/css" id="less:alita">
/* 这里就是重新生成的 css 样式 */
.center {
color: #0000FF;
}
</style>
然后理解了原理,那么接下来就是实现了。
实现
变量提取
看了一些实现,还是觉得 antd 的官网的实现最为靠谱(Ant Design Runtime Theme Update #10007),并且 mzohaibqc 已经写了一个很好的工具 (antd-theme-generator),看了下源码,里面写死了 antd 的目录路径和变量名称,但是提供的方法基本上都可以使用,因为我需要的是移动端的方案,即需要的是 antd-mobile,好在 antd-mobile@2 的结构和 antd 基本上一致,通过简单修改之后,就可以使用。
但是发现一个问题,变量修改只能够修改 antd-mobile 组件中的变量,并无法修改项目中用到的变量名。
import { Button } from 'antd';
import styles from './index.less';
// index.less
// @import '~antd-mobile/lib/style/themes/default.less';
// .center {
// .primary {
// color: @brand-primary
// }
// }
const Page: FC<PageProps> = () => {
return (
<div className={styles.center} >
<Button type="primary" >按钮</Button>
<span className={styles.primary}>这里的颜色,在less文件中使用了主题色的变量。 @brand-primary </span>
</div>
);
}
修改变量之后发现按钮和其他用到主题色的组件都发生了变化,但是在项目中使用一样变量的类却无法改变,
window.less.modifyVars({
'brand-primary': '#FF0000'
})
初次猜测原因可能是自定义的 less 文件没有被编译到。通过查看最终的产物,发现 antd-theme-generator 编译的时候读取到的文件是原始文件,而 umi 项目中使用了 css module 之后,在 css-loader 的时候,类名会被默认加上后缀,导致 less 编译后的类名为 .center 而真实的类名为 .center__kjahd。
因为 umi 生命周期中,并没有一个时机,能够获取到带有 less 变量和类名后缀的文件。因此直接从 webpack 构建环节中,读取了 umi 编译后的 css 文件。 考虑到可能存在按需加载的情况,因此取了所有的 css 文件。
class UmiThemePlugin {
apply(compiler) {
const options = this.options;
compiler.hooks.emit.tapAsync('UmiThemePlugin', (compilation, callback) => {
options.customCss = '';
Object.keys(compilation.assets).map((i) => {
if (i.endsWith(".css")) {
options.customCss = `${options.customCss}\n${compilation.assets[i].source()}`
}
})
generateTheme(options)
});
}
}
module.exports = UmiThemePlugin;
既然 less 转 css 都通过 umi 编译了,那 antd-theme-generator 中就没有必要二次编译了。因此简单的删除了这里面编译 css 文件的内容。
umi 插件
本着框架中做的越多,项目交付中做的就越少的原则,将 antd-theme-generator 文档中要求的,手动引入的文件,和其他需要注意的事项通过 umi 插件的形式实现。 最终完成 @alitajs/plugin-theme,安装完成后,在配置文件中配置使用:
plugins:['@alitajs/plugin-theme'],
dynamicTheme:{
type:'antd-mobile',
varFile: path.join(__dirname, '../src/default.less'),
themeVariables: ['@brand-primary','@abcd-efg'],
}
| 属性 | 说明 |
|---|---|
| type | 声明是 antd 还是 antd-mobile ,会自动找到包的路径 |
| varFile | 声明 less 变量的文件路径,未提供的话,会默认找到 'style/themes/default.less' |
| themeVariables | 需要提取的变量名,需要显示指明,才能在修改变量时使用,因为需要修改的变量越多,生成的 less 文件越大 |
遗留的问题
-
less 版本问题
引入less@2.7 的用 window.less.modifyVars 的方式可以。但是在项目中使用了less@3 ,使用 import less from less; less.modifyVars 的方式,就算 javascriptEnabled 设置为 true ,也是不能使用.bezierEasingMixin();
相关 Issues github.com/mzohaibqc/a…
-
less 变量的值必须唯一
由于使用的 css 是由 umi 编译后的文件,中间未记录 less 变量,后续动作是采用值匹配来做反向绑定的。
缺点就是如果两个变量名都指明了同一个颜色值,最终会被合并为一个。
好处是就算在项目中写色值的时候忘记使用变量,也可以实现动态换肤,这对于遗留项目的功能跟进有着极大的好处。
适用的项目
理论上所有 umi 系的项目都可以使用,比如 umi、dumi、ant-design-pro、alita 等。目前测试了 umi 、alita 和 ant-design-pro 的项目。
闭眼测试 ant-design-pro
- 拉取当前 v5 分支代码
- yarn add @alitajs/plugin-theme
- config/config.ts 中添加配置
- src/pages/User/login/index.tsx 中,随便写了一个按钮
config/config.ts
export default defineConfig({
+ plugins: ['@alitajs/plugin-theme'],
+ dynamicTheme: {
+ type: 'antd',
+ themeVariables: ['@layout-body-background'],
+ },
});
src/pages/User/login/index.tsx
+ <Button type="primary" onClick={() => {
+ window.less.modifyVars({
+ 'layout-body-background': '#FF0000'
+ })
+ }}>点击背景色改变</Button>
随意的效果
总结
我的水文总是不能缺了总结,这个方案还是挺有趣的,跑方案的时候,发现很多有趣的问题,写文章的时候,倒觉得都挺简单的了。这个方案从开始收到项目组需求到最终可用,总共花了4天时间,新发了三个包。
欢迎大家试用,欢迎讨论。
源码
【umi 插件】: github.com/alitajs/plu…
【从 umi.css 中生成 less 文件】 : github.com/alitajs/umi…
【webpack 插件,主要作用是取到 umi.css 文件】: github.com/alitajs/umi…
参考链接
【Ant Design Runtime Theme Update 】: github.com/ant-design/…
【antd-theme-generator】 : github.com/mzohaibqc/a…
【antd-theme-webpack-plugin】: github.com/mzohaibqc/a…