今天准备使用一下Ant Design Pro,全程按照使用文档进行项目的构建和启动,结果启动时报如下错误:
ERROR in ./src/components/index.md
Module build failed (from ./node_modules/@umijs/preset-dumi/lib/loader/index.js): Error: [BABEL] @babel/helper-compilation-targets: 'opera_mobile' is not a valid target
### 问题出现的环境背景及自己尝试过哪些方法
Ant Design Pro版本:5.2.0
node.js:v18.12.1
npm:8.19.2
目前已经试过删除node_modules文件夹,并重新安装依赖包,依旧报错。
解决办法参考:https://github.com/ant-design/ant-design-pro/issues/10829 ,就是删除这个文件 ./src/components/index.md,我建的项目是 simple,删了之后直接可以运行。
配置指南
React入门第一章:了解Ant Design Pro基础框架 - 掘金
打造属于你的Ant Design Pro V5(一) - 掘金(提取了常用的几个方法)
在开发之前我们首先要把名称、logo、主题颜色进行修改,那么该如何修改呢
位置 \config\defaultSetting.js
import { Settings as LayoutSettings } from '@ant-design/pro-layout';
const Settings: LayoutSettings & {
pwa?: boolean;
} = {
navTheme: 'dark', // 主题颜色
primaryColor: '#1890ff', //颜色
layout: 'side', // 菜单模式 side:右侧导航,top:顶部导航
contentWidth: 'Fluid', // 内容模式 Fluid:自适应,Fixed:定宽 1200px
fixedHeader: false, // 是否固定头部
fixSiderbar: true, // 是否固定导航
colorWeak: false,
// headerRender: false, // 是否拥有头部
// menuRender: false, // 是否拥有菜单
title: 'Domesy',
pwa: false,
iconfontUrl: '', //icon
};
export default Settings;
这是一个小细节,需要去稍微改动
我们需要将项目的小图标做替换即可, 有的时候替换后没有效果,是因为缓存的问题,我们只需要重新启动下浏览器,或是本地服务即可
位置 /public/favicon.ico
3、清除头部小组件或是修改
我们看到在原本的上面有 全局搜索、使用文档、消息提示、国际化,这时我们可以根据自己的需求去做清楚或是修改
位置: src/components/RightContent
搜索框位置:src/components/HeaderSearch
消息通知:src/components/NoticeIcon
头像: src/components/RightContent/AvatarDropdown
4、加载页面
我们在启动整个项目的时候、或者在刷新的时候会出现一个加载页面,这个页面的位置在 src\pages\document.ejs
中。他在基础上使用了图片,可以进行修改,换成项目的名称和加载图片
5、去除水印
我们可以全局设置自己的水印,或者取消,那么这些在哪里配置呢
位置 /src/app.tsx
为了简便期间我在这个做了些全局配置,可直接更改 不用去懂 app.tsx 的代码
要想每个页面都有页脚,我们只需要更改 /src/component/Footer
即可
在我们的日常开发中,往往会有多个环境,一般的都是测试环境和线上环境,当然环境对于前端而言是接口的域名地址等,如果每次要发环境控制一个变量去改变地址,或者监听地址等操作太过麻烦,那么我们可以通过命令来改变打包的环境
那么改在哪里配置呢?
位置: /package.json
{
...
"scripts": {
...
"build:pre": "cross-env REACT_APP_ENV=pre umi build",
...
}
}
加上这段话即可,简单的介绍一下
build:pre
它就打包命令,使用跟普通的打包命令一样 yarn run build:pre
REACT_APP_ENV
就是我们区别到底是哪个环境的
cross-env
这个主要是处理 windows 的,如果不加,windows 是无法正常打包的
那么有小伙伴问了,我在这里改完如何更换地址呢
我在 src/utils/config.ts
下做了个简单配置就行了
let host = 'http://www.domesy.cn:8081';
if (REACT_APP_ENV === 'pre') {
host = 'http://www.domesy.cn:8083';
}
export { host };
除此之外,模板里也提供了直接修改的地方
文件位置:config/defaultSettings.ts
import { Settings as LayoutSettings } from '@ant-design/pro-layout';
const Settings: LayoutSettings & {
pwa?: boolean;
} = {
navTheme: 'dark', // 主题颜色
primaryColor: '#1890ff', //颜色
layout: 'side', // 菜单模式 side:右侧导航,top:顶部导航
contentWidth: 'Fluid', // 内容模式 Fluid:自适应,Fixed:定宽 1200px
fixedHeader: false, // 是否固定头部
fixSiderbar: true, // 是否固定导航
colorWeak: false,
// headerRender: false, // 是否拥有头部
// menuRender: false, // 是否拥有菜单
title: 'Domesy',
pwa: false,
iconfontUrl: '', //icon
};
export default Settings;
国际化是Ant Design Pro 一个非常强大的功能,但对国内的项目并不需要要国际化,所以当自己的项目不需要这个功能时,我们可以考虑去除这个功能
我们只需要执行 npm run i18n-remove
这个命令即可,但此时我们再将 local 删掉,还是会发现有大量的报错原因是代码中只用了 umi的 useIntl 这个方法, 那么现在只需要把文件的所有代码删除就可以了
当我们去除国际化后我们还是会遇见一些小问题
1.浏览器自带的翻译功能
这是因为在 src/page/document.ejs
文件中的 lang 是 en 的原因
我们需要将它改为zh-CN
就行了
2.Ant Design 的部分组件会显示英文(如日期组件)
这时我们还需要在 config/config.js
中的 locale
配置 default: 'zh-CN'
即可
首先 V5 自带一个全局状态 (initialState),用官方的话说是:
initialState
在 v5 中替代了原来的自带 model,global,login,setting
都并入了 initialState
中。我们需要删除 src/models/global.ts,src/models/login.ts,src/models/setting.ts ,
并且将请求用户信息和登陆拦截放到 src/app.tsx
中
理解下官方的话:
看看如何使用吧
import { useModel } from 'umi';
export default () => {
const { initialState, loading, error, refresh, setInitialState } = useModel('@@initialState');
return <>{initialState}>
};
使用起来非常的方便,先介绍下所有参数的用途
initialState
: 返回全局状态,也就是 getInitialState
的返回值
setInitialState
: (state:any) => 手动设置 initialState 的值,手动设置完毕会将 loading 置为 false.
loading
: getInitialState 是否处于 loading 状态,在首次获取到初始状态前,页面其他部分的渲染都会被阻止。loading 可用于判断 refresh 是否在进行中。
error
: 当运行时配置中,getInitialState throw Error 时,会将错误储存在 error 中。
refresh
: () => void 重新执行 getInitialState 方法,并获取新数据。
讲完了官方自带的 initialState
,接下来我们单独讲讲如何创建属于自己的 model
这块没有太多要讲解的地方,我们直接上代码吧~,以最简单的计时器即可
首先我们需要在 src/model
创建自己模块
文件位置 src/models/test/modelTest.ts
import { useState, useCallback } from 'react';
interface Props {
count?: number
}
const initInfoValue: Props = {
count: 1,
}
export default function modelTest() {
const [init, setInitValue] = useState(initInfoValue);
const [loading, setLoading] = useState(false);
const waitTime = (time: number = 2000) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
const setInit = useCallback(async(res:any) => {
setLoading(true)
await waitTime()
setLoading(false)
setInitValue({count: res})
}, [init])
const setAdd= useCallback((res:any) => {
setInitValue({ count: res +1})
}, [init])
return {
loading,
init,
setAdd,
setInit
};
}
然后在所需的页面直接通过useModel,获取就 OK 了
import React from 'react';
import { useModel } from 'umi';
import { Button } from 'antd';
export const MockModel: React.FC = () => {
const { init, setInit, setAdd, loading } = useModel('test.modelTest');
return
count 对应的值{init.count}
}
总的来说,只要把对应的方法,值全部返回,然后在调用就OK了,是不是很简单~
性能优化
当我们存在 model
的数据越来越多,就需要使用 useModel
的第二个可选参数来进行性能优化,只消费model
中的部分参数,而不使用其他参数,并返回的值则是 useModel
最终的返回值
我们以上述为例
首先,所有的路由都在config/routes
下,我们配置路由都在此文件下进行
我们先建一个一级目录
export default [
{
path: '/test',
name: '一级目录',
icon: 'smile',
component: './Welcome'
}
]
看看此时实现的效果
而多级目录只需要使用 routes 这个参数即可,其他配置一样
我们在这里在创建一个二级目录,在创建一个二级目录的子目录,我们希望由二级目录跳入这个子目录,但在菜单上不显示,这时我们只需要使用hideInMenu
即可在菜单上隐藏,但我们会发现变成了这样
这时我们发现了两个问题:一个是左边的菜单栏并没有高亮,二是面包屑展示的不对,这是因为我们写的路劲不对,我们需要把这个子页面挂载二级目录的下面就能完美解决了~
export default [
{
path: '/test',
name: '一级目录',
icon: 'smile',
routes: [
{
path: '/test',
redirect: '/test/twotest',
},
{
path: '/test/twotest',
name: '二级目录',
component: './Welcome',
},
{
path: '/test/twotest/threetest',
name: '二级目录的子页面',
component: './Welcome',
hideInMenu: true
}
]
}
]
效果:
我们再来总结下常用路由的参数(其余的参数可看官网):
path
: 地址栏的访问路径
name
: 名称
icon
:前面的小图标
component
:对应的文件夹目录
redirect
:重定向后的地址
authority
:权限,大型项目不建议使用,直接用动态菜单即可
hideInMenu
: 是否影藏菜单栏
routes
:对应的子路由
在 V5 中,提供两种,一种是 权限, 一种是动态菜单,在这里建议使用动态菜单,所以只介绍下动态菜单的用法。
所谓动态菜单,需要接口的配合,返回什么菜单就展示什么,在本人的案例中,我通过 mock 模拟出数据,并将它放入 utils/initData
中
使用中发现的问题:
如有写的不对请留言指出~
解决方法:
针对问题1和问题2,我们必须跟后端协商好,这个路由必须与接口返回的字段对应,并且只需要返回对应的名称、icon、路径即可
问题3,我们需要单独写个方法来适配就行了
使用: menuData: formatter(menuData.data)
const formatter = (data: any[]) => {
data.forEach((item) => {
if (item.icon) {
const { icon } = item;
const v4IconName = toHump(icon.replace(icon[0], icon[0].toUpperCase()));
const NewIcon = allIcons[icon] || allIcons[''.concat(v4IconName, 'Outlined')];
if (NewIcon) {
try {
// eslint-disable-next-line no-param-reassign
item.icon = React.createElement(NewIcon);
} catch (error) {
console.log(error);
}
}
}
if (item.routes || item.children) {
const children = formatter(item.routes || item.children); // Reduce memory usage
item.children = children;
}
});
return data;
};
const toHump = (name: string) => name.replace(/-(\w)/g, (all: string, letter: any) => letter.toUpperCase());
针对问题5和问题6,我们进行详细的描述下:
比如说我现在原有的页面配置上A页面(第一个页面),但我在其权限下不想展示A页面,不显示的时候,就会出现这个问题
解决方法
在点击头部的方法和登录的方法(不包括重定向)跳转到获取路由的第一个上,并且,将取消原有路由的重定向。在getInitialState
上统一设置,如果路径是/则自动获取第一个参数的路径,就能解决了~
网络请求可以说是前端与后端的桥,我们需要这个桥将后端绑定起来
对于一个系统来说,请求的方法与接受的参数都是统一的,所以我们需要集中配置我们的请求模块,来适配自己的系统。
在 V5 中设置请求的模块在src/app.tsx中
在V5中我们需要在 umi 中引入,并且相对于 V4 ,V5扩张了一个配置 skipErrorHandler, 这个配置的作用是:跳过默认的错误处理,用于处理特殊的接口
import { request } from 'umi';
request('/api/user', {
params: {
name: 1,
},
skipErrorHandler: true,
});
,并提供一些公共的配置和方法。以下是一个简单的封装示例:
// request.js
import { extend } from 'umi-request';
const request = extend({
// 全局配置
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json', // 默认请求头
},
});
// 请求拦截器
request.interceptors.request.use((url, options) => {
// 添加请求头、请求体等处理逻辑
return {
url,
options,
};
});
// 响应拦截器
request.interceptors.response.use(async (response) => {
// 处理响应数据
const data = await response.clone().json();
// 如果响应状态码不为 200,抛出异常
if (response.status !== 200) {
throw new Error(data.message || '请求失败');
}
// 如果响应状态码为 200,返回响应数据
return data;
});
export default request;
在上述代码中,我们使用 extend
方法创建了一个全局的 request
实例,并进行了一些全局配置,如超时时间和默认请求头。然后,通过 interceptors
方法设置了请求拦截器和响应拦截器,用于在请求和响应过程中进行一些公共处理逻辑。
接下来,在需要进行网络请求的文件中,可以直接引入封装好的 request
对象,并使用它来发送请求
import request from '@/utils/request';
request('/api/user').then(response => {
console.log(response);
}).catch(error => {
console.error(error);
});
这样,我们就可以通过封装的 request
对象来发送请求,并在请求和响应过程中进行一些公共处理。同时,也可以根据实际需求,进一步封装一些常用的请求方法,如 GET、POST 等,以便更方便地使用
如果需要在请求中传递参数,可以在调用 request
方法时,传递一个 params
参数。params
参数是一个对象,其中的键值对表示请求的参数名和参数值。
以下是一个示例:
import request from '@/utils/request';
request('/api/user', {
params: {
id: 1,
name: 'John',
},
}).then(response => {
console.log(response);
}).catch(error => {
console.error(error);
});
在上述代码中,我们通过传递 params
参数来设置请求的参数。在请求的 URL 中,会自动将参数拼接在 URL 后面,形成类似 /api/user?id=1&name=John
的形式。
如果需要发送 POST 请求,可以将请求的方法设置为 post
,并将参数放在 data
参数中。例如:
import request from '@/utils/request';
request('/api/user', {
method: 'post',
data: {
id: 1,
name: 'John',
},
}).then(response => {
console.log(response);
}).catch(error => {
console.error(error);
});
在上述代码中,我们将请求的方法设置为 post
,并将参数放在 data
参数中。这样,参数会以 JSON 格式发送到服务器。
需要注意的是,request
方法的第二个参数是一个配置对象,可以设置请求的方法、请求体、请求头等。具体的配置项可以参考 umi-request 的官方文档:https://github.com/umijs/umi-request
// request.js
import { extend } from 'umi-request';
const request = extend({
// 全局配置
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json', // 默认请求头
},
});
// 请求拦截器
request.interceptors.request.use((url, options) => {
// 添加请求头、请求体等处理逻辑
return {
url,
options,
};
});
// 响应拦截器
request.interceptors.response.use(async (response) => {
// 处理响应数据
const data = await response.clone().json();
// 如果响应状态码不为 200,抛出异常
if (response.status !== 200) {
throw new Error(data.message || '请求失败');
}
// 如果响应状态码为 200,返回响应数据
return data;
});
// 封装 GET 请求
export const get = (url, params) => {
return request(url, {
method: 'get',
params,
});
};
// 封装 POST 请求
export const post = (url, data) => {
return request(url, {
method: 'post',
data,
});
};
在上述代码中,我们通过封装 get
和 post
方法,将请求的方法和参数进行了封装。这样,在项目中使用时,只需引入相应的方法,并传递对应的 URL 和参数即可。
以下是一个使用示例:
import { get, post } from '@/utils/request';
// 发送 GET 请求
get('/api/user', { id: 1, name: 'John' })
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
// 发送 POST 请求
post('/api/user', { id: 1, name: 'John' })
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
通过封装的 get
和 post
方法,可以更方便地发送 GET 和 POST 请求,并且可以在请求和响应过程中进行一些公共处理。
在上篇文章介绍了分模块打包 其本质就是通过不同的命令,打出不同的包,请求的接口就需要在这里通过prefix
来配置
export const request: RequestConfig = {
prefix: process.env.NODE_ENV === "production" ? host : '/api/',
};
每个系统对应的后端请求都不相同,所以应该在请求前和请求后做一些特定的处理,以此来帮助我们快速开发,比如:在请求的时,请求头上加入 token
因此,V5提供了两种方式,一是中间件(middlewares),另一种则是拦截器
这两种方式都可以优雅地做网络请求前后的增强处理,但中间件使用起来比较复杂,所以这里只介绍拦截器
首先,我们来说说请求拦截需要配置什么
这里要说明一点 token 通常需要存储到本地的,原因是每次启动项目都会用到,当然缓存能少用就少用,尽量使用数据流做处理。
因此 我们需要设置一个变量来存储 token
另外我们需要注意一点,在未登录的时候并无token,并且在退出登录后,要清空缓存
废话有点多~ 直接看代码吧~
/**请求拦截 */
export const requestInterceptors: any = (url: string, options: RequestInit) => {
if (storageSy.token) {
const token = `Bearer ` + localStorage.getItem(storageSy.token);//存储的token
options.headers = {
...options.headers,
"Authorization": token,
'Content-Type': 'application/json',
}
}
return { url, options };
}
跟请求拦截一样,我们先来说说响应拦截做的的做了什么吧
catch
来进行捕获了。// 响应拦截
export const responseInterceptors:any = async (response: Response) => {
if (!response) {
notification.error({
description: '您的网络发生异常,无法连接服务器',
message: '网络异常',
});
return;
}
const data = await response.clone().json();
if ([10001,10008].includes(data.resultCode)) {
message.error(data.message);
localStorage.clear();
return false;
}
if (data.code !== 200) {
message.error(data.message);
return false;
}
return data.data;
}
mock 是什么呢? 他是模拟接口请求的数据,并且可以随机生成测试数据,当后端还未好时,我们可以与后端沟通变量的名称,之后再靠 mock 来模拟数据,实现开发,等后端好了,直接替换接口就行了~~
文档请参考: mock官网
mock 是模拟的接口,所以在正式打包后,mock 数据是无法使用,那么如果在开发时候不用mock数据,该怎么处理呢?
命令 npm run start:no-mock
就行
我们知道,有的时候 mock 数据 是可以和真实的 Api 请求并存的
但在打包后 mock 是无法使用的,那么能否启动一个 mock 服务呢?然后通过 nginx 代理到这个mock服务呢?
官方给出了一种方法: umi-serve
安装命令 yarn add global umi-serve
为了方便起见
我们可以再 package.json
中的 script 中加入 "serve":"umi-serve"
即可
下次启动 umi-serve 服务,只需在控制台中输入:npm run serve ,即可。
这块内容比较简单,就没必要说了,直接提供两种请求方式就ok了
'GET /api/form/queryDetail': async (req: Request, res: Response) => {
const { detail } = req.query;
if (detail === 'introduce') {
res.send(
resData({
list: introduce,
anchorList: introduceAnchorList
}
))
}
res.send({
code: 400,
detail,
message: '请输入参数'
})
}
'POST /api/domesy/queryDetail': async (req: Request, res: Response) => {
const { detail } = req.query
if(detail === 'welcome') {
res.send(
resData({
list: welcome,
anchorList: welcomeAnchorList
}
))
return
}
res.send({
code: 400,
detail,
message: '请输入参数'
})
},
├── config # umi 配置,包含路由,构建等配置
├── mock # 本地模拟数据
├── public
│ └── favicon.png # Favicon
├── src
│ ├── assets # 本地静态资源
│ ├── commonPages # 公共页面
│ ├── components # 业务通用组件
│ ├── e2e # 集成测试用例
│ ├── layouts # 通用布局
│ ├── models # 全局 dva model
│ ├── pages # 业务页面入口和常用模板
│ ├── services # 后台接口服务
│ ├── utils # 工具库
│ ├── locales # 国际化资源
│ ├── global.less # 全局样式
│ └── global.ts # 全局 JS
├── tests # 测试工具
├── README.md
└── package.json
我们先看看 /scr/app.tsx
这段代码
首先页面打开(无论哪个页面都会执行),会执行 app.tsx
里的 getInitialState
,然后走向queryCurrentUser
这个函数,在这个函数上他会判断 access
是否存在,如果不存在则会报错,发送状态码为401, 然后就会走向登录页面,反之则会停留在当前页面
到达登录页面后。输入账号密码时走向 fetchUserInfo
这个方法,这个方法主要的作用是存储了一开始登录接口所需的函数
注意,这里进去的页面不会执行 getInitialState
这个函数,所以再次要执行获取用户信息的方法
我们做的页面会放在浏览器上,我们需要登录信息才能够打开,但由于许多外部原因会导致存储的信息发生变化,如清储缓存,时间过长导致登录信息失效等,那么在 V5 中如何判断的呢?
会将登录信息放在 initialState 中,我们需要在 app,tsx
中的 onPageChange
这个方法里
这个方法是通过页面转换而触发的,在这里也会判断用户信息是否存在,如果不存在,则会重新跳转登录界面
@ 做了什么
我们随意的可以看见类似这样的引入
import Footer from '@/components/Footer';
那么 @是干嘛的呢?
其实在项目中引入分为绝对路径和相对路径,我们通常将组件放置在 component
模块下,配置放置在 utils
模块下,那么 @ 实际上就是 相当于绝对路径 也就是 /src 的作用,他就是别名
帮助我更快速的引入,详细的可参考 webpack别名设置
@@initialState是一个特殊的关键字,用于在UmiJS中获取应用程序的初始状态。它是通过使用useModel钩子来获取的。
在UmiJS中,可以通过在src/app.tsx文件中的getInitialState函数中设置初始状态。这个函数会在应用程序初始化时被调用,并返回一个包含特定结构的对象,表示应用程序的初始状态。
使用@@initialState关键字和useModel钩子,可以在任何组件中获取和使用应用程序的初始状态。例如,在上面提到的代码中,通过const { initialState, setInitialState } = useModel('@@initialState'),我们可以获取到应用程序的初始状态,并在组件中使用它。
这样做的好处是,我们可以在应用程序的任何地方获取和使用初始状态,而不需要显式地传递它。这使得状态管理更加方便和灵活。
希望这样解释清楚了@@initialState的作用。如果还有其他问题,请随时提问!