antd 系动态切换主题实践,含 IE 兼容方案

简介

动态切换主题通常指的更多的是运行时切换页面主题的功能,比如用户通过点击页面中的一个按钮来达到几种预制主题的切换,这一般都是通过提前将几种预制主题的样式进行独立编译,供用户选择使用。这只是动态主题的基本功能,更进一步的,应该允许用户定制和设计页面中的任何一个组件元素。

动态切换主题的需求一般在"多租户"的场景会是一个比较强烈的需求,可以实现同一套代码,在不同的租户现场又能呈现不同的页面风格的好处。但是在技术实现上却存在不少难点,这个功能常常因为体验太差,或者影响页面性能而遭到用户的吐槽。

很多人第一次了解到这个功能,应该是从 ant-design-pro 项目的主题设置功能开始的。

但是当你想把这个功能用到项目中时,会得到一个提示:"配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件"。

本文整理了如今 antd 系列组件的动态切换主题实践,其中不同的方案存在不同的优点和弊端,请在选择方案时,酌情考虑。

实践

antd-mobile@2.x 使用的技术和 antd@4 基本一致,所以以下以 antd@4 举例的方案,也能用于 antd-mobile@2.x

不支持 IE 是本文中很多方案的共同缺点

antd@5

antd@5 采用的是 cssinjs 的技术实现,所以在实现动态切换主题能力的时候,可以说是最轻松的,也可以说"动态主题"也是推动 antd 到 5 一个很重要的原因。

在 v5 中,动态切换主题对用户来说是非常简单的,可以在任何时候通过 ConfigProvider 的 theme 属性来动态切换主题,而不需要任何额外配置。

ts 复制代码
import { Button, ConfigProvider } from 'antd';
import React from 'react';

const App: React.FC = () => (
  <ConfigProvider
    theme={{
      token: {
        colorPrimary: '#1677ff',
      },
    }}
  >
    <Button />
  </ConfigProvider>
);

export default App;

直接从服务端或者用户修改 antd token 传递给 theme,就能实现动态配置主题了。技术实现上非常的简便,但是很多现有项目多是基于 antd@4 版本构建的,要直接切换到 antd@5 成本有点大,而且需要注意的是 antd@5.0 之后不再支持 IE。

antd@4.17.0-alpha.0 CSS 变量

antd@4 从 4.17.0-alpha.0 通过 CSS 变量支持了动态切换主题的功能,由于 IE 不支持 CSS 变量,因此在 IE 浏览器环境中也无法使用这个方案。

修改全局的 CSS 变量对所有的组件生效

css 复制代码
:root:root {
  --ant-primary-color-hover: #c89deb;
}

也可以指定部分的组件生效,如在 <div className="purple-theme"> 包裹下的组件生效。

css 复制代码
.purple-theme {
  --ant-primary-color-hover: #c89deb;
}

优点显而易见,通过动态的注入 CSS 变量,就能达到主题切换的功能,但是需要修改和注意的地方不少。如果有成体系的资产方案,改动较大。

如果你是使用全局的 CSS 文件,那需要换成带有 CSS 变量的全局文件。

diff 复制代码
-- import 'antd/dist/antd.min.css';
++ import 'antd/dist/antd.variable.min.css';

如果基于 antd@4 封装的组件,需要将 less 变量引用换成带有 CSS 变量的文件

diff 复制代码
-- @import (reference) '~antd/lib/style/themes/default.less';

++ @import (reference) '~antd/lib/style/themes/variable.less';

而且需要注意的是不能在任意地方再次重新定义 antd 支持的原有的 less 变量,因为会导致最终编译产物中的 less 变量没有使用 CSS 变量,在项目中的修改,应该通过修改对应 CSS 变量的方式实现自定义。

如果修改了前缀 prefixCls,如:

ts 复制代码
import { ConfigProvider } from 'antd';

export default () => (
  <ConfigProvider prefixCls="custom">
    <MyApp />
  </ConfigProvider>
);

需要重新编译一份 css

bash 复制代码
lessc --js --modify-var="ant-prefix=custom" antd/dist/antd.variable.less output.css

antd-mobile@5 CSS 变量

antd-mobile@5 本身就大量使用了 CSS 变量,现有的移动端设备低版本浏览器的场景也比较少,影响并不大。但是相比于 antd-mobile@2 的项目还是比 antd-mobile@5 要多的。

同样的也支持全局覆盖和局部覆盖的方式

css 复制代码
:root:root {
  --adm-color-primary: #a062d4;
  --adm-button-border-radius:200px;
}
.purple-theme {
  --adm-color-primary: #a062d4;
  --adm-button-border-radius:200px;
}

与 antd@4 不同的是,antd-mobile@5 还支持直接通过 style 传递 CSS 变量

ts 复制代码
<Button style={{
  '--border-radius': '2px'
}}/>

antd@4 动态 less 编译

先说结论吧,需要将所有用到的 less 编译成一个较大的 less 文件挂载到 html 中,然后在运行时,使用 less.modifyVars 方法动态编译 less。是优点也是缺点,在 IE 浏览器上 less 编译需要 5-6s 。

通过 webpack 插件来实现以上功能,通过 antd-pro-merge-less 将用到的 less 文件合并为一个文件。

ts 复制代码
import MergeLessPlugin from 'antd-pro-merge-less';
const outFile = path.join(__dirname, '../node_modules/.temp/merge.less');
const stylesDir = path.join(__dirname, '../src/');

    config.plugin('merge-less').use(new MergeLessPlugin(), [
      {
        stylesDir,
        outFile,
      },
    ]);

然后通过 antd-theme-webpack-plugin 插件将合并后的 less 文件和 antd 的 less 变量生成特定颜色变量的 less 文件,并注入到 html 中。

ts 复制代码
import AntDesignThemePlugin from 'antd-theme-webpack-plugin';

const outFile = path.join(__dirname, '../node_modules/.temp/merge.less');
const stylesDir = path.join(__dirname, '../src/');

    config.plugin('ant-design-theme').use(AntDesignThemePlugin, [
      {
        antDir: path.join(__dirname, '../node_modules/antd'),
        stylesDir,
        varFile: path.join(
          __dirname,
          '../node_modules/antd/lib/style/themes/default.less',
        ),
        mainLessFile: outFile,
        // themeVariables: ['@primary-color',],
        indexFileName: 'index.html',
        generateOne: true,
        lessUrl: 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js',
      },
    ]);

执行构建后会生成一个 color.less 文件,当你执行动态编译时,会挂在 <link rel="stylesheet/less" href="/color.less"> 并且生成一个 <style type="text/css" id="less:color"></style> 覆盖你的现有样式。

这两个插件需要锁定版本,因为不同版本功能不同 "antd-pro-merge-less": "1.0.0", "antd-theme-webpack-plugin": "1.3.9",

antd@4 粗暴的 CSS 样式覆盖

由于 antd@4 和 antd-mobile@2 已经进入稳定维护期,因此它的 css 结构不会发生变化,我们就可以通过简单粗暴的 CSS 样式覆盖的方式来实现动态主题切换。

通过在浏览器端编译 css ,采用 css 注入的方式,来进行动态覆盖。同样的可以指定 id 或者 class 选择器下生效的方式来实现局部的样式覆盖。

tsx 复制代码
import { Button } from 'antd';
import { normalizeCSS, insertRules } from 'theme-utils';

export default () => {
  return (
    <div id="purple-theme">
      <Button
        type="primary"
        block
        size="large"
        onClick={() => {
          const css = `.ant-btn-primary {
            border-color: red;
            background: red;
          }
          `;
          const a = normalizeCSS(css, '#purple-theme');
          insertRules('12312', a);
        }}
      >
        注入(模拟预览,只有部分生效)
      </Button>
    </div>
  );
};

这个方案最大的好处就是兼容性好,复杂的地方维护很繁琐,因此我将这个方法整理到了 theme-utils 包中。

除了上文中提到的浏览器端编译 css 的功能外,还支持根据指定模版,将对象的值解析道模版中,生成最终的 css 字符串(stringifyCss)。这样只需要简单的维护各个组件的 css 模版即可。

下文通过几个场景的简述,来说明如何借助 theme-utils 的能力在页面配置主题。

新建场景描述:

1、通过编辑页面 form 得到,theme 的对象 如 { backgroundColor:'red',fontSize:'12px'}

2、根据模版 '.cc{ background-color: backgroundColor; font-size: fontSize;}' 使用 stringifyCss 解析 css

3、得到最终 css '.cc{ background-color:red; font-size:12px;bor:123}'

4、将最终 css 提交到服务端

编辑场景描述:

1、从服务端取得 css

2、使用 parseCss 根据模版和 css 字符串解析出配置对象 { backgroundColor:'red',fontSize:'12px'}

3、将对象赋值到编辑页面 form

4、重复新建场景

预览场景描述:

1、每次编辑都会实时的生成 css

2、通过 normalizeCSS(css,'#previewId') 将 css 作用到指定 id dom 下 ' #previewId { .cc{ background-color:red; font-size:12px; }}'

3、通过 insertRules('12312', css, document.getElementById('previewId')); 将 style 挂载到预览 dom 上

4、挂载的样式仅会对预览生效

5、这个能力也可用于自定义生效,在动态挂在样式的时候,可以做到局部覆盖的能力。

使用场景描述:

1、通过 insertLink('12312', 'xxx.css',true); 在页面上加载所有的生成的 css,将会对所有的场景生效。

总结

上文中主要提到了 antd 系动态切换主题实践,主要涉及的技术是 cssinjs、CSS 变量、less 动态编译和 CSS 样式覆盖。从上文中我们能够看到如果应用场景需要兼容 IE 那最好的选择就是 CSS 样式覆盖。

技术 优点 弊端 IE 兼容 性能
cssinjs 实现简单,可自定义内容最多 新方案,推进成本较高,兼容性较差 不兼容
CSS 变量 实现简单,对部分变量替换较为快速 过于依赖浏览器的能力 不兼容
less 动态编译 浏览器兼容较好,变更较齐全 性能消耗较高,实现较难,需要维护构建插件 兼容
CSS 样式覆盖 兼容性最好,使用场景较多 只能用于稳定维护期的资产方案中,要求组件层级结构不能发生变化 兼容

演示demo

theme-demo

相关推荐
开心工作室_kaic1 分钟前
ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js
放逐者-保持本心,方可放逐1 分钟前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
IT古董2 分钟前
【前端】vue 如何完全销毁一个组件
前端·javascript·vue.js
Henry_Wu0014 分钟前
从swagger直接转 vue的api
前端·javascript·vue.js
SameX14 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_41 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito44 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js