简介
动态切换主题通常指的更多的是运行时切换页面主题的功能,比如用户通过点击页面中的一个按钮来达到几种预制主题的切换,这一般都是通过提前将几种预制主题的样式进行独立编译,供用户选择使用。这只是动态主题的基本功能,更进一步的,应该允许用户定制和设计页面中的任何一个组件元素。
动态切换主题的需求一般在"多租户"的场景会是一个比较强烈的需求,可以实现同一套代码,在不同的租户现场又能呈现不同的页面风格的好处。但是在技术实现上却存在不少难点,这个功能常常因为体验太差,或者影响页面性能而遭到用户的吐槽。
很多人第一次了解到这个功能,应该是从 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 样式覆盖 | 兼容性最好,使用场景较多 | 只能用于稳定维护期的资产方案中,要求组件层级结构不能发生变化 | 兼容 | 低 |