如何在 umi 系项目中实现动态换肤

3,705 阅读5分钟

我所知道的动态换肤方案有两种,一种是通过使用 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 文件越大

遗留的问题

  1. less 版本问题

    引入less@2.7 的用 window.less.modifyVars 的方式可以。但是在项目中使用了less@3 ,使用 import less from less; less.modifyVars 的方式,就算 javascriptEnabled 设置为 true ,也是不能使用.bezierEasingMixin();

    相关 Issues github.com/mzohaibqc/a…

  2. less 变量的值必须唯一

    由于使用的 css 是由 umi 编译后的文件,中间未记录 less 变量,后续动作是采用值匹配来做反向绑定的。

    缺点就是如果两个变量名都指明了同一个颜色值,最终会被合并为一个。

    好处是就算在项目中写色值的时候忘记使用变量,也可以实现动态换肤,这对于遗留项目的功能跟进有着极大的好处。

适用的项目

理论上所有 umi 系的项目都可以使用,比如 umi、dumi、ant-design-pro、alita 等。目前测试了 umi 、alita 和 ant-design-pro 的项目。

闭眼测试 ant-design-pro

  1. 拉取当前 v5 分支代码
  2. yarn add @alitajs/plugin-theme
  3. config/config.ts 中添加配置
  4. 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…