解决 Ant Design V5 样式兼容问题

引言

Ant Design V5 是截止目前 Ant Design 组件库最新版本,我们在业务场景已经开始应用,但在近期的测试中发现 Ant Design 组件无法在 360 极速浏览器(极速模式)下显示组件样式。经过查看样式和查阅官方文档, Ant Design V5 组件的样式中大量使用了 :where() 选择器来降低选择器的优先级,以此减少开发者升级组件库时额外调整自定义样式的成本。:where() 对于 Chrome 仅支持 Chrome 88 以上,截止目前 360 极速浏览器的 Chrome 版本为 86,因此无法显示组件样式。

参考

  1. 浅谈CSS逻辑选择器 is、where、not、has
  2. :where() - MDN

对于此样式兼容问题,主要解决思路如下:

  1. 参照官方样式兼容文档,使用其提供的降级方案,移除掉 :where() 选择器。主要是通过 Context 为组件传入配置,关闭降低样式优先级的配置,以此移除 :where() 选择器。但针一些无法接收 Context 的场景,例如 Message、Modal 和 Notification 等组件的静态调用形式,仍然会使用 :where() 选择器。
  2. 对于第一点产生的问题,如果要在组件中使用 Message、Modal 和 Notification 组件,需要避免静态调用形式,采用官方推荐的 Hook 调用形式,可以通过 App 包裹组件完成此操作。
  3. 但 Hook 调用形式只能在组件使用,业务场景上常常需要在非组件内调用相关,比较典型就是在请求的响应拦截器中调用 Message 实现错误提示。对于在非组件调用可以通过类 Redux 数据全局共享与通信方案,在相关逻辑中(例如响应拦截器中)通知组件调用 Message ,从而实现此需求。

通过降级方案移除 :where() 选择器

入口文件:

import ReactDOM from "react-dom/client";
import {App} from "antd";
import {StyleProvider, legacyLogicalPropertiesTransformer} from '@ant-design/cssinjs';
import "./index.css";

// 其他无关代码已省略...

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}> /* 关闭降权操作,移除 :where() 选择器 */
    <App> /* Ant Design 提供的包裹组件,用于使得 Message、Modal、Notification 等组件的静态调用形式能获取到上下文,从而读取移除 :where() 选择器的配置  */
      <RouterProvider router={router}/> 
    </App>
  </StyleProvider>
);

组件内静态调用 Message、Modal、Notification 等组件的方式:

import {FC, useEffect} from "react";
import {App} from "antd";

const Main: FC = () => {
  const {message, modal} = App.useApp();

  useEffect(()=>{
    message.success("成功");
  },[])

  return (
    <div>
    </div>
  );
};

export default Main;

通过数据通信机制解决非组件内调用 Message 等组件

通过 Redux 解决

以在 Axios 响应拦截器内使用 Message 组件完成错误提示为例,其他 Modal、Notification 组件使用方式同理。
依赖:react-redux、@reduxjs/toolkit

MessageAlert 组件 - 用于通知此组件触发相关的消息通知:

import {useEffect} from "react";
import {App} from "antd";
import {useSelector} from "react-redux";
import {RootState} from "@/store";

function MessageAlert() {
  const {message: messageApi} = App.useApp();
  const messageInfo = useSelector((state: RootState) => state.global.messageInfo);
  useEffect(() => {
    const {type, content} = messageInfo;
    if (messageApi && content) {
      switch (type) {
        case "success":
          messageApi.success(content);
          break;
        case "error":
          messageApi.error(content);
          break;
        case "warning":
          messageApi.warning(content);
          break;
        case "info":
          messageApi.info(content);
          break;
        default:
          messageApi.error(content);
          break;
      }
    }
  }, [messageApi, messageInfo]);

  return null;
}

export default MessageAlert;

入口文件:

import ReactDOM from "react-dom/client";
import {App} from "antd";
import {StyleProvider, legacyLogicalPropertiesTransformer} from '@ant-design/cssinjs';
import MessageAlert from "@/components/MessageAlert";
import "./index.css";

// 其他无关代码已省略...

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}> /* 关闭降权操作,移除 :where() 选择器 */
    <App> /* Ant Design 提供的包裹组件,用于使得 Message、Modal、Notification 等组件的静态调用形式能获取到上下文,从而顺利读取移除 :where() 选择器的配置  */
      <RouterProvider router={router}/>
      <MessageAlert></MessageAlert> /* 消息提示组件,用于在非组件内通过数据通信方式实现调用。 */
    </App>
  </StyleProvider>
);

Redux Store 配置
store/global.ts

import {createSlice} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';

export type MessageInfoType = {
  type: 'success' | 'error' | 'info' | 'warning';
  content?: string;
}

export interface GlobalState {
  messageInfo: MessageInfoType;
}

const initialState: GlobalState = {
  messageInfo: {
    type: 'success',
  }
};

export const globalSlice = createSlice({
  name: 'global',
  initialState,
  reducers: {
    saveMessageInfo: (state, action: PayloadAction<MessageInfoType>) => {
      state.messageInfo = {...action.payload};
    }
  },
});

export const {saveMessageInfo} = globalSlice.actions;

export default globalSlice.reducer;

store/index.ts

import {configureStore} from '@reduxjs/toolkit';
import globalReducer from './global';

export const store = configureStore({
  reducer: {
    global: globalReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

请求拦截器内调用相关 action 实现消息通知:

import {store} from "@/store";

// 其他无关代码已省略...

axiosInstance.interceptors.response.use(
  async (response) => {
    const {data} = response;
    const {code, msg} = data;
    if (code !== 0) {
      // 错误提示
      store.dispatch({
        type: 'global/saveMessageInfo',
        payload: {
          type: 'error',
          content: msg || '服务端发生错误'
        }
      });
    }
    return response;
  },
  // ...
);

export default axiosInstance;

不引入 Redux(自行实现小型状态管理器)

如果项目中不想额外引入 Redux,可以考虑自行实现小型状态管理器。

store.ts - 适用于简单的数据共享场景的小型状态管理器

export type MessageInfoType = {
  type: 'success' | 'error' | 'info' | 'warning';
  content?: string;
}
type State = {
  messageInfo: MessageInfoType;
};
type Listener = () => void;

class Store {
  private state: State; // 用于存储状态
  private listeners: Listener[]; // 用于存储所有组件对状态进行更新的函数

  constructor() {
    this.state = {messageInfo: {type: 'success', content: ''}};
    this.listeners = [];
  }

  // 获取状态
  getState(): State {
    return this.state;
  }

  // 修改状态
  setState(newState: State): void {
    this.state = newState;
    this.notify();
  }

  // 订阅状态
  subscribe(listener: Listener): void {
    this.listeners.push(listener);
  }

  // 取消订阅
  unsubscribe(listener: Listener): void {
    this.listeners = this.listeners.filter((l) => l !== listener);
  }

  // 用于通知所有组件对状态进行更新
  private notify(): void {
    for (const listener of this.listeners) {
      listener();
    }
  }
}

const store = new Store();

export {store};

MessageAlert 组件 - 使用小型状态管理器

import {useEffect, useState} from "react";
import {App} from "antd";
import {MessageInfoType, store} from '@/store/store';

function MessageAlert() {
  const {message: messageApi} = App.useApp();
  const [messageInfo, setMessageInfo] = useState<MessageInfoType>(store.getState().messageInfo);

  useEffect(() => {
    const handleMessageInfoChange = () => {
      setMessageInfo(store.getState().messageInfo);
    };
    store.subscribe(handleMessageInfoChange);
    return () => {
      store.unsubscribe(handleMessageInfoChange);
    };
  }, []);

  useEffect(() => {
    const {type, content} = messageInfo;
    if (messageApi && content) {
      switch (type) {
        case "success":
          messageApi.success(content);
          break;
        case "error":
          messageApi.error(content);
          break;
        case "warning":
          messageApi.warning(content);
          break;
        case "info":
          messageApi.info(content);
          break;
        default:
          messageApi.error(content);
          break;
      }
    }
  }, [messageApi, messageInfo]);

  return null;
}

export default MessageAlert;

请求拦截器配置

import {store} from "@/store.ts";

// 其他无关代码已省略...

axiosInstance.interceptors.response.use(
  async (response) => {
    const {data} = response;
    const {code, msg} = data;
    if (code !== 0) {
      // 错误提示
      store.setState({messageInfo: {type: 'error', content: msg || '服务端发生错误'}});
    }
    return response;
  },
  // ...
);

export default axiosInstance;
出问题的项目react是17.0.0版本,package.json内容是{ "private": true, "scripts": { "analyze": "cross-env ANALYZE=1 umi build", "start": "cross-env UMI_ENV=local umi dev", "build:dev": "cross-env UMI_ENV=dev umi build", "build:test": "cross-env UMI_ENV=test umi build", "build:uat": "cross-env UMI_ENV=uat umi build", "build:prod": "cross-env UMI_ENV=prod umi build", "check:model": "umi dva list model", "postinstall": "umi generate tmp", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "test": "umi-test", "test:coverage": "umi-test --coverage" }, "gitHooks": { "pre-commit": "lint-staged" }, "engines": { "node": ">= 16.14.0" }, "lint-staged": { "*.{js,jsx,less,md,json}": [ "prettier --write" ], "*.ts?(x)": [ "prettier --parser=typescript --write" ] }, "dependencies": { "@ant-design/pro-layout": "^6.5.0", "antd": "4.24.0", "antd-img-crop": "^3.16.0", "axios": "^1.6.8", "braft-editor": "^2.3.9", "braft-extensions": "^0.1.1", "classnames": "^2.2.6", "clipboard": "^2.0.8", "crypto-js": "^4.1.1", "dva-model-extend": "^0.1.2", "easy-email-core": "^4.16.1", "easy-email-editor": "^4.16.1", "jsencrypt": "3.2.1", "less-vars-to-js": "^1.3.0", "locales": "^0.0.2", "lodash": "^4.17.21", "lrz": "^4.9.41", "mjml-browser": "^4.15.3", "moment": "^2.25.3", "nprogress": "^0.2.0", "react": "17.0.0", "react-color": "^2.19.3", "react-dnd": "^14.0.4", "react-dom": "17.0.0", "react-final-form": "^6.5.9", "react-webcam": "^7.2.0", "store": "2.0.12", "umi": "3.5.20", "yarn": "^1.22.22" }, "devDependencies": { "@lingui/babel-preset-react": "^2.9.0", "@types/crypto-js": "^4.0.2", "@types/lodash": "^4.14.176", "@types/nprogress": "^0.2.0", "@types/qs": "^6.9.7", "@types/react": "17.0.0", "@types/react-dom": "17.0.0", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "@umijs/preset-react": "^2.1.7", "cross-env": "^7.0.3", "eslint": "^8.1.0", "eslint-plugin-react": "^7.26.1", "lint-staged": "^10.0.7", "prettier": "^2.2.0", "typescript": "^4.1.2", "yorkie": "^2.0.0", "zip-webpack-plugin": "^4.0.1" }, "resolutions": { "@types/react": "17.0.0", "@types/react-dom": "17.0.0", "immer": "9.0.7" }, "preinstall": "npm install --package-lock-only --ignore-scripts && npx npm-force-resolutions", "type": "module" }
03-08
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值