背景
由于某些原因,我们团队负责在 GitLab 上做二次开发,简单理解就是在 GitLab 上挂个 DOM 渲染用 React 写的一些组件,组件库选择了 antd,尴尬的是引入之后发现,GitLab 自身是带一套全局样式的,而 antd 又带了一套全局样式,导致 GitLab 的部分样式被覆盖,如图: a 标签颜色被 antd 覆盖:
checkbox 细微的样式错乱及大小改变:
原因
antd 的全局样式也不是一天两天的问题了,在社区中已经有很多讨论(#4331 #9363 #13459),但直到今天也没有进展。因为 Ant-Design 是一套设计语言,所以 antd 会引入一套 fork 自 normalize.css 的浏览器默认样式重置库。
引入全局样式的这个文件是 style/core/base.less,就是这个 base.less 会对各种元素的默认样式一顿格式化,截取一段:
...
// remove inner border and padding from Firefox, but don't restore the outline like Normalize.
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type='radio'],
input[type='checkbox'] {
box-sizing: border-box; // 1. Add the correct box sizing in IE 10-
padding: 0; // 2. remove the padding in IE 10-
}
...
下图为 antd 的 CSS 打包时的依赖关系,这张图有助于我们理清怎样才能避免把 base.less 引入。
解决核心问题
核心问题就是 base.less 这个文件对全局样式的侵入。那这个文件可以不要吗?不行,antd 的组件样式都是建立在这个格式化后的样式上的,不引这个文件样式就错位了(如下图),所以要在不影响全局样式的条件下引入。
并且,一般我们需要收敛 antd 全局样式时,都是因为当前页面存在另一套全局样式库(比如笔者遇到的 GitLab 的全局样式),我们需要达到的目的可以进一步变为 「收敛 base.less,并保证外部的全局样式无法轻易覆盖 antd 的样式」。
简单限定 base.less
之前社区中出现过将 base.less 外面套一层 .ant-container
的方案,但一个显著的缺陷就是提高了 base.less 中样式的权重导致样式错位。
全面提高 ant- 的优先级
但是限定 base.less 这个思路是没有错的,base.less 需要被套一层「作用域」,那再给所有已有的 antd 组件提高权重保证原有的选择器优先级不变就好了。
幸运的是,antd 相关的组件都至少会有一个以 ant-
开头的 class,我们只要利用好这个特点及 CSS 属性选择器即可达到目的。
流程如下:
- 将 antd 中的 base.less 替换为(具体怎么见「使用方式」)魔改的 base.less,这个魔改的 base.less 外面会套一层
*[class*='ant-']
限定其样式的「作用域」。这一步将全局样式限定在了所有有ant-
的 class 的元素里。
*[class*='ant-'] {
@import '~antd/lib/style/core/base.less';
}
-
提高完了 base.less 的权重,再来提升组件的样式的权重,此举还能间接提升所有 antd 的样式的权重,避免外部的全局样式对 antd 造成侵入。
既然是改样式,那就用 CSS 界的 babel —— PostCSS,写个 PostCSS 插件,github.com/fi3ework/po… ,将所有
.ant
开头的类选择器都同样升高即可,利用的是 postcss-selector-parser 这个 PostCSS 官方提供的解析选择器的库,过滤出「第一个以 ant- 开头的类选择器」,在其前面添加一个属性选择器[class*='ant-']
,如果这个选择器排在当前 rule 的第一个或者前面是一个 combinator,则再加一个通配符*
,这个同上面给 base.less 添加的选择器,两者同时提高相同权重既维持原有优先级不变。另外,如果某些元素虽然不在 antd 的组件里,但是也想走 antd 的全局样式,只需在这些元素的最外层套一个 class
className="ant-whatever"
,只要是ant-
开头的就可以。
import parser, { Node } from 'postcss-selector-parser'
import { SelectorReplace } from '../index'
export function antdScopeReplacerFn(node: Node) {
if (node.type !== 'selector') return
const firstAntClassNodeIndex = node.nodes.findIndex((n) => {
return n.type === 'class' && n.value.startsWith('ant-')
})
if (firstAntClassNodeIndex < 0) return
const firstAntClassNode = node.nodes[firstAntClassNodeIndex]
const prevNode = node.nodes[firstAntClassNodeIndex - 1]
// preserve line break
const spaces = {
before: firstAntClassNode.rawSpaceBefore,
after: firstAntClassNode.rawSpaceAfter,
}
firstAntClassNode.setPropertyWithoutEscape('rawSpaceBefore', '')
const toInsert = []
if (firstAntClassNodeIndex === 0 || prevNode.type === 'combinator') {
const universal = parser.universal({
value: '*',
})
toInsert.push(universal)
}
const attr = parser.attribute({
attribute: 'class',
operator: '*=',
value: `"ant-"`,
raws: {},
})
toInsert.push(attr)
toInsert[0].spaces = spaces
firstAntClassNode.parent!.nodes.splice(firstAntClassNodeIndex, 0, ...toInsert)
}
export const antdReplacer: SelectorReplace = {
type: 'each',
replacer: antdScopeReplacerFn,
}
这个 antd 的配置已经作为 preset 提供了,如果想使用直接引入即可
const { replacer, presets } = require('postcss-rename-selector')
plugins: [
replacer(presets.antdReplacer)
]
效果如图:
使用方式
建了 demo 仓库,下面几种的方式在 demo 仓库中都可以找到:github.com/fi3ework/re…
方式 1:删除 base.less 一把梭
全量
思路是:在 post-install 阶段将 antd/lib/style/core/index.less
引入的 @import base;
这一行直接删掉,然后手动引入我们自己魔改的 base.less。
步骤:
- 写一个 post-install 脚本,直接改写
antd/lib/style/core/index.less
,这边已经有实现 github.com/ant-design/… - PostCSS 中添加 postcss-rename-selector 插件并配置:
const { replacer, presets } = require('postcss-rename-selector')
plugins: [
replacer(presets.antdReplacer)
]
- 引入全量样式
import 'antd/dist/antd.less'
- 额外引入一个 base.less,限定在一个「作用域」下
@import '~antd/lib/style/mixins/index.less';
*[class*='ant-'] {
@import '~antd/lib/style/core/base.less';
}
看下效果,antd 的样式正常,并且最上方的一个 a 标签并没有被 antd 所影响:
按需引入
- 同全量引入 1
- 同全量引入 2
- 配置 babel-plugin-import
[
'import',
{
libraryName: 'antd',
style: true,
},
],
- 同全量引入 4
方式 2:手动拼接 antd.less
全量
post-install 的方法多少显得有些 hack,另一种方法是手动拼出 antd/dist/antd.less
的文件依赖然后引入。
@import '~antd/lib/style/themes/index.less';
@import '~antd/lib/style/mixins/index.less';
*[class*='ant-'] {
@import '~antd/lib/style/core/base.less';
}
@import '~antd/lib/style/core/iconfont.less';
@import '~antd/lib/style/core/motion.less';
@import '~antd/lib/style/components.less';
结构与原本的引入相同,唯一不同的地方就是将 base.less 包裹了一层「作用域」,然后还需要在 webpack 的配置中添加 alias
alias: {
'antd/dist/antd.less$': path.resolve(__dirname, '../src/custom-dist.less')
}
然后在整个文件的入口引入
import './custom-dist.less`
就好啦。
按需引入
很遗憾,在这种方式下,笔者折腾了半天也无法做到配合 babel-plugin-import 做按需引入。babel-plugin-import 提供了几种预置的样式加载方式及可定制化的方法,拿 Button 这个组件举例
- 引
antd/lib/button/index.css
,就是将 babel-plugin-import 配成这样:
[
'import',
{
libraryName: 'antd',
customStyleName: (name) => {
return `antd/lib/${name}/style/index.css`
}
}
],
Button 这个组件没有问题,但是有些组件,比如 Col
是放在 Layout 这个目录的,按照组件名拼名字会找不到文件直接报错。还有,比如 Input
这个组件是依赖 Button
的样式的,只按需引 Input
的样式是不行的,还要手动引入 Button
的样式。
- 引
antd/lib/button/css.js
,就是将 babel-plugin-import 配成这样:
[
'import',
{
libraryName: 'antd',
style: 'css'
}
],
这个文件长这个样子
'use strict'
require('../../style/index.css')
require('./index.css')
只需要把 require("../../style/index.less");
的这个引入干掉即可。但是遗憾的是,笔者试了 IgnorePlugin
和 alias
均无效。尤其是 IgnorePlugin
,按照官方文档给的对 Moment.js 的处理方式,理论上应该可以忽略。
new webpack.IgnorePlugin(/\.\.\/\.\.\/style\/index\.css/, /antd$/),
但实际没有任何效果,如果哪位知道是为什么请告知。
总结
目前笔者所用的 antd 的版本还是 3.x,还没有升级到 v4 验证过,不过看了下 v4 的代码,base.less 还安安静静的躺在那里,目测使用方法是类似的。
这套方案在我们自己的业务上已经跑了几个月了,暂时没有发现什么问题。Ant Design 作为一套设计规范提供全局样式也是合理的,但还是希望官方可以提供一种可选的限定范围的全局样式,毕竟隔壁的 Material-UI 可是没这个问题(逃),默默许愿 antd v5 中可以解决!
彩(广)蛋(告)
之前为 antd 写了个 VS Code 生产力插件,自认为是最好用的 antd VS Code 插件了(逃),欢迎 Star,Issue。