Ant Design Style Overwrite

JavaScript

现状

当前的 Ant Design,常见的样式覆盖方案,大体上有两种:

  1. 使用 Ant Design 提供的 LESS 变量来覆盖原有的样式(详情可以参考官方的文档);
  2. 先一次性载入完整的 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-loadercss-loaderstyle-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 官方的教学文档