前端初学者的Ant Design Pro V6总结(下)

前端初学者的Ant Design Pro V6总结(上)
前端初学者的Ant Design Pro V6总结(下)

@umi 请求相关

一个能用的请求配置

Antd Pro的默认的请求配置太复杂了,我写了个简单的,能用,有需要可以做进一步拓展。

import { message } from 'antd';
import { history } from '@umijs/max';
import type { RequestOptions } from '@@/plugin-request/request';
import { RequestConfig } from '@@/plugin-request/request';
import { LOGIN_URL } from '@/common/constant';

export const httpCodeDispose = async (code: string | number) => {
  if (code.toString().startsWith('4')) {
    message.error({ content: `请求错误` });
    if (code === 401) {
      message.error({ content: `登录已过期,请重新登录` });
      history.replace({ pathname: LOGIN_URL });
    }
    if (code === 403) {
      message.error({ content: `登录已过期,请重新登录` });
      localStorage.removeItem('UserInfo');
      history.replace({ pathname: LOGIN_URL });
    }
  }
  // 500状态码
  if (code.toString().startsWith('5')) {
    message.error({ content: `服务器错误,请稍后再试` });
  }
};

// 运行时配置
export const errorConfig: RequestConfig = {
  // 统一的请求设定
  timeout: 20000,
  headers: { 'X-Requested-With': 'XMLHttpRequest' },

  // 错误处理: umi@3 的错误处理方案。
  errorConfig: {
    /**
     * 错误接收及处理,主要返回状态码非200,Axios错误的情况
     * @param error 错误类型
     * @param opts 请求参数,请求方法
     */
    errorHandler: async (error: any, opts: any) => {
      if (opts?.skipErrorHandler) throw error;
      // 我们的 errorThrower 抛出的错误。
      if (error.response) {
        // Axios 的错误
        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
        if ((error.message as string).includes('timeout')) {
          message.error('请求错误,请检查网络');
        }
        await httpCodeDispose(error.response.status);
      } else if (error.request) {
        // 请求已经成功发起,但没有收到响应
        // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
        // 而在node.js中是 http.ClientRequest 的实例
        // message.error('无服务器相应,请重试');
      } else {
        // 发送请求时出了点问题
        message.error('请求错误,请重试');
      }
    },
  },

  // 请求拦截器
  requestInterceptors: [
    (config: RequestOptions) => {
      // 拦截请求配置,进行个性化处理。
      const userInfo = JSON.parse(localStorage.getItem('UserInfo') ?? '{}');
      const token = userInfo.token ?? '';
      const headers = {
        ...config.headers,
        'Content-Type': 'application/json',
        Whiteverse: token,
        // Authorization: {
        //   key: 'Whiteverse',
        //   value: `Bearer ${token}`
        // },
      };
      return { ...config, headers };
    },
  ],
  /**
   * 响应拦截器,主要处理服务器返回200,但是实际请求异常的问题
   */
  responseInterceptors: [
    (response: any) => response,
    (error: any) => {
      const code = error.data.code;
      if (!code.toString().startsWith('2')) {
        httpCodeDispose(code);
        return Promise.reject(error);
      }
      return error;
    },
  ],
};

Service层 TS 类型规范

目前团队采用 [name].d.ts 的方式定义公用类型

- src > - types > 
	service.d.ts
	env.d.ts
	module.d.ts

服务层命名 nameplace 要求全部大写

type SortOrder = 'descend' | 'ascend' | null;

/**
 * 通用API
 */
declare namespace API {
  type Response<T> = {
    message: string;
    code: number;
    data: T;
  };

  type QuerySort<T = any> = Record<string | keyof T, SortOrder>;
}

declare namespace COMMON {
  interface Select {
    value: string;
    label: string;
  }
}

/**
 * 分页相关
 */
declare namespace PAGINATE {
  type Data<T> = { total: number; data: T };
  type Query = { current?: number; pageSize?: number };
}

/**
 * 用户服务相关
 */
declare namespace USER {
  /**
   * 用户
   */
  interface User {
    id: string;
    /**
     * 头像
     */
    avatar: string;
    /**
     * 昵称
     */
    nickname: string;
  }

  /**
   * 用户基本信息
   */
  type UserInfo = Omit<User, 'roleIds' | 'updatedAt'>;

  type UsersQuery = PAGINATE.Query & {
    sort?: API.QuerySort;
    nickname?: string;
    mobile?: string;
    roleId?: string;
  };

  /**
   * 创建用户
   */
  type Create = Omit<User, 'id'>;

  /**
   * 登录信息
   */
  interface Login {
    Mobile: string;
    VerificationCode: string;
  }

  /**
   * 管理员登录参数
   */
  interface ALoginParam {
    Mobile: string;
    VerificationCode: string;
  }

  /**
   * 验证码
   */
  interface Captcha {
    base64: string;
    id: string;
  }
}

Service层 函数定义

  1. 为了与普通的函数做区别,方法名全部大写
  2. 使用 PREFIX_URL 请求前缀,方便后期维护

src -> services -> activity -> index.ts

export async function GetActivityList(
  body: ACTIVITY.ActivitiesQuery,
  options?: { [key: string]: any },
) {
  return request<API.Response<PAGINATE.Data<ACTIVITY.Activity[]>>>(`${PREFIX_URL}/activity/list`, {
    method: 'POST',
    data: body,
    ...(options || {}),
  });
}

@umi 请求代理 Proxy

在开发阶段,如果后端服务的端口经常发生变化,可以使用umi 请求代理 替换原有的请求前缀,转发请求。

/**
 * @name 代理的配置
 * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
 * -------------------------------
 * The agent cannot take effect in the production environment
 * so there is no configuration of the production environment
 * For details, please see
 * https://pro.ant.design/docs/deploy
 *
 * @doc https://umijs.org/docs/guides/proxy
 */
export default {
  // 如果需要自定义本地开发服务器  请取消注释按需调整
  dev: {
    '/api-mock/': {
      // 要代理的地址
      target: 'http://127.0.0.1:4523/m1/3280694-0-default',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-mock': '' },
    },
    '/api-sys/': {
      // 要代理的地址
      target: 'http://192.168.50.131:8021',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-sys': '' },
    },
    '/api-user/': {
      // 要代理的地址
      target: 'http://192.168.50.131:8020',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-user': '' },
    },
  },

  /**
   * @name 详细的代理配置
   * @doc https://github.com/chimurai/http-proxy-middleware
   */
  test: {
    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
    '/api/': {
      target: 'https://proapi.azurewebsites.net',
      changeOrigin: true,
      pathRewrite: { '^': '' },
    },
  },
  pre: {
    '/api/': {
      target: 'your pre url',
      changeOrigin: true,
      pathRewrite: { '^': '' },
    },
  },
};

@umi/max 简易数据流

useModel 没有类型提示?

还原 tsconfig.json 为默认配置

{
  "extends": "./src/.umi/tsconfig.json"
}

useModel 书写规范

定义Model仓库时,推荐使用匿名默认导出语法

export default () => {}

如果为页面绑定Model,注意页面的层级不要过深,页面组件的名称尽量短

  • 文件名定义
- pages
	- Activity
		- components
			- ActivityList.tsx
		- models
			- ActivityModels.ts
  • 使用Model
const { getActivityData } = useModel('Activity.ActivityModels', (models) => ({
	getActivityData: models.getActivityData,
}));

带有分页查询的 Model

带有loading,query,分页

可使用Ahooks 的 useRequest 或 自定封装 useRequest

注意Ahooks的 usePagination函数 对Service层的参数有要求

  • service 的第一个参数为 { current: number, pageSize: number }
  • service 返回的数据结构为 { total: number, list: Item[] }
  • 具体看Ahooks文档,不推荐使用或二封分页Hook.
import { useEffect, useState } from 'react';
import { useSetState } from 'ahooks';
import to from 'await-to-js';
import { GetActivityList } from '@/services/activity';

export default () => {
  const initialParam = { current: 1, pageSize: 20 };
  const [query, queryChange] = useSetState<ACTIVITY.ActivitiesQuery>(initialParam);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<Error | null>();

  const [activityData, setActivityData] = useState<ACTIVITY.Activity[]>();
  const [total, setTotal] = useState<number>(0);
  const getActivityData = async (_param: ACTIVITY.ActivitiesQuery) => {
    // 请求前
    if (loading) await Promise.reject();

    // 请求中
    setLoading(true);
    const [err, res] = await to(GetActivityList(_param));
    setLoading(false);

    // 请求结束
    if (!err && res.code === 200) {
      setActivityData(res.data.data);
      setTotal(res.data.total);
      return res.data;
    } else {
      setError(err);
      return await Promise.reject();
    }
  };

  useEffect(() => {
    if (!activityData) getActivityData(query);
  }, []);

  return {
    // 状态
    loading,
    setLoading,
    error,
    setError,
    query,
    queryChange,
    total,
    setTotal,
    activityData,
    setActivityData,
    // 方法
    getActivityData,
  };
};

ProForm 复杂表单

当外部数据发生变化,ProForm不更新?

解决方案一:

// 监测外部值的变化,更新表单内的数据
useEffect(() => formRef.current && formRef.current.setFieldsValue(selectedNode), [selectedNode]);

解决方案二:

<ProForm<SysRole.Role>
	request={async (params) => {
		formRef.current?.resetFields();
		const res = await GetRole({id: params.id});
		return res.data
	}}
>
// ...	
</ProForm>

ProForm onFinish中请求错误,提交按钮一直Loading

onFinish 方法需要返回一个Promise.resolve(boolean),reject时,会一直loading

一个综合案例

const handleAddActivity = async (fields: ACTIVITY.Create) => {
	const hide = message.loading('正在创建活动');
	try {
		const response = await CreateActivity({ ...fields });
		hide();
		message.success('活动创建成功!');
		return response;
	} catch (error) {
		hide();
		message.error('添加失败,请重试!');
		return Promise.reject(false);
	}
};

<StepsForm.StepForm<ACTIVITY.Create>
  title={"创建活动"}
  stepProps={{
    description: "请输入活动信息",
  }}
  onFinish={async (formData: ACTIVITY.Create & { ActivityTime?: string[] }) => {
    try {
      const requestBody = { ...formData };
      requestBody.StartTime = formData.ActivityTime![0];
      requestBody.EndTime = formData.ActivityTime![1]!;
      delete requestBody["ActivityTime"];
      const response = await handleAddActivity(requestBody);
      const ActivityId = response.data;
      uploadFormsRef.current?.setFieldValue("ActivityId", ActivityId);
      return Promise.resolve(true);
    } catch (e) {
      return Promise.resolve(true);
    }
  }}
/>

更加优雅的办法是给onFinish 提交的数据添加一个convertValues

const convertValues = useMemo((values: FormColumn) => {
    return { ...values };
}, []);

注意:

ProForm中的transform和convertValue属性,仅能操作本字段内容,这个特性在某种情况下会出现一些问题

例如:

<ProFormDateTimeRangePicker
    name="ActivityTime"
    label="投放时间"
    width={'lg'}
    rules={[{required: true, message: '请选择活动投放时间!'}]}
    dataFormat={FORMAT_DATE_TIME_CN}
/>

时间范围组件返回的数据格式是

ActivityTime: string[] // 如果不给dataFormat,就是 Dayjs[]

如果后端接口的数据格式是

{startTime: string, endTime: string}

这个时候如果使用convertValue无法解决业务问题,需要在onFinish或onSubmit中进行数据转化。

EditorTable 可编辑表格

提交按钮一直Loading?

如果onSave时网络请求错误或者发生异常,返回Promise.reject,onSave就不会生效。

if (!activityIdField) {
	const errorContent = '请先创建活动';
	message.error(errorContent);
	return Promise.reject(errorContent);
}

return handleSaveRow(record);

columns 自定义表单、自定义渲染

 const columns: ProColumns<DataSourceType>[] = [
    {
        title: '模型文件',
        dataIndex: '_File',
        width: 150,
        render: (_, entity) => {
          return (
            <Button
              type={'link'}
              onClick={() => {
                downloadFile(entity._File!.originFileObj!);
              }}
            >
              {entity._File?.name}
            </Button>
          );
        },
        formItemProps: {
          valuePropName: 'file',
          trigger: 'fileChange',
          rules: [{ required: true, message: '此项是必填项.' }],
        },
        renderFormItem: () => <ModelUploadButton />,
    }   
]

formItemProps 它本质就是<Form.Item>,基本照着Form.Item那边去配置就行。

form / formRef 的 setFieldValue / getFieldsValue 无效?

原因一:

由于EditorTable的 Form实际上是新增的一行,是动态的,formRef 更新不及时可能导致formRef.current 为 undefined。

原因二:

普通的form组件内部的数据模型形如这样:

{
    "homePath": "/",
    "status": true,
    "sort": 1
}

但是editorForm在编辑时内部的数据模型是这样的:

{
    "229121": {
        "ModelLoadName": "11",
        "ModelShowName": "222",
        "ModelNo": "333",
        "MobileOS": "android",
        "_Position": [
            {
                "position": [
                    123.42932734052755,
                    41.79745486673118
                ]
            }
        ],
    }
}

它在外面包了一层,因此设置列的时候需要这么写

renderFormItem: (schema, config, form, action) => {
    const fieldsValue = form.getFieldsValue()
    const key = Object.keys(fieldsValue)[0];
    const fields = fieldsValue[key];
    const fieldName = schema.dataIndex! as keyof typeof fields // you want setting field
    fields[fieldName] = 'you want setting value';
    formRef?.current?.setFieldValue(key, fields);
    return <Component />
},

Upload / ProUploader 文件上传

ImgCrop 实现图片裁切

实现功能:

  • 文件格式限制
  • 文件上传尺寸限制
  • 文件缩放大小限制

工具函数

function getImageFileAsync(file: File): Promise<{
  width: number;
  height: number;
  aspectRatio: number;
  image: HTMLImageElement;
}> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    const img = new Image();

    reader.onload = () => {
      img.src = reader.result as string;
    };

    img.onload = () => {
      const width = img.width;
      const height = img.height;
      const aspectRatio = width / height;
      resolve({
        width,
        height,
        aspectRatio,
        image: img,
      });
    };

    img.onerror = () => {
      reject(new Error('图片加载失败'));
    };

    reader.onerror = () => {
      reject(new Error('文件读取错误'));
    };
    // 读取文件内容
    reader.readAsDataURL(file);
  });
}

组件

import { FC, ReactNode, useRef, useState } from 'react';
import { message, Modal, Upload, UploadFile, UploadProps } from 'antd';
import ImgCrop, { ImgCropProps } from 'antd-img-crop';
import { RcFile } from 'antd/es/upload';
import { getBase64, getImageFileAsync } from '@/utils/common';

const fileTypes = ['image/jpg', 'image/jpeg', 'image/png'];

interface PictureUploadProps {
  // 上传最大数量
  maxCount?: number;
  // 文件更新
  filesChange?: (files: UploadFile[]) => void;
  // 图片最小大小,宽,高
  minImageSize?: number[];
  // 图片裁切组件配置
  imgCropProps?: Omit<ImgCropProps, 'children'>;
  // 上传提示内容文本
  children?: ReactNode | ReactNode[];
}

const PictureUpload: FC<PictureUploadProps> = ({
  maxCount,
  filesChange,
  minImageSize,
  imgCropProps,
  children,
}) => {
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState('');
  const [previewTitle, setPreviewTitle] = useState('');
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const [maxZoom, setMaxZoom] = useState(2);
  const isCropRef = useRef<boolean>(false);

  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    setFileList(newFileList);
    if (filesChange) filesChange(fileList);
  };

  const handleCancel = () => setPreviewOpen(false);

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as RcFile);
    }

    setPreviewImage(file.url || (file.preview as string));
    setPreviewOpen(true);
    setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1));
  };

  return (
    <>
      <ImgCrop
        quality={1}
        zoomSlider={true}
        minZoom={1}
        maxZoom={maxZoom}
        aspect={minImageSize && minImageSize[0] / minImageSize[1]}
        beforeCrop={async (file) => {
          isCropRef.current = false;
          // 判断文件类型
          const typeMatch = fileTypes.some((type) => type === file.type);
          if (!typeMatch) {
            await message.error(
              '图片格式仅支持' +
                fileTypes.reduce(
                  (prev, cur, index, array) => prev + cur + (index === array.length - 1 ? '' : ','),
                  '',
                ),
            );
            return false;
          }
          // 判断图片大小限制
          if (minImageSize) {
            const { width: imageWidth, height: imageHeight } = await getImageFileAsync(file);
            if (imageWidth < minImageSize[0]) {
              await message.error(
                `当前图片宽度为${imageWidth}像素,请上传不小于${minImageSize[0]}像素的图片.`,
              );
              return false;
            }
            if (imageHeight < minImageSize[1]) {
              await message.error(
                `当前图片高度为${imageHeight}像素,请上传不小于${minImageSize[1]}像素的图片.`,
              );
              return false;
            }
            // 计算最大缩放比例
            const widthMaxZoom = Number((imageWidth / minImageSize[0]).toFixed(1));
            const heightMaxZoom = Number((imageHeight / minImageSize[1]).toFixed(1));
            setMaxZoom(Math.min(widthMaxZoom, heightMaxZoom));
          }
          isCropRef.current = true;
          return true;
        }}
        {...imgCropProps}
      >
        <Upload
          action="/"
          listType="picture-card"
          fileList={fileList}
          onPreview={handlePreview}
          onChange={(files) => {
            handleChange(files);
            console.log(files);
          }}
          maxCount={maxCount}
          accept={'.jpg, .jpeg, .png'}
          beforeUpload={async (file) => {
            if (!isCropRef.current) return Upload.LIST_IGNORE;
            return file;
          }}
        >
          {maxCount ? fileList.length < maxCount && children : children}
        </Upload>
      </ImgCrop>
      <Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}>
        <img alt="example" style={{ width: '100%' }} src={previewImage} />
      </Modal>
    </>
  );
};

export default PictureUpload;

ImgCrop 组件注意事项

  • 拦截裁切事件

    • ImgCrop 组件 的 beforeCrop 返回 false 后不再弹出模态框,但是文件会继续走 Upload 的 beforeUpload 流程,如果想要拦截上传事件,需要在beforeUpload 中返回 Upload.LIST_IGNORE
    • 判断是否拦截的状态变量需要用 useRef ,useState测试无效。
  • Upload组件 配合 ImgCrop组件时,一定要在 beforeUpload 中返回 事件回调中的 file,否则裁切无效。

  • 如果不想做像素压缩,设置quality={1}

StepsForm 分布表单

如何在 StepsForm 中 更新子表单?

通过StepsForm的 formMapRef 属性,它可以拿到子StepForm的全部ref。

const stepFormMapRef = useRef<Array<MutableRefObject<ProFormInstance>>>([]);
return <StepsForm formMapRef={stepFormMapRef} />

打印 ref.current

[
    {
        "current": {
            // getFieldError: f(name)
        }
    },
    {
        "current": {
            // getFieldError: f(name)
        }
    },
    {
        "current": {
            // getFieldError: f(name)
        }
    }
]

如何手动控制 步骤 前进、后退?

灵活使用 current、onCurrentChange、submitter属性

const [currentStep, setCurrentStep] = useState<number>(0);

return (
	<StepsForm 
		current={currentStep}
		onCurrentChange={setCurrentStep}
		submitter={{
          render: (props) => {
            switch (props.step) {
              case 0: {
                return (
                  <Button type="primary" onClick={() => props.onSubmit?.()}>
                    下一步
                  </Button>
                );
              }
              case 1: {
                return (
                  <Button type="primary" onClick={() => props.onSubmit?.()}>
                    下一步
                  </Button>
                );
              }
              case 2: {
                return (
                  <Button
                    type="primary"
                    onClick={() => {
                      setCurrentStep(0);
                      onCancel();
                    }}
                  >
                    完成
                  </Button>
                );
              }
            }
          },
        }}
        stepsProps={{ direction: 'horizontal', style: { padding: '0 50px' } }}
	>
		{ // StepForm }
	</StepsForm>
)

微前端 Qiankun

文档:https://umijs.org/docs/max/micro-frontend

子应用配置(@umi)

一、使用umi创建React App

二、配置umi

这里有一些WASM的配置,不想要可以去掉

import { defineConfig } from 'umi';

export default defineConfig({
  title: 'xxxxxx',
  routes: [
    {
      path: '/',
      component: 'index',
    },
    { path: '/scene-obj', component: 'OBJScene' },
    { path: '/*', redirect: '/' },
  ],
  npmClient: 'pnpm',
  proxy: {
    '/api': {
      target: 'http://jsonplaceholder.typicode.com/',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  },
  plugins: [
    '@umijs/plugins/dist/model',
    '@umijs/plugins/dist/qiankun',
    '@umijs/plugins/dist/request',
  ],
  model: {},
  qiankun: {
    slave: {},
  },
  request: {
    dataField: 'data',
  },
  mfsu: {
    mfName: 'umiR3f', // 默认的会冲突,所以需要随便取个名字避免冲突
  },
  chainWebpack(config) {
    config.set('experiments', {
      ...config.get('experiments'),
      asyncWebAssembly: true,
    });

    const REG = /\.wasm$/;

    config.module.rule('asset').exclude.add(REG).end();

    config.module
      .rule('wasm')
      .test(REG)
      .exclude.add(/node_modules/)
      .end()
      .type('webassembly/async')
      .end();
  },
});

三、跨域配置

import type { IApi } from 'umi';

export default (api: IApi) => {
  // 中间件支持 cors
  api.addMiddlewares(() => {
    return function cors(req, res, next) {
      res.setHeader('Access-Control-Allow-Origin', '*');
      res.setHeader('Access-Control-Allow-Headers', '*');
      next();
    };
  });
  api.onBeforeMiddleware(({ app }) => {
    app.request.headers['access-control-allow-origin'] = '*';
    app.request.headers['access-control-allow-headers'] = '*';
    app.request.headers['access-control-allow-credentials'] = '*';
    app.request.originalUrl = '*';
  });
};

四、修改app.ts,子应用配置生命周期钩子.

export const qiankun = {
  // 应用加载之前
  async bootstrap(props: any) {
    console.log('app1 bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props: any) {
    console.log('app1 mount', props);
  },
  // 应用卸载之后触发
  async unmount(props: any) {
    console.log('app1 unmount', props);
  },
};

父应用配置(@umi/max)

config.ts

export default defineConfig({
  qiankun: {
    master: {
      apps: [
        {
          name: 'r3f-viewer', // 子应用的名称
          entry: 'http://localhost:5174', // your microApp address
        },
      ],
    },
  },
})

使用路由的方式引入子应用

export default [
  {
    name: 'slave',
    path: '/slave/*',
    microApp: 'slave',
    microAppProps: {
      autoSetLoading: true,
      autoCaptureError: true,
      className: 'MicroApp',
      wrapperClassName: 'MicroAppWrapper'
    },
  },
]

使用组件的方式引入子应用

index.tsx

import { PageContainer } from '@ant-design/pro-components';
import { memo } from 'react';
import { MicroAppWithMemoHistory } from '@umijs/max';
import './index.less';

const Role = () => {
  return (
    <PageContainer>
      <MicroAppWithMemoHistory
        name="r3f-viewer"
        url="/umi-r3f-view"
        autoSetLoading={true}
        className={'microApp'}
      />
    </PageContainer>
  );
};

export default memo(Role);

index.less

.microApp,
#root {
  min-height: 800px !important;
  height: 800px !important;
  max-height: 800px !important;
  width: 100% !important;
}
  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Ant Design 是一个很流行的 UI 设计语言和 React 组件库。但是,它也有一些缺点,包括: 1. 包体积较大:Ant Design 的包体积较大,如果只是为了使用几个组件,使用它可能会带来较大的代价。 2. 使用学习曲线较陡:Ant Design 的 API 设计较复杂,对于新手来说,学习曲线可能较陡。 3. 个性化能力较差:Ant Design 的组件和样式都是按照它的设计理念来实现的,如果想要对组件进行大量的定制和修改,可能会受到一定的限制。 4. 升级频繁:Ant Design 经常更新和升级,如果你的项目使用的是旧版本的 Ant Design,那么在升级到新版本时可能需要花费一定的时间来调整代码。 希望这些信息对你有帮助。 ### 回答2: Ant Design 是一款流行的前端 UI 库,提供了丰富的组件和样式供开发者使用。尽管 Ant Design 有很多优点,但也存在一些缺点。 首先,Ant Design 的样式风格比较统一,这在一些特定的项目中可能会显得过于相似。某些开发者可能希望为自己的项目选择不同的风格,但在 Ant Design 中这很难实现。这意味着在定制化需求较高的项目中,可能需要耗费更多的时间和精力来调整样式。 其次,Ant Design 的组件库非常庞大,其中包含了大量的组件和功能。尽管这是一个优点,但对于新手来说可能会感到压倒性。学习和理解如此庞大的组件库需要花费相当长的时间。对于一些小型项目或者只需要使用一两个组件的开发者来说,Ant Design 的规模可能过于庞大,不太适合他们的需求。 此外,Ant Design 的文档和示例相对较少。在初学阶段,开发者通常依赖文档来学习和使用一个新的技术或库。Ant Design 的文档并不是很详细,不够清晰地解释每个组件的用法和特性。开发者可能需要花费更多的时间在官方文档以外寻找其他资源来弥补这一不足。 总结来说,尽管 Ant Design 是一款功能强大的 UI 库,但它也存在一些缺点。样式风格统一、组件库庞大且不易定制、文档不够详细是其中的三个主要缺点。开发者在选择是否使用 Ant Design 时需要根据自己的项目需求综合考虑。 ### 回答3: Ant Design 是一个广泛使用的UI框架,拥有很多出色的特点和优势。然而,它也存在一些缺点需要考虑。 首先,Ant Design 是一个相对庞大的框架,它的文件大小相对较大。这可能导致网页加载速度变慢,特别是对于访问速度较慢的用户来说。对于那些对页面性能要求非常高的项目,这可能是一个问题。 其次,Ant Design 的学习曲线可能相对陡峭。用户需要熟悉 Ant Design 的各种组件、API 和样式系统,才能发挥其最大的潜力。对于新手来说,这可能需要一些时间和精力来掌握。 此外,Ant Design 的定制化选项相对有限。虽然 Ant Design 提供了一些自定义主题的能力,但对于一些特殊需求的项目来说,可能还需要额外的定制。在这种情况下,可能需要花费更多的时间和资源来适应项目需求。 最后,Ant Design 的文档和社区支持相对较好,但仍然可能存在一些问题或疑惑没有得到及时解决。对于一些特殊场景或较新的功能,可能需要自行探索或依赖开发者社区的支持。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值