背景

在扫码登录的需求中,就是在 商城和至造云的登录框上挂个 DOM 渲染用 Vue写的一些组件实现扫码,组件库选择了 ant design vue(以下简称antd),尴尬的是引入之后发现,商城和至造云自身是带一套全局样式的,而 antd又带了一套全局样式,导致 商城和至造云的部分样式被覆盖,如a 标签颜色被 antd 覆盖,布局出现问题。因为 Ant-Design 的理念是一套设计语言,所以 antd 会引入一套 fork 自 normalize.css 的浏览器默认样式重置库。直到今天官方没有进展。引入全局样式的这个文件是 style/core/base.less,就是这个 base.less 会对各种元素的默认样式一顿格式化。
如图:
image.png

解决思路

核心问题就是base.less 这个文件对全局样式的侵入。那这个文件可以不要吗?不行,antd 的组件样式都是建立在这个格式化后的样式上的,所以要在不影响全局样式的条件下。如果将 base.less 外面套一层 .ant-container 的方案,但一个显著的缺陷就是提高了 base.less 中样式的权重导致样式错位。但是限定 base.less 这个思路是没有错的,base.less 需要被套一层「作用域」,那再给所有已有的 antd 组件提高权重保证原有的选择器优先级不变就好了。幸运的是,antd 相关的组件都至少会有一个以 ant- 开头的 class,我们只要利用好这个特点及 CSS 属性选择器即可达到目的。
流程如下:

  1. 将 antd 中的 base.less 替换为魔改的 base.less,这个魔改的 base.less 外面会套一层 *[class*='ant-'] 限定其样式的「作用域」。这一步将全局样式限定在了所有有 ant- 的 class 的元素里。

    1. *[class*='ant-'] {
    2. @import '~antd/lib/style/core/base.less';
    3. }
  2. 提高完了 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- 开头的就可以。

    1. import parser, { Node } from 'postcss-selector-parser'
    2. import { SelectorReplace } from '../index'
    3. export function antdScopeReplacerFn(node: Node) {
    4. if (node.type !== 'selector') return
    5. const firstAntClassNodeIndex = node.nodes.findIndex((n) => {
    6. return n.type === 'class' && n.value.startsWith('ant-')
    7. })
    8. if (firstAntClassNodeIndex < 0) return
    9. const firstAntClassNode = node.nodes[firstAntClassNodeIndex]
    10. const prevNode = node.nodes[firstAntClassNodeIndex - 1]
    11. // preserve line break
    12. const spaces = {
    13. before: firstAntClassNode.rawSpaceBefore,
    14. after: firstAntClassNode.rawSpaceAfter,
    15. }
    16. firstAntClassNode.setPropertyWithoutEscape('rawSpaceBefore', '')
    17. const toInsert = []
    18. if (firstAntClassNodeIndex === 0 || prevNode.type === 'combinator') {
    19. const universal = parser.universal({
    20. value: '*',
    21. })
    22. toInsert.push(universal)
    23. }
    24. const attr = parser.attribute({
    25. attribute: 'class',
    26. operator: '*=',
    27. value: `"ant-"`,
    28. raws: {},
    29. })
    30. toInsert.push(attr)
    31. toInsert[0].spaces = spaces
    32. firstAntClassNode.parent!.nodes.splice(firstAntClassNodeIndex, 0, ...toInsert)
    33. }
    34. export const antdReplacer: SelectorReplace = {
    35. type: 'each',
    36. replacer: antdScopeReplacerFn,
    37. }

    解决步骤

    1. 改写antd/lib/style/core/index.less

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

    1. "scripts": {
    2. "serve": "vue-cli-service serve",
    3. "build": "vue-cli-service build",
    4. "lint": "vue-cli-service lint --fix",
    5. "postinstall": "node scripts/postinstall.js"
    6. }

    ```javascript

    !/usr/bin/env node

const replace = require(‘replace-in-file’)

const cwd = process.cwd()

const removeAntdGlobalStyles = () => { const options = { files: [${cwd}/node_modules/ant-design-vue/lib/style/core/index.less, ${cwd}/node_modules/ant-design-vue/es/style/core/index.less], from: “@import ‘base’;”, to: ‘’, }

replace(options) .then(() => { console.log(‘[INFO] Successfully Removed Antd Global Styles:’) }) .catch((e) => { console.error(‘[ERR] Error removing Antd Global Styles:’, e) process.exit(1) }) }

removeAntdGlobalStyles()

  1. <a name="TasCu"></a>
  2. ### 2. PostCSS 中添加 [postcss-rename-selector](https://github.com/fi3ework/postcss-rename-selector) 插件并配置:
  3. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/294143/1608514029930-58703dbb-c063-4b7e-a28a-f18930335388.png#align=left&display=inline&height=728&margin=%5Bobject%20Object%5D&name=image.png&originHeight=728&originWidth=874&size=125638&status=done&style=none&width=874)
  4. ```javascript
  5. const { replacer, presets } = require('postcss-rename-selector')
  6. plugins: [
  7. replacer(presets.antdReplacer)
  8. ]

3. 引入全量样式 import 'antd/dist/antd.less'或者按需引入配置 babel-plugin-import

  1. [
  2. 'import',
  3. {
  4. libraryName: 'antd',
  5. style: true,
  6. },
  7. ],

4. 额外引入一个 base.less,限定在一个「作用域」下

  1. @import '~antd/lib/style/mixins/index.less';
  2. *[class*='ant-'] {
  3. @import '~antd/lib/style/core/base.less';
  4. }

5.完善问题

问题:html标签css 不生效

  1. /* stylelint-disable at-rule-no-unknown */
  2. // Reboot
  3. //
  4. // Normalization of HTML elements, manually forked from Normalize.css to remove
  5. // styles targeting irrelevant browsers while applying new styles.
  6. //
  7. // Normalize is licensed MIT. https://github.com/necolas/normalize.css
  8. // HTML & Body reset
  9. @{html-selector},
  10. body {
  11. .square(100%);
  12. }
  13. // remove the clear button of a text input control in IE10+
  14. input::-ms-clear,
  15. input::-ms-reveal {
  16. display: none;
  17. }
  18. // Document
  19. //
  20. // 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
  21. // 2. Change the default font family in all browsers.
  22. // 3. Correct the line height in all browsers.
  23. // 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.
  24. // 5. Setting @viewport causes scrollbars to overlap content in IE11 and Edge, so
  25. // we force a non-overlapping, non-auto-hiding scrollbar to counteract.
  26. // 6. Change the default tap highlight to be completely transparent in iOS.
  27. *,
  28. *::before,
  29. *::after {
  30. box-sizing: border-box; // 1
  31. }
  32. @{html-selector} {
  33. font-family: sans-serif; // 2
  34. line-height: 1.15; // 3
  35. -webkit-text-size-adjust: 100%; // 4
  36. -ms-text-size-adjust: 100%; // 4
  37. -ms-overflow-style: scrollbar; // 5
  38. -webkit-tap-highlight-color: fade(@black, 0%); // 6
  39. }

解决:修改html-selector 变量

  1. // eslint-disable-next-line @typescript-eslint/no-var-requires
  2. const { replacer, presets } = require('postcss-rename-selector')
  3. module.exports = {
  4. css: {
  5. // 是否使用css分离插件 ExtractTextPlugin
  6. extract: false, // 注释css热更新生效
  7. // 开启 CSS source maps?
  8. sourceMap: false,
  9. loaderOptions: {
  10. postcss: {
  11. plugins: [
  12. replacer(presets.antdReplacer),
  13. ],
  14. },
  15. less: {
  16. lessOptions: {
  17. modifyVars: {
  18. 'primary-color': '#2c4298',
  19. 'link-color': '#2c4298',
  20. 'html-selector': '.my-html-selector',
  21. },
  22. javascriptEnabled: true,
  23. },
  24. },
  25. },
  26. },
  27. }

问题:弹窗类组件默认挂在body下,导致不在ant-类下
解决:修改弹窗类组件默认挂载点

  1. <template>
  2. <!-- 如果某些元素虽然不在 antd 的组件里,但是也想走 antd 的全局样式,只需在这些元素的最外层套一个 class className="ant-whatever",只要是 ant- 开头的就可以 -->
  3. <a-config-provider :locale="locale" :getPopupContainer="getPopupContainer">
  4. <div class="ant-root">
  5. <div class="my-html-selector">
  6. <my-root></my-root>
  7. <!-- 弹窗类组件默认挂载点-->
  8. <div id="ant-popupContainer"></div>
  9. </div>
  10. </div>
  11. </a-config-provider>
  12. </template>
  13. <script lang="ts">
  14. import '@/assets/styles/base.less'
  15. import {
  16. defineComponent, readonly, ref, reactive,
  17. } from 'vue'
  18. import { ConfigProvider } from 'ant-design-vue'
  19. import zhCN from 'ant-design-vue/es/locale/zh_CN'
  20. // import Root from '@/views/MissionSquare.vue'
  21. // import Root from '@/views/Return.vue'
  22. // import Root from '@/views/Refund.vue'
  23. import Root from '@/views/ScanLogin.vue'
  24. // import Root from '@/views/SmartBuy.vue'
  25. function getPopupContainer(triggerNode: HTMLElement) {
  26. const targetNode = document.getElementById('ant-popupContainer')
  27. if (targetNode) {
  28. return targetNode
  29. }
  30. return document.body
  31. }
  32. export default defineComponent({
  33. components: {
  34. [ConfigProvider.name]: ConfigProvider,
  35. 'my-root': Root,
  36. },
  37. setup() {
  38. return {
  39. getPopupContainer,
  40. locale: zhCN,
  41. }
  42. },
  43. })
  44. </script>

效果

a标签颜色没有被覆盖
image.png
修改后引入的css(部分)

  1. [class*=ant-] .my-html-selector, [class*=ant-] body {
  2. width: 100%;
  3. height: 100%
  4. }
  5. [class*=ant-] input::-ms-clear, [class*=ant-] input::-ms-reveal {
  6. display: none
  7. }
  8. [class*=ant-] *, [class*=ant-] :after, [class*=ant-] :before {
  9. box-sizing: border-box
  10. }
  11. [class*=ant-] .my-html-selector {
  12. font-family: sans-serif;
  13. line-height: 1.15;
  14. -webkit-text-size-adjust: 100%;
  15. -ms-text-size-adjust: 100%;
  16. -ms-overflow-style: scrollbar;
  17. -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
  18. }
  19. @-ms-viewport {
  20. width: device-width
  21. }
  22. [class*=ant-] article, [class*=ant-] aside, [class*=ant-] dialog, [class*=ant-] figcaption, [class*=ant-] figure, [class*=ant-] footer, [class*=ant-] header, [class*=ant-] hgroup, [class*=ant-] main, [class*=ant-] nav, [class*=ant-] section {
  23. display: block
  24. }

资料

  1. 如何优雅地彻底解决 antd 全局样式问题
  2. https://github.com/vueComponent/ant-design-vue/blob/master/components/style/themes/default.less