现状
当前的 Ant Design,常见的样式覆盖方案,大体上有两种:
- 使用 Ant Design 提供的 LESS 变量来覆盖原有的样式(详情可以参考官方的文档);
- 先一次性载入完整的 Ant Design CSS/LESS 文件,然后再载入新的覆盖样式(一些文章提到了这样的处理方法,比如这里)。
第一种方案的主要缺点是写法不太直观,优点是替换非常彻底,而且是官方推荐的方案;第二种方案,优点是覆盖的写法非常的直观,直接写 CSS/LESS 覆盖原有样式就可以了,但是缺点是需要一次性加载所有的样式,再覆盖。
按需加载的困难点
如果使用了 babel-plugin-import
对样式进行按需加载,再想要进行样式的覆盖,就很容易出现问题。
在了解具体可能存在的问题前,先来看一下 babel-plugin-import
的按需加载是如何运作的:在 JavaScript / TypeScript 文件遇到任何 Ant Design 组件的引用,就会同时将该组件的样式也插入到引用的位置。也就是说,如果有下面的 JavaScript 代码:
import { Select } from 'antd';
那么,编译转化后的代码大体如下:
import 'antd/lib/select/style';
import Select from 'antd/lib/select';
这里实际载入的样式文件来自 antd/lib/select/style
目录下的 index.js
文件。在这个文件中,具体引用了需要用到的各个 LESS 文件。对 Select 来说,这个样式引用的代码是:
require("../../style/index.less");
require("./index.less");
require("../../empty/style");
可以看到,除了 Select 自身的 index.less
文件外,先后还引用了 ../../style/index.less
文件和 ../../empty/style
文件。换句话说,babel-plugin-import
在处理按需加载的时候,并不是仅加载了当前使用组件的样式,还包含了一些组件需要的隐含依赖样式。这一点,在 AutoComplete 这样的复杂组件中更为明显。在使用 AutoComplete 的时候,其 style/index.js
内容如下:
require("../../style/index.less");
require("./index.less");
require("../../select/style");
require("../../input/style");
可以看到,除了自身的样式之外,Select 和 Input 的样式代码也被加载了一遍。换句话说,如果希望做按需加载的样式覆盖,在加载 AutoComplete 组件的时候,除了需要加载样式覆盖 AutoComplete 的部分,还需要额外加载样式将 Select 和 Input 的样式也覆盖一遍。而这些隐含的样式依赖,在代码上是不容易被察觉的。一旦漏了 Select 和 Input 的样式覆盖,就容易出现问题:明明 Select 的样式在加载的时候已经覆盖过了,但是在加载了 AutoComplete 组件之后,原先已经被覆盖的样式,又被新载入的 Select 原始样式给覆盖回去了。
因为 Ant Design 的样式没有采用 CSS Module,因此 CSS/LESS 的样式覆盖就强依赖于正确的加载顺序。覆盖的样式必须在原始样式的后面加载,否则结果就会出现错乱。
解决方案
为了确保按需加载的情况下,样式的覆盖顺序也是正确的,一个可行的思路是使用 Webpack 中的 loader 功能。根据 Webpack 打包的原理,任何的非 JavaScript 代码,都需要通过合适的 loader 转化成 JavaScript 文件,最终被打包到 bundle 中。而不管 babel-plugin-import
插件如何处理 Ant Design 的样式加载,这些最终被引用的 LESS 文件,都需要经过一些 loader 最终处理成可执行的文件(一般需要用到的 loader 包括 less-loader
,css-loader
和 style-loader
)。
既然 Webpack 的打包已经保证了统一的处理入口,那么就可以考虑在 loader 这一层,将样式的覆盖处理掉。
示例代码如下:
const fs = require('fs');
const path = require('path');
const pattern = /antd\/lib\/([^\/]+)\/style\/index.less/;
module.exports = function (content/*, map, meta */) {
/**
* 这里的 resourcePath 就是具体被使用的 LESS 文件的目录,详情可以参考 Webpack 文档:
* https://webpack.js.org/api/loaders/#thisresourcepath
*/
const { resourcePath } = this;
const match = pattern.exec(resourcePath);
/**
* 1. 如果不是 Antd 相关的 LESS 文件,直接忽略不处理
*/
if (!match) return content;
const component = match[1];
/**
* 2. 根据使用的 Component 组件,找到对应的覆盖样式文件,赋值给 customizedLessPath
*/
const customizedLessPath = getCustomizedLessFile(component);
if (!customizedLessPath) return content;
/**
* 3. 如果找到了覆盖文件,就将覆盖文件插入到 LESS 的最后面,保证调用顺序
*/
return [
content,
`@import "${customizedLessPath}";`,
].join('\n');
}
接下来,在 Webpack 中配置对应的 LESS 文件处理 loader,确保这个自定义的 loader 在 less-loader 的前面:
{
test: /\.less$/,
use: [
'postcss-loader',
{
loader: 'less-loader',
options: {
// ...
}
},
{
loader: require.resolve('path-to-custom-loader')
}
]
},
这样,假设 babel-plugin-import
插件插入了一段 Select 的 LESS 文件:
@select-prefix-cls: ~'@{ant-prefix}-select';
// ...
经过上面的自定义 loader 处理之后,就会变成:
@select-prefix-cls: ~'@{ant-prefix}-select';
// ...
@import "customized-less-path";
可以看到,自定义的 LESS 文件一定会在原始 LESS 文件的后面,从顺序上可以保证样式一定可以正确的被覆盖。剩下的事情,就交给 less-loader 及后续 loader 去处理就可以了。
延伸阅读
关于 Webpack loader 的写法,可以参考 Webpack 官方的教学文档。