0


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

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

文章目录

@umi 请求相关

一个能用的请求配置

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

  1. import{ message }from'antd';import{ history }from'@umijs/max';importtype{ RequestOptions }from'@@/plugin-request/request';import{ RequestConfig }from'@@/plugin-request/request';import{LOGIN_URL}from'@/common/constant';exportconsthttpCodeDispose=async(code:string|number)=>{if(code.toString().startsWith('4')){
  2. message.error({ content:`请求错误`});if(code ===401){
  3. message.error({ content:`登录已过期,请重新登录`});
  4. history.replace({ pathname:LOGIN_URL});}if(code ===403){
  5. message.error({ content:`登录已过期,请重新登录`});
  6. localStorage.removeItem('UserInfo');
  7. history.replace({ pathname:LOGIN_URL});}}// 500状态码if(code.toString().startsWith('5')){
  8. message.error({ content:`服务器错误,请稍后再试`});}};// 运行时配置exportconst errorConfig: RequestConfig ={// 统一的请求设定
  9. timeout:20000,
  10. headers:{'X-Requested-With':'XMLHttpRequest'},// 错误处理: umi@3 的错误处理方案。
  11. errorConfig:{/**
  12. * 错误接收及处理,主要返回状态码非200,Axios错误的情况
  13. * @param error 错误类型
  14. * @param opts 请求参数,请求方法
  15. */errorHandler:async(error:any, opts:any)=>{if(opts?.skipErrorHandler)throw error;// 我们的 errorThrower 抛出的错误。if(error.response){// Axios 的错误// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围if((error.message asstring).includes('timeout')){
  16. message.error('请求错误,请检查网络');}awaithttpCodeDispose(error.response.status);}elseif(error.request){// 请求已经成功发起,但没有收到响应// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,// 而在node.js中是 http.ClientRequest 的实例// message.error('无服务器相应,请重试');}else{// 发送请求时出了点问题
  17. message.error('请求错误,请重试');}},},// 请求拦截器
  18. requestInterceptors:[(config: RequestOptions)=>{// 拦截请求配置,进行个性化处理。const userInfo =JSON.parse(localStorage.getItem('UserInfo')??'{}');const token = userInfo.token ??'';const headers ={...config.headers,'Content-Type':'application/json',
  19. Whiteverse: token,// Authorization: {// key: 'Whiteverse',// value: `Bearer ${token}`// },};return{...config, headers };},],/**
  20. * 响应拦截器,主要处理服务器返回200,但是实际请求异常的问题
  21. */
  22. responseInterceptors:[(response:any)=> response,(error:any)=>{const code = error.data.code;if(!code.toString().startsWith('2')){httpCodeDispose(code);returnPromise.reject(error);}return error;},],};

Service层 TS 类型规范

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

  1. - src > - types >
  2. service.d.ts
  3. env.d.ts
  4. module.d.ts

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

  1. typeSortOrder='descend'|'ascend'|null;/**
  2. * 通用API
  3. */declarenamespaceAPI{typeResponse<T>={
  4. message:string;
  5. code:number;
  6. data:T;};typeQuerySort<T=any>= Record<string|keyofT, SortOrder>;}declarenamespaceCOMMON{interfaceSelect{
  7. value:string;
  8. label:string;}}/**
  9. * 分页相关
  10. */declarenamespacePAGINATE{typeData<T>={ total:number; data:T};typeQuery={ current?:number; pageSize?:number};}/**
  11. * 用户服务相关
  12. */declarenamespaceUSER{/**
  13. * 用户
  14. */interfaceUser{
  15. id:string;/**
  16. * 头像
  17. */
  18. avatar:string;/**
  19. * 昵称
  20. */
  21. nickname:string;}/**
  22. * 用户基本信息
  23. */typeUserInfo= Omit<User,'roleIds'|'updatedAt'>;typeUsersQuery=PAGINATE.Query &{
  24. sort?:API.QuerySort;
  25. nickname?:string;
  26. mobile?:string;
  27. roleId?:string;};/**
  28. * 创建用户
  29. */typeCreate= Omit<User,'id'>;/**
  30. * 登录信息
  31. */interfaceLogin{
  32. Mobile:string;
  33. VerificationCode:string;}/**
  34. * 管理员登录参数
  35. */interfaceALoginParam{
  36. Mobile:string;
  37. VerificationCode:string;}/**
  38. * 验证码
  39. */interfaceCaptcha{
  40. base64:string;
  41. id:string;}}

Service层 函数定义

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

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

  1. exportasyncfunctionGetActivityList(
  2. body:ACTIVITY.ActivitiesQuery,
  3. options?:{[key:string]:any},){returnrequest<API.Response<PAGINATE.Data<ACTIVITY.Activity[]>>>(`${PREFIX_URL}/activity/list`,{
  4. method:'POST',
  5. data: body,...(options ||{}),});}

@umi 请求代理 Proxy

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

  1. /**
  2. * @name 代理的配置
  3. * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
  4. * -------------------------------
  5. * The agent cannot take effect in the production environment
  6. * so there is no configuration of the production environment
  7. * For details, please see
  8. * https://pro.ant.design/docs/deploy
  9. *
  10. * @doc https://umijs.org/docs/guides/proxy
  11. */exportdefault{// 如果需要自定义本地开发服务器 请取消注释按需调整
  12. dev:{'/api-mock/':{// 要代理的地址
  13. target:'http://127.0.0.1:4523/m1/3280694-0-default',// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个,比如 cookie
  14. changeOrigin:true,
  15. pathRewrite:{'^/api-mock':''},},'/api-sys/':{// 要代理的地址
  16. target:'http://192.168.50.131:8021',// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个,比如 cookie
  17. changeOrigin:true,
  18. pathRewrite:{'^/api-sys':''},},'/api-user/':{// 要代理的地址
  19. target:'http://192.168.50.131:8020',// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个,比如 cookie
  20. changeOrigin:true,
  21. pathRewrite:{'^/api-user':''},},},/**
  22. * @name 详细的代理配置
  23. * @doc https://github.com/chimurai/http-proxy-middleware
  24. */
  25. test:{// localhost:8000/api/** -> https://preview.pro.ant.design/api/**'/api/':{
  26. target:'https://proapi.azurewebsites.net',
  27. changeOrigin:true,
  28. pathRewrite:{'^':''},},},
  29. pre:{'/api/':{
  30. target:'your pre url',
  31. changeOrigin:true,
  32. pathRewrite:{'^':''},},},};

@umi/max 简易数据流

useModel 没有类型提示?

还原 tsconfig.json 为默认配置

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

useModel 书写规范

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

  1. export default () => {}

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

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

带有分页查询的 Model

带有loading,query,分页

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

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

  • service 的第一个参数为 { current: number, pageSize: number }
  • service 返回的数据结构为 { total: number, list: Item[] }
  • 具体看Ahooks文档,不推荐使用或二封分页Hook.
  1. import{ useEffect, useState }from'react';import{ useSetState }from'ahooks';import to from'await-to-js';import{ GetActivityList }from'@/services/activity';exportdefault()=>{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);constgetActivityData=async(_param:ACTIVITY.ActivitiesQuery)=>{// 请求前if(loading)awaitPromise.reject();// 请求中setLoading(true);const[err, res]=awaitto(GetActivityList(_param));setLoading(false);// 请求结束if(!err && res.code ===200){setActivityData(res.data.data);setTotal(res.data.total);return res.data;}else{setError(err);returnawaitPromise.reject();}};useEffect(()=>{if(!activityData)getActivityData(query);},[]);return{// 状态
  2. loading,
  3. setLoading,
  4. error,
  5. setError,
  6. query,
  7. queryChange,
  8. total,
  9. setTotal,
  10. activityData,
  11. setActivityData,// 方法
  12. getActivityData,};};

ProForm 复杂表单

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

解决方案一:

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

解决方案二:

  1. <ProForm<SysRole.Role>
  2. request={async (params) => {
  3. formRef.current?.resetFields();
  4. const res = await GetRole({id: params.id});
  5. return res.data
  6. }}
  7. >
  8. // ...
  9. </ProForm>

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

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

一个综合案例

  1. consthandleAddActivity=async(fields:ACTIVITY.Create)=>{const hide = message.loading('正在创建活动');try{const response =awaitCreateActivity({...fields });hide();
  2. message.success('活动创建成功!');return response;}catch(error){hide();
  3. message.error('添加失败,请重试!');returnPromise.reject(false);}};<StepsForm.StepForm<ACTIVITY.Create>
  4. title={"创建活动"}
  5. stepProps={{
  6. description:"请输入活动信息",}}
  7. onFinish={async(formData:ACTIVITY.Create &{ ActivityTime?:string[]})=>{try{const requestBody ={...formData };
  8. requestBody.StartTime = formData.ActivityTime![0];
  9. requestBody.EndTime = formData.ActivityTime![1]!;delete requestBody["ActivityTime"];const response =awaithandleAddActivity(requestBody);const ActivityId = response.data;
  10. uploadFormsRef.current?.setFieldValue("ActivityId", ActivityId);returnPromise.resolve(true);}catch(e){returnPromise.resolve(true);}}}/>

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

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

注意:

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

例如:

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

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

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

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

  1. {startTime:string, endTime:string}

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

EditorTable 可编辑表格

提交按钮一直Loading?

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

  1. if(!activityIdField){const errorContent ='请先创建活动';
  2. message.error(errorContent);returnPromise.reject(errorContent);}returnhandleSaveRow(record);

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

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

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

form / formRef 的 setFieldValue / getFieldsValue 无效?

原因一:

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

原因二:

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

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

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

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

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

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

Upload / ProUploader 文件上传

ImgCrop 实现图片裁切

实现功能:

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

工具函数

  1. functiongetImageFileAsync(file: File):Promise<{
  2. width:number;
  3. height:number;
  4. aspectRatio:number;
  5. image: HTMLImageElement;}>{returnnewPromise((resolve, reject)=>{const reader =newFileReader();const img =newImage();
  6. reader.onload=()=>{
  7. img.src = reader.result asstring;};
  8. img.onload=()=>{const width = img.width;const height = img.height;const aspectRatio = width / height;resolve({
  9. width,
  10. height,
  11. aspectRatio,
  12. image: img,});};
  13. img.onerror=()=>{reject(newError('图片加载失败'));};
  14. reader.onerror=()=>{reject(newError('文件读取错误'));};// 读取文件内容
  15. reader.readAsDataURL(file);});}

组件

  1. 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'];interfacePictureUploadProps{// 上传最大数量
  2. maxCount?:number;// 文件更新
  3. filesChange?:(files: UploadFile[])=>void;// 图片最小大小,宽,高
  4. minImageSize?:number[];// 图片裁切组件配置
  5. imgCropProps?: Omit<ImgCropProps,'children'>;// 上传提示内容文本
  6. children?: ReactNode | ReactNode[];}const PictureUpload:FC<PictureUploadProps>=({
  7. maxCount,
  8. filesChange,
  9. minImageSize,
  10. imgCropProps,
  11. 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);};consthandleCancel=()=>setPreviewOpen(false);consthandlePreview=async(file: UploadFile)=>{if(!file.url &&!file.preview){
  12. file.preview =awaitgetBase64(file.originFileObj as RcFile);}setPreviewImage(file.url ||(file.preview asstring));setPreviewOpen(true);setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('/')+1));};return(<><ImgCrop
  13. quality={1}
  14. zoomSlider={true}
  15. minZoom={1}
  16. maxZoom={maxZoom}
  17. aspect={minImageSize && minImageSize[0]/ minImageSize[1]}
  18. beforeCrop={async(file)=>{
  19. isCropRef.current =false;// 判断文件类型const typeMatch = fileTypes.some((type)=> type === file.type);if(!typeMatch){await message.error('图片格式仅支持'+
  20. fileTypes.reduce((prev, cur, index, array)=> prev + cur +(index === array.length -1?'':','),'',),);returnfalse;}// 判断图片大小限制if(minImageSize){const{ width: imageWidth, height: imageHeight }=awaitgetImageFileAsync(file);if(imageWidth < minImageSize[0]){await message.error(`当前图片宽度为${imageWidth}像素,请上传不小于${minImageSize[0]}像素的图片.`,);returnfalse;}if(imageHeight < minImageSize[1]){await message.error(`当前图片高度为${imageHeight}像素,请上传不小于${minImageSize[1]}像素的图片.`,);returnfalse;}// 计算最大缩放比例const widthMaxZoom =Number((imageWidth / minImageSize[0]).toFixed(1));const heightMaxZoom =Number((imageHeight / minImageSize[1]).toFixed(1));setMaxZoom(Math.min(widthMaxZoom, heightMaxZoom));}
  21. isCropRef.current =true;returntrue;}}{...imgCropProps}><Upload
  22. action="/"
  23. listType="picture-card"
  24. fileList={fileList}
  25. onPreview={handlePreview}
  26. onChange={(files)=>{handleChange(files);console.log(files);}}
  27. maxCount={maxCount}
  28. accept={'.jpg, .jpeg, .png'}
  29. 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></>);};exportdefault 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。

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

打印 ref.current

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

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

灵活使用 current、onCurrentChange、submitter属性

  1. const[currentStep, setCurrentStep]=useState<number>(0);return(<StepsForm
  2. current={currentStep}
  3. onCurrentChange={setCurrentStep}
  4. submitter={{render:(props)=>{switch(props.step){case0:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
  5. 下一步
  6. </Button>);}case1:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
  7. 下一步
  8. </Button>);}case2:{return(<Button
  9. type="primary"
  10. onClick={()=>{setCurrentStep(0);onCancel();}}>
  11. 完成
  12. </Button>);}}},}}
  13. stepsProps={{ direction:'horizontal', style:{ padding:'0 50px'}}}>{// StepForm }</StepsForm>)

微前端 Qiankun

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

子应用配置(@umi)

一、使用umi创建React App

二、配置umi

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

  1. import{ defineConfig }from'umi';exportdefaultdefineConfig({
  2. title:'xxxxxx',
  3. routes:[{
  4. path:'/',
  5. component:'index',},{ path:'/scene-obj', component:'OBJScene'},{ path:'/*', redirect:'/'},],
  6. npmClient:'pnpm',
  7. proxy:{'/api':{
  8. target:'http://jsonplaceholder.typicode.com/',
  9. changeOrigin:true,
  10. pathRewrite:{'^/api':''},},},
  11. plugins:['@umijs/plugins/dist/model','@umijs/plugins/dist/qiankun','@umijs/plugins/dist/request',],
  12. model:{},
  13. qiankun:{
  14. slave:{},},
  15. request:{
  16. dataField:'data',},
  17. mfsu:{
  18. mfName:'umiR3f',// 默认的会冲突,所以需要随便取个名字避免冲突},chainWebpack(config){
  19. config.set('experiments',{...config.get('experiments'),
  20. asyncWebAssembly:true,});constREG=/\.wasm$/;
  21. config.module.rule('asset').exclude.add(REG).end();
  22. config.module
  23. .rule('wasm').test(REG).exclude.add(/node_modules/).end().type('webassembly/async').end();},});

三、跨域配置

  1. importtype{ IApi }from'umi';exportdefault(api: IApi)=>{// 中间件支持 cors
  2. api.addMiddlewares(()=>{returnfunctioncors(req, res, next){
  3. res.setHeader('Access-Control-Allow-Origin','*');
  4. res.setHeader('Access-Control-Allow-Headers','*');next();};});
  5. api.onBeforeMiddleware(({ app })=>{
  6. app.request.headers['access-control-allow-origin']='*';
  7. app.request.headers['access-control-allow-headers']='*';
  8. app.request.headers['access-control-allow-credentials']='*';
  9. app.request.originalUrl ='*';});};

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

  1. exportconst qiankun ={// 应用加载之前asyncbootstrap(props:any){console.log('app1 bootstrap', props);},// 应用 render 之前触发asyncmount(props:any){console.log('app1 mount', props);},// 应用卸载之后触发asyncunmount(props:any){console.log('app1 unmount', props);},};

父应用配置(@umi/max)

config.ts

  1. exportdefaultdefineConfig({
  2. qiankun:{
  3. master:{
  4. apps:[{
  5. name:'r3f-viewer',// 子应用的名称
  6. entry:'http://localhost:5174',// your microApp address},],},},})

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

  1. exportdefault[{
  2. name:'slave',
  3. path:'/slave/*',
  4. microApp:'slave',
  5. microAppProps:{
  6. autoSetLoading:true,
  7. autoCaptureError:true,
  8. className:'MicroApp',
  9. wrapperClassName:'MicroAppWrapper'},},]

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

index.tsx

  1. import{ PageContainer }from'@ant-design/pro-components';import{ memo }from'react';import{ MicroAppWithMemoHistory }from'@umijs/max';import'./index.less';constRole=()=>{return(<PageContainer><MicroAppWithMemoHistory
  2. name="r3f-viewer"
  3. url="/umi-r3f-view"
  4. autoSetLoading={true}
  5. className={'microApp'}/></PageContainer>);};exportdefaultmemo(Role);

index.less

  1. .microApp,
  2. #root{min-height: 800px !important;height: 800px !important;max-height: 800px !important;width: 100% !important;}

本文转载自: https://blog.csdn.net/qq_36833171/article/details/134790884
版权归原作者 流水吾情 所有, 如有侵权,请联系我们删除。

“前端初学者的Ant Design Pro V6总结(下)”的评论:

还没有评论