我所知道的动态换肤方案有两种,一种是通过使用 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 https://github.com/mzohaibqc/antd-theme-generator/issues/41#issuecomment-768734824 - 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 插件】: https://github.com/alitajs/plugin-theme
【从 umi.css 中生成 less 文件】 : https://github.com/alitajs/umi-theme-generator
【webpack 插件,主要作用是取到 umi.css 文件】: https://github.com/alitajs/umi-theme-webpack-plugin
参考链接
【Ant Design Runtime Theme Update 】: https://github.com/ant-design/ant-design/issues/10007
【antd-theme-generator】 : https://github.com/mzohaibqc/antd-theme-generator
【antd-theme-webpack-plugin】: https://github.com/mzohaibqc/antd-theme-webpack-plugin