前端初学者的Ant Design Pro V6总结(上)
前端初学者的Ant Design Pro V6总结(下)
文章目录
@umi 请求相关
一个能用的请求配置
Antd Pro的默认的请求配置太复杂了,我写了个简单的,能用,有需要可以做进一步拓展。
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')){
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:`服务器错误,请稍后再试`});}};// 运行时配置exportconst 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 asstring).includes('timeout')){
message.error('请求错误,请检查网络');}awaithttpCodeDispose(error.response.status);}elseif(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);returnPromise.reject(error);}return error;},],};
Service层 TS 类型规范
目前团队采用 [name].d.ts 的方式定义公用类型
- src > - types >
service.d.ts
env.d.ts
module.d.ts
服务层命名 nameplace 要求全部大写
typeSortOrder='descend'|'ascend'|null;/**
* 通用API
*/declarenamespaceAPI{typeResponse<T>={
message:string;
code:number;
data:T;};typeQuerySort<T=any>= Record<string|keyofT, SortOrder>;}declarenamespaceCOMMON{interfaceSelect{
value:string;
label:string;}}/**
* 分页相关
*/declarenamespacePAGINATE{typeData<T>={ total:number; data:T};typeQuery={ current?:number; pageSize?:number};}/**
* 用户服务相关
*/declarenamespaceUSER{/**
* 用户
*/interfaceUser{
id:string;/**
* 头像
*/
avatar:string;/**
* 昵称
*/
nickname:string;}/**
* 用户基本信息
*/typeUserInfo= Omit<User,'roleIds'|'updatedAt'>;typeUsersQuery=PAGINATE.Query &{
sort?:API.QuerySort;
nickname?:string;
mobile?:string;
roleId?:string;};/**
* 创建用户
*/typeCreate= Omit<User,'id'>;/**
* 登录信息
*/interfaceLogin{
Mobile:string;
VerificationCode:string;}/**
* 管理员登录参数
*/interfaceALoginParam{
Mobile:string;
VerificationCode:string;}/**
* 验证码
*/interfaceCaptcha{
base64:string;
id:string;}}
Service层 函数定义
- 为了与普通的函数做区别,方法名全部大写
- 使用 PREFIX_URL 请求前缀,方便后期维护
src -> services -> activity -> index.ts
exportasyncfunctionGetActivityList(
body:ACTIVITY.ActivitiesQuery,
options?:{[key:string]:any},){returnrequest<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
*/exportdefault{// 如果需要自定义本地开发服务器 请取消注释按需调整
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';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{// 状态
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
一个综合案例
consthandleAddActivity=async(fields:ACTIVITY.Create)=>{const hide = message.loading('正在创建活动');try{const response =awaitCreateActivity({...fields });hide();
message.success('活动创建成功!');return response;}catch(error){hide();
message.error('添加失败,请重试!');returnPromise.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 =awaithandleAddActivity(requestBody);const ActivityId = response.data;
uploadFormsRef.current?.setFieldValue("ActivityId", ActivityId);returnPromise.resolve(true);}catch(e){returnPromise.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);returnPromise.reject(errorContent);}returnhandleSaveRow(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!askeyoftypeof fields // you want setting field
fields[fieldName]='you want setting value';
formRef?.current?.setFieldValue(key, fields);return<Component />},
Upload / ProUploader 文件上传
ImgCrop 实现图片裁切
实现功能:
- 文件格式限制
- 文件上传尺寸限制
- 文件缩放大小限制
工具函数
functiongetImageFileAsync(file: File):Promise<{
width:number;
height:number;
aspectRatio:number;
image: HTMLImageElement;}>{returnnewPromise((resolve, reject)=>{const reader =newFileReader();const img =newImage();
reader.onload=()=>{
img.src = reader.result asstring;};
img.onload=()=>{const width = img.width;const height = img.height;const aspectRatio = width / height;resolve({
width,
height,
aspectRatio,
image: img,});};
img.onerror=()=>{reject(newError('图片加载失败'));};
reader.onerror=()=>{reject(newError('文件读取错误'));};// 读取文件内容
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'];interfacePictureUploadProps{// 上传最大数量
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);};consthandleCancel=()=>setPreviewOpen(false);consthandlePreview=async(file: UploadFile)=>{if(!file.url &&!file.preview){
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
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?'':','),'',),);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));}
isCropRef.current =true;returntrue;}}{...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></>);};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。
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){case0:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
下一步
</Button>);}case1:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
下一步
</Button>);}case2:{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';exportdefaultdefineConfig({
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,});constREG=/\.wasm$/;
config.module.rule('asset').exclude.add(REG).end();
config.module
.rule('wasm').test(REG).exclude.add(/node_modules/).end().type('webassembly/async').end();},});
三、跨域配置
importtype{ IApi }from'umi';exportdefault(api: IApi)=>{// 中间件支持 cors
api.addMiddlewares(()=>{returnfunctioncors(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,子应用配置生命周期钩子.
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
exportdefaultdefineConfig({
qiankun:{
master:{
apps:[{
name:'r3f-viewer',// 子应用的名称
entry:'http://localhost:5174',// your microApp address},],},},})
使用路由的方式引入子应用
exportdefault[{
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';constRole=()=>{return(<PageContainer><MicroAppWithMemoHistory
name="r3f-viewer"
url="/umi-r3f-view"
autoSetLoading={true}
className={'microApp'}/></PageContainer>);};exportdefaultmemo(Role);
index.less
.microApp,
#root{min-height: 800px !important;height: 800px !important;max-height: 800px !important;width: 100% !important;}
版权归原作者 流水吾情 所有, 如有侵权,请联系我们删除。