fi3ework / blog

📝
861 stars 51 forks source link

如何优雅地彻底解决 antd 全局样式问题 #44

Open fi3ework opened 4 years ago

fi3ework commented 4 years ago

背景

由于某些原因,我们团队负责在 GitLab 上做二次开发,简单理解就是在 GitLab 上挂个 DOM 渲染用 React 写的一些组件,组件库选择了 antd,尴尬的是引入之后发现,GitLab 自身是带一套全局样式的,而 antd 又带了一套全局样式,导致 GitLab 的部分样式被覆盖,如图: a 标签颜色被 antd 覆盖:

image

checkbox 细微的样式错乱及大小改变:

image

原因

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 引入。

image

解决核心问题

核心问题就是 base.less 这个文件对全局样式的侵入。那这个文件可以不要吗?不行,antd 的组件样式都是建立在这个格式化后的样式上的,不引这个文件样式就错位了(如下图),所以要在不影响全局样式的条件下引入。

image

并且,一般我们需要收敛 antd 全局样式时,都是因为当前页面存在另一套全局样式库(比如笔者遇到的 GitLab 的全局样式),我们需要达到的目的可以进一步变为 「收敛 base.less,并保证外部的全局样式无法轻易覆盖 antd 的样式」

简单限定 base.less

之前社区中出现过将 base.less 外面套一层 .ant-container方案,但一个显著的缺陷就是提高了 base.less 中样式的权重导致样式错位。

全面提高 ant- 的优先级

但是限定 base.less 这个思路是没有错的,base.less 需要被套一层「作用域」,那再给所有已有的 antd 组件提高权重保证原有的选择器优先级不变就好了。

幸运的是,antd 相关的组件都至少会有一个以 ant- 开头的 class,我们只要利用好这个特点及 CSS 属性选择器即可达到目的。

流程如下:

  1. 将 antd 中的 base.less 替换为(具体怎么见「使用方式」)魔改的 base.less,这个魔改的 base.less 外面会套一层 *[class*='ant-'] 限定其样式的「作用域」。这一步将全局样式限定在了所有有 ant- 的 class 的元素里。
*[class*='ant-'] {
  @import '~antd/lib/style/core/base.less';
}
  1. 提高完了 base.less 的权重,再来提升组件的样式的权重,此举还能间接提升所有 antd 的样式的权重,避免外部的全局样式对 antd 造成侵入。

    既然是改样式,那就用 CSS 界的 babel —— PostCSS,写个 PostCSS 插件,https://github.com/fi3ework/postcss-rename-selector ,将所有 .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)
]

效果如图:

image

使用方式

建了 demo 仓库,下面几种的方式在 demo 仓库中都可以找到:https://github.com/fi3ework/restricted-antd-style-demo

方式 1:删除 base.less 一把梭

全量

思路是:在 post-install 阶段将 antd/lib/style/core/index.less 引入的 @import base; 这一行直接删掉,然后手动引入我们自己魔改的 base.less。

步骤:

  1. 写一个 post-install 脚本,直接改写 antd/lib/style/core/index.less,这边已经有实现 https://github.com/ant-design/ant-design/issues/9363#issuecomment-598091517
  2. PostCSS 中添加 postcss-rename-selector 插件并配置:
const { replacer, presets } = require('postcss-rename-selector')

plugins: [
    replacer(presets.antdReplacer)
]
  1. 引入全量样式 import 'antd/dist/antd.less'
  2. 额外引入一个 base.less,限定在一个「作用域」下
@import '~antd/lib/style/mixins/index.less';

*[class*='ant-'] {
  @import '~antd/lib/style/core/base.less';
}

看下效果,antd 的样式正常,并且最上方的一个 a 标签并没有被 antd 所影响:

image

按需引入

  1. 同全量引入 1
  2. 同全量引入 2
  3. 配置 babel-plugin-import
  [
    'import',
    {
      libraryName: 'antd',
      style: true,
    },
  ],
  1. 同全量引入 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 这个组件举例

  1. 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 的样式。

  1. 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"); 的这个引入干掉即可。但是遗憾的是,笔者试了 IgnorePluginalias 均无效。尤其是 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。

vscode-antd-rush

Ref

Sec-ant commented 4 years ago

这样做好像 html 和 body 标签下的样式无法指定,主要影响的是 font,antd 的会被全局样式所覆盖。不过如果这点不重要的话,其他样式看起来的都是没什么问题的,感谢提供的方案。

多说一句,antd 4.7.0 里面不再是 base.less 设定全局样式,所有的全局样式都移动到了 global.less 这个文件里,base.less 已经限定了作用域,所以如果要改写 antd.less 的话,需要改成如下内容:

@import '~antd/lib/style/themes/index.less';
@import '~antd/lib/style/mixins/index.less';

*[class*='ant-'] {
    @import '~antd/lib/style/core/global.less';
}

@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';
darknessjs commented 2 years ago

挂个私人链接: https://www.yuque.com/zhongjing-tfizt/darkness/ihl4ef

zhongweiming commented 2 years ago

虽然确保了antd组件样式不影响全局的,但是提高了antd样式的层级,导致后面如果要写自己的私有样式,一个类根本不够,会被antd的[class *=ant-]h1这种的覆盖掉

fi3ework commented 2 years ago

虽然确保了antd组件样式不影响全局的,但是提高了antd样式的层级,导致后面如果要写自己的私有样式,一个类根本不够,会被antd的[class *=ant-]h1这种的覆盖掉

直接 !important

NameWjp commented 2 years ago

不错的方案,学习了

a1ooha commented 2 years ago

patch-package 打个补丁,直接将 node_modules/antd/lib/style/core/index.less 改成下面这样就行:

@import '../mixins/index';
@import 'base';
*[class*='ant-'] {
    @import 'global';
}
@import 'iconfont';
@import 'motion';

步骤少,很方便。

PaperTiger7 commented 2 years ago

patch-package 打个补丁,直接将 node_modules/antd/lib/style/core/index.less 改成下面这样就行:

@import '../mixins/index';
@import 'base';
*[class*='ant-'] {
    @import 'global';
}
@import 'iconfont';
@import 'motion';

步骤少,很方便。

兄弟,试了下,不行呀,和antd版本有关系吗?

a1ooha commented 2 years ago

patch-package 打个补丁,直接将 node_modules/antd/lib/style/core/index.less 改成下面这样就行:

@import '../mixins/index';
@import 'base';
*[class*='ant-'] {
    @import 'global';
}
@import 'iconfont';
@import 'motion';

步骤少,很方便。

兄弟,试了下,不行呀,和antd版本有关系吗?

应该没关系,你按照 patch-package 的文档操作,生成 patches 文件夹及对应的文件了吗?

csr632 commented 2 years ago

我写了一个less插件来自动移除antd的全局样式antd/lib/style/core/global.lesshttps://github.com/csr632/less-plugin-remove-antd-global-styles 能够与vite, webpack, rollup以及babel-plugin-import一起使用。