『web前端开发』基于Ant Design的前端快速开发
前言
课题组前端开发项目,主要需求为开发一个Web平台:
- 1)基本的注册登录功能
- 2)三种权限,不同权限访问不同页面
- 3)用户管理界面,管理员可以访问该界面进行用户的增删改查
- 4)分页查询、下载功能
- 5)token动态验证鉴权
调研后选择基于Ant Design Pro开发,网上并没有比较全面的Ant Design Pro教程,笔者主要基于Ant Design Pro 官方文档 和 Ant Design Pro 从零到一教程 学习,结合chatgpt进行快速开发,本文主要记录学习和解决问题的过程。
代码开源在:基于Ant Design Pro + Flask 的Web系统
技术框架
如果你第一次使用前端框架,并不了解 umi ,Ant Design,webpack 等开发工具可以参考下面的文档,了解 Ant Design Pro 的技术框架
在我们第一次接触AntD的时候,会遇到两个东西,一个是Ant Design 另一个是Ant Design Pro,
- Ant Design 是一套设计语言与组件库
- Ant Design Pro是一套基于 Ant Design 和 umi 封装的解决方案
可能这样说还不够直接,说白了就是,Ant Design Pro 是Ant Design 的脚手架,当你构建项目基本框架用Pro ,然后要使用其中一些组件就去Ant Design中去查。
环境搭建
需要本地安装yarn、node和git
- yarn:facebook发布的一款取代npm的包管理工具。
- node:安装node会顺便自动安装Npm
-
首先初始化
1
2
3
4
5# 安装所需要的包
npm i @ant-design/pro-cli -g
# 创建项目
npx pro create myapp选择simple,执行完成后生成项目目录
-
使用webstorm或者vscode打开项目文件夹,使用npm或yarn安装依赖
1
2
3
4
5cd myapp
npm install
或者
yarm -
启动项目
1
npm start
访问
http://localhost:8000
,可以看到成功启动
项目结构
1 | ├── config # umi 配置,包含路由,构建等配置 |
配置文件
config\config.ts
: webpack的配置config\proxy.ts
: 代理的配置config\router.ts
: 路由表的配置src\app.ts
:运行时配置
-
config\routes.ts :路由配置文件
主要包含以下字段:
-
path :访问的路径,即web页面中输入的url
- 以
/
开头为绝对路径 - 如果不是以
/
开头会拼接父路由
- 以
-
name:该路由的名称,用于国际化(i18n)处理。它会从国际化文件
src\locales\zh-CN\menu.ts
中获取相应的值来作为路由的标题,并在导航菜单或者页面标题中展示出来。例如name为welcome的话就会去文件中找到对应的条目,可能为“欢迎”,显示在页面标题上这里我们也可以修改这个文件,让条目的名称改变。如果没有在这里找到的话就会直接显示原始的名称
-
icon:指定路由对应的图标。这些图标通常会在导航菜单或其他需要展示图标的地方显示。
-
routes:指定该路由下面的子路由
-
component:指定当路径与路由匹配时要渲染的 React 组件。这个字段可以包含绝对路径或者相对路径。相对路径通常是从
src/pages
目录开始查找。 -
redirect: 定义了一个重定向目标路径,当用户访问该路由时,会被自动重定向到指定的路径。
-
layout:用于指定是否使用全局布局组件。
-
layout: false
表示不使用全局布局组件,路由将直接渲染配置的组件,而不包含全局布局。 -
如果没有指定
layout
字段或设置为true
(默认值),则路由会使用全局布局组件,这通常包含导航栏、侧边栏、页脚等公共部分。通过配置
layout
字段,可以灵活地决定某些页面是否需要包含全局布局。例如,登录页面通常不需要包含导航栏和侧边栏,因此可以设置layout: false
,而其他页面可能需要全局布局组件,以提供一致的用户体验。
-
-
access:定义该路由的访问权限控制,后面跟的是定义的权限函数。这里是
canAdmin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
path: '/admin',
name: 'admin',
icon: 'crown',
access: 'canAdmin',
routes: [
{
path: '/admin',
redirect: '/admin/sub-page',
},
{
path: '/admin/sub-page',
name: 'sub-page',
component: './Admin',
},
],
},在项目中有一个
src/access.ts
文件,在其中定义了权限函数 canAdmin。1
2
3
4
5
6
7
8
9/**
* @see https://umijs.org/docs/max/access#access
* */
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: currentUser && currentUser.access === 'admin',
};
}后续说到权限控制的时候再详细说明
-
-
config/defaultSettings 网站主题配置文件
-
title :网站标题
-
logo:网站logo
-
iconfontUrl:网站标签页的icon
-
TypeScript 基本语法
-
组件
TypeScript 本身要求对 props 进行类型检查, JSX并没有此类规定。通过 React.FC,可以确保组件和其子组件遵循正确的类型。
- React.FC 表示 Function Component
TypeScript 定义组件的基本语法如下:
1
2
3
4
5
6
7
8
9import React from 'react';
interface Props {
message: string;
}
const MyComponent: React.FC<Props> = ({ message }) => {
return <div>{message}</div>;
};
页面开发方法
新增页面
这里的『页面』指配置了路由,能够通过链接直接访问的模块,要新建一个页面,需要:
-
绑定路由
在
config\routes.ts
文件中添加新的页面路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
path:'/demo',
name:'demo',
icon: 'star',
routes:[
{
name: 'demo1',
// path: 'demo1',
path: '/demo/demo1',
component: './demo/demo1'
},{
name: 'demo2',
path: 'demo2',
component: './demo/demo2'
}
]
},注意这里不要把path写错了,上面介绍的两种写法都是可以的,但是不要加上
.
,不然会无法访问 -
新建ts、less 文件
.tsx
:构建界面主要文件,这里采用TypeScript的形式进行创建(ts的拓展语言tsx构建的文件,相当于js的拓展语言文件.jsx
).less
:样式文件,相当于css。咋办暂时不考虑
1)在
src\page\
下新建文件夹2)写入
index.tsx
文件(如果你选择jsx进行开发的话,新建index.js
也是可以的)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// demo1:index.js
export default () => {
return <div>我的第一个AntD-demo1</div>;
};
// demo2:index.tsx
import React from "react";
class Index extends React.Component {
render() {
return <div>
我的第一个AntD-demo2
</div>
}
}
export default Index;成功创建了页面
-
使用ant design组件
创建的页面比较简单,可以使用一些 ant design 的组件来丰富我们的页面:ant design组件总览
打开页面后,我们可以找自己想要的组件。这里我们选择表格组件,找到一个比较满意的代码演示效果,展开代码后复制对应的代码
Mock模拟数据
mock文件夹
-
基本概念
Mock用来模拟数据,为什么会有它的出现呢?因为现在前后端开发基本是分离的,但是数据结构一般都会先定好,在日常开发中,为了前端的进度不受到后端的影响,常用Mock来做虚拟数据来模拟后端发来的请求。当你在开发过程中请求某个 API 时,
mock
文件夹中的模拟接口会拦截这个请求并返回模拟数据。 -
创建mock
AntdPro 中约定了mock在根目录的 mock目录 中接入,该目录下的ts文件都是用来模拟数据
例如我们可以创建一个文件
test.ts
:1
2
3
4
5
6
7
8
9
10
11export default {
'GET /api/getValue': {
data:[{
name:'zhangsang',
sex:'男'
},{
name:'李四',
sex:'男'
},]
},
};当前端代码中对
http://localhost:8000/api/getValue
发起请求后,mock拦截并发挥模拟是数据,我们可以直接在浏览器中访问这个接口。可以看到直接返回了我们定义的模拟数据
-
创建异步mock
我们一般发送请求都是异步的,
-
什么是异步:异步行为意味着当你向服务器发送请求(比如通过浏览器访问一个API接口)时,服务器会在后台处理这个请求,而不会阻塞或等待这个请求完成后再处理其他请求。这样,服务器可以同时处理多个请求,提升效率和响应速度。对前端而言,前端向API发出请求,这时候后端响应可能需要时间。而异步操作允许前端在等待服务器响应的同时继续执行其他任务。例如,用户点击一个按钮触发数据请求时,前端可以立即更新界面上的按钮状态(如显示“加载中”),而不需要等到请求完成。而如果不使用异步操作,用户界面会被阻塞,导致用户无法进行其他操作。这会严重影响用户体验。
-
如何设置mock发送异步请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import {Request,Response} from "express";
const getList = async (req:Request,res:Response)=>{
const result = {
success:true,
data:[
{
id:1,
name:'test01',
sort:13
}
]
};
return res.json(result);
}
export default {
'GET /api/testList':getList,
}从上面可以看到就是简单的加了async关键字,然后加了request和response
-
services文件夹
-
基本概念
services文件夹用于存储与后端 API 交互的服务函数。前端通过这些服务函数向后端发送请求,并处理返回的数据。定义实际的 API 调用,封装 HTTP 请求逻辑,方便在各个组件中复用和管理与后端的通信。
注意services文件夹和mock文件夹之间的区别:
- services:定义实际的 API 调用,主要在生产阶段使用。
- mock:定义了模拟的数据。如果设置了mock的话,services中定义的API调用会被mock拦截(API名称相同),返回mock模拟数据
-
基本写法
在services文件夹下创建ts文件,在其中定义API函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import {request} from "umi";
// get请求可以不写method字段
export async function rule(
params: {
// query
/** 当前的页码 */
current?: number;
/** 页面的容量 */
pageSize?: number;
},
options?: { [key: string]: any },
) {
return request<API.RuleList>('/api/rule', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
// post请求
export async function fakeAccountLogin(params: LoginParamsType) {
return request('/api/login/account', {
method: 'POST',
data: params,
});
}-
?:
表示为非必须字段 -
options?: { [key: string]: any }
:表示一个可选的参数对象,键为字符串,值为任何类型 -
API.RuleList
表示该请求的返回值被强制指定为API.RuleList
类型,对应的类型声明可以在src\services\ant-design-pro\typings.d.ts
中找到1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21type RuleListItem = {
key?: number;
disabled?: boolean;
href?: string;
avatar?: string;
name?: string;
owner?: string;
desc?: string;
callNo?: number;
status?: number;
updatedAt?: string;
createdAt?: string;
progress?: number;
};
type RuleList = {
data?: RuleListItem[];
/** 列表的内容总数 */
total?: number;
success?: boolean;
}; -
...(options || {})
的作用是展开传入的 options 参数。如果 options 没有传递或为 null 或 undefined,则使用一个空对象 {} 作为默认值。
-
例子
在 test\index.tsx
中写入以下内容
1 | import React from "react"; |
model数据流管理
这里主要参考 DvaJS入门课 | Model
-
基本概念
中后台场景下,绝大多数页面的数据流转都是在当前页完成,在页面挂载的时候请求后端接口获取并消费,这种场景下并不需要复杂的数据流方案。但是也存在需要全局共享的数据,如用户的角色权限信息或者其他一些页面间共享的数据。那么怎么才能缓存并支持在多个页面直接去共享这部分数据呢。
为了实现在多个页面中的数据共享,以及一些业务可能需要的简易的数据流管理的场景,model 是用于管理应用状态的一个重要概念。Ant Design Pro 默认使用 dva 进行状态管理,而 model 是 dva 的核心部分之一。
model其实就是对数据的处理过程又进行了一次封装,每个 model 负责管理一种特定类型的数据(按照功能业务逻辑进行花粉)。例如,你可以有一个用户数据的 model,一个商品数据的 model,一个订单数据的 model 等等。model 不仅仅是存储数据,还包含了如何处理这些数据的业务逻辑。它包括同步操作(reducers)和异步操作(effects)。
一般来说在AntD中数据请求过程是这样的:
1.UI 组件交互操作;
2.调用 model 的 effect;
3.调用统一管理的 service 请求函数;
4.使用封装的 request.ts 发送请求;
5.获取服务端返回;
6.然后调用 reducer 改变 state;
7.更新 model。
View->model->Service->后端(或Mock)
-
数据流图
核心概念:
1)state:一个对象,保存整个应用状态。是储存数据的地方,收到 Action 以后会更新数据。
2)View:React 组件构成的视图层。从 State 取数据后,渲染成 HTML 代码。只要 State 有变化,View 就会自动更新
3)Action:用来描述 UI 层事件的一个对象。
4)connect:一个函数,绑定 State 到 View。
5)dispatch:一个函数方法,用来将 Action 发送给 State。
-
使用Model
- state :保存应用状态。每个 model 都有自己独立的 state,用于存储与该 model 相关的数据。状态可以是任何类型的值,包括对象、数组等。在 Page 中连接的时候使用到。
- reducers:处理同步操作的纯函数,接收当前的 state 和 action,返回新的 state。它们描述了如何根据 action 来修改 state。
- effects:用于处理异步操作的函数,通常用于发起网络请求、执行异步逻辑等。effects 通常使用 redux-saga 来实现,effects 可以调用其他的 reducers 来更新 state。
- subscriptions :用于订阅数据源的机制,可以在应用启动时进行一些数据初始化操作或监听路由变化等。例如当访问某个页面的时候设置自动获取某些数据。
- namespace: model 的命名空间,用于区分不同 model 的作用域。在 dispatch action 时,需要加上 namespace 以便明确指向哪个 model。
-
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69import type {Effect} from "umi";
import {Reducer} from "umi";
import {getTableValue, selectTableValue} from "@/pages/table/service";
// 1. 设置类型
export type TableValue={
key?: String;
name?:String;
age?:Number;
address?: String;
time?:String;
}
export type allValue = {
tableValue?:TableValue;
}
// 2. Model类型定义
export type TableModelType = {
namespace:'tableDemo';
state:allValue;
effects:{
getValue:Effect;
selectValue:Effect;
}
reducers:{
saveValue:Reducer<allValue>;
}
}
// Model的实现
const TableModel:TableModelType={
//命名空间
namespace: "tableDemo",
//设置state
state: {
tableValue:{},
},
//提供给view调用的接口
effects: {
*getValue(_,{call,put}){
const res = yield call(getTableValue);
yield put({
type:'saveValue',
payload:res
})
},
*selectValue({payload},{call,put}){
console.log("model:"+payload)
const res = yield call(selectTableValue,payload);
console.log(res)
yield put({
type:'saveValue',
payload:res
})
}
},
//提供给effects通信put的接口
reducers: {
saveValue(date,action){
return{
...date,
tableValue: action.payload
};
}
}
}
export default TableModel;- export type:定义数据类型(
?:
表示为非必须字段) - 2.Model类型定义:通过 TypeScript 类型定义,明确描述模型的结构,包括命名空间、状态、effects 和 reducers。这样可以在编写和维护代码时提供类型检查和自动补全功能,提高代码的可靠性和可读性
- 其中effects内我们可以看到有一个带*号的方法,表示这个方法是提供给Page调用的
- put、call、select方法 是提供通信的,例如yield put()是对reducers中的方法调用。yield call是去访问service中的方法。
- export type:定义数据类型(
权限设置
-
全局初始数据
几乎大部分中台项目都有一个需求,就是在整个应用加载前请求用户信息或者一些全局依赖的基础数据。这些信息通常会用于 Layout 上的基础信息(通常是用户信息),权限初始化,以及很多页面都可能会用到的基础数据。
如何使用全局初始数据:
在运行时配置文件
src/app.ts
中添加运行时配置getInitialState
1
2
3
4
5export async function getInitialState() {
return {
userName: 'xxx',
};
}该方法返回的数据最后会被默认注入到一个 namespace 为
@@initialState
的 model 中。后续可以通过useModel
这个 hook来消费它1
2
3
4
5
6
7
8
9
10
11
12
13
14import React from 'react';
import { useModel } from 'umi';
import { Spin } from 'antd';
export default () => {
const { initialState, loading, refresh, setInitialState } = useModel('@@initialState');
if (loading) {
return <Spin />;
}
return <div>{initialState.userName}</div>;
}; -
初始化
在权限定义文件
src\access.ts
下定义用户拥有的权限,在该文件中export default
一个函数,定义用户拥有的权限,以下是示例定义:1
2
3
4
5
6
7
8// src/access.ts
export default function (initialState) {
return {
canReadFoo: true,
canUpdateFoo: () => true,
canDeleteFoo: (data) => data?.status < 1, // 按业务需求自己任意定义鉴权函数
};
}该文件需要返回一个 function,返回的 function 会在应用初始化阶段被执行,执行后返回的对象将会被作为用户所有权限的定义。对象的每个 key 对应一个 boolean 值,只有 true 和 false,代表用户是否有该权限。
-
页面内的权限控制(即设置页面中的部分内容对用户不可见)
使用
useAccess
hook 来获取权限定义也可以使用
Access
组件用于页面的元素显示和隐藏的控制。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import React from 'react';
import { useAccess, Access } from 'umi';
const PageA = (props) => {
const { foo } = props;
const access = useAccess(); // access 实例的成员: canReadFoo, canUpdateFoo, canDeleteFoo
if (access.canReadFoo) {
// 任意操作
}
return (
<div>
<Access accessible={access.canReadFoo} fallback={<div>Can not read foo content.</div>}>
Foo content.
</Access>
<Access accessible={access.canUpdateFoo()} fallback={<div>Can not update foo.</div>}>
Update foo.
</Access>
<Access accessible={access.canDeleteFoo(foo)} fallback={<div>Can not delete foo.</div>}>
Delete foo.
</Access>
</div>
);
}; -
路由和菜单的权限控制
在路由配置文件
config.ts
中想要添加访问控制的路由上加上access
字段即可1
2
3
4
5
6
7{
path: '/admin',
name: 'admin',
icon: 'crown',
// 调用 src/access.ts 中返回的 normalRouteFilter 进行鉴权
access: 'normalRouteFilter'
}对应鉴权函数(比如
adminRouteFilter
)在接收路由作为参数后返回值为false
,该条路由将会被禁用,并且从左侧 layout 菜单中移除,如果直接从 URL 访问对应路由,将看到一个 403 页面。
页面开发实例
创建mock
在 mock 文件夹下新建 mock\logindata.mock.ts
文件添加新定义的API所返回的模拟数据json形式,定义 GET /api/logindata
。实际情况下几乎所有的请求都应该设置为异步,但在某些特定情况下,例如需要确保顺序执行的任务,可以根据需求使用同步请求。然而,这种情况非常少见,大多数前端应用都应使用异步请求。
所以我们在这里创建的是异步请求,并且模拟异步请求延迟,请求API后5秒返回数据
1 | import { Request, Response } from 'express'; |
访问 http://localhost:8000/api/logindata
,加载5秒后可以查看返回的模拟数据
创建API函数
创建 src\services\ant-design-pro\logindata.ts
,
1 | import { request } from '@umijs/max'; |
这里的 API.LogindataList
是事先定义好的返回数据类型,写在 src\services\ant-design-pro\typings.d.ts
中
1 | type LogindataListItem = { |
编写组件函数
创建 src\pages\Search\index.tsx
,来写一个查询+展示的页面
结合按钮、输入框和表格组件,编写主组件对查询到的数据进行渲染
笔者也是初学react,所以对react的语法并不熟悉,而为了快速完成开发,我主要结合GPT来写代码。可以将 ant design组件库 中你想要使用的组件代码喂给gpt,让他为你生成主组件代码并实现你自己的一些业务需求。注意可以一步步去添加实现功能,慢慢让gpt完善,一次性提太多复杂的功能会影响代码的质量
1 | //查询页面 |
权限控制
ant design pro默认设置了user和admin两个用户类型,user可以范围跟所有的页面除了用户管理界面(仅admin可以访问)
需求:用户分为3种,包括管理员、普通会员、高级会员
-
普通用户:只拥有部分查询页面权限
-
高级会员:拥有所有查询功能
-
管理员:拥有完整后台权限包含所有查询功能,会员管理功能
这样看的话我们只需要再添加一个高级会员类型的用户
首先在 mock\user.ts
中的登录API函数 POST /api/login/account
中添加
1 | // 高级用户权限 |
在 src\access.ts
文件中,定义权限 canAdvancedUser
1 | export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) { |
在 config\routes.ts
中,设置仅advancedUser可以访问的页面
1 | // 高级会员和管理员可访问,进行需求2的操作 |
因为我们只需要设置页面权限,不需要设置页面内部分内容的权限,这里就已经完成了。
登录页面修改
默认的登录页面如下:
需求:仅需要账号密码登录,添加注册功能,删除掉其他登陆方式,再修改一下UI
这一部分主要参考 react+antdpro+ts实现企业级前端项目三:实现系统登陆 文章进行修改
删除其他登录方式
打开 src\pages\User\Login\index.tsx
文件,找到下面两块代码,注释掉:
- ActionIcons 组件,定义了登录页面底部的其他登录方式图标,使用了
AlipayCircleOutlined
、TaobaoCircleOutlined
和WeiboCircleOutlined
三个图标。
1 | // ActionIcons 组件 |
删除掉手机号注册
直接参考上面的文章进行注释
字样修改
src\components\Footer\index.tsx
中修改底边栏
找到标题和副标题,修改主标题,对副标题进行注释
1 | title="Ant Design" |
如果想要换成其他副标题的话可以直接:
1 | subTitle='基于React+ant design Pro +Ts的企业级应用' |
添加注册页面
需求:前端存在对字段的各种验证机制
最终页面显示如下:
在pages/User下创建Register文件夹并创建index.tsx文件
然后在config/routes创建register注册路由。
1 | { |
去 mock\user.ts
文件下添加处理注册请求的mock
1 | // 注册 |
去 src\services\ant-design-pro\api.ts
文件下添加对应的API接口
1 | /** 注册接口 POST /api/register */ |
在 src\services\ant-design-pro\typings.d.ts
文件下定义数据类型 API.RegisterResult
和 API.RegisterParams
1 | type RegisterParams = { |
接下来就是编写注册组件页面 src\pages\User\Register\index.tsx
,这里不贴代码了,可以直接看仓库源码
最后再把页面绑定到登录页面 src\pages\User\Login\index.tsx
上,找到下面的代码
1 | <a |
注释掉后换成
1 | <a |
完成注册界面的编写
用户管理页面
添加用户管理页面,设置了用户的增删改查功能,设置username为主键,不可修改username
弹窗修改和新增用户
这里就不贴代码了,直接去仓库看源码吧。可以仔细学习下这个页面怎么开发,大部分的管理页面都可以基于这个页面来进行修改
下载功能页面
在编写的时候,出现了一些问题:
-
单个下载、批量下载和全选下载:
-
一般情况下,设计单个下载和批量下载两个接口即可。单个下载直接将下载的文件id发送给对应的api/download,批量下载将所有需要下载的文件id列表发送给api/download/batch
-
但是因为我们的数据库数据量比较大,所以需要分页查询,即点击新的页面会发送新的请求,请求数据包中包含page、pagesize等参数。这样的话全选下载就会存在一些问题,在前端页面中,我们点击第二页的时候才会向服务器请求第二页的数据。如果我们在第一页直接选择全选下载,我们只向后端请求了第一页的数据,是获取不到第二页及后面所有的页面的id的,这样就不能用批量下载来进行全选下载了。
-
因此,在这里我们设计三个api来分别实现单个下载、批量下载和全选下载
-
-
如果我在第一页选中了一些行,点击第二页的时候,如果在第二页页选中了一些行,当我又选中一些行后,点击批量下载,第一页选中的行并没有下载
为了解决这个问题,我们需要在前端保存所有选中的行,无论它们在哪一页。我们可以使用一个状态来跟踪所有选中的行,而不仅仅是当前页的选中行。然后,我们在批量下载时使用这个全局的选中行状态。
-
使用上面的方法后,发送给API的参数列表中包含了之前选中的行。但是,在前端页面中,如果我在第一页选中了一些行,点击第二页的时候,如果在第二页页选中了一些行,再点击第一页的时候,之前选中的行前面的勾选框没有被勾选(虽然点击批量下载按钮后这些行的数据发送给了API)
这是因为在切换页面时,表格组件的
selectedRowKeys
状态没有被持久化并重新应用到表格中。当切换回第一页时,selectedRowKeys
的状态丢失了。我们需要确保在每次加载数据后,表格组件的selectedRowKeys
与allSelectedRows
同步。 -
已经勾选了的行并不能取消勾选
为了解决已勾选的行无法取消勾选的问题,我们需要确保在取消勾选时,
allSelectedRows
状态能正确更新。我们需要在handleSelectChange
函数中增加逻辑,以便在取消勾选时移除相应的行。
连接实际API
连接后端API,发现产生了报错:
这是因为默认不允许浏览器跨域,需要在服务端设置CORS。
为了方便,我们可以也先设置浏览器使其允许跨域,参考:Chrome浏览器的跨域设置
使用跨域浏览器打开,成功获取到数据
添加token机制
src\requestErrorConfig.ts
文件中有请求拦截器
1 | // 请求拦截器 |
这段代码的作用在于给每一个API请求都加上参数token=123,而在实际部署和上线的环境中,硬编码的 token=123
是不合理的,因为这样会暴露敏感信息并且无法应对动态变化的认证需求。正确的做法应该是使用动态获取和安全存储的方式来处理 token。
在这里,我们设置通过登录获取 Token 并在请求中使用
- 实现用户登录功能,从服务器获取 Token。
- 将 Token 存储在客户端(例如 localStorage 或 cookies 中)。
- 在每个请求中从存储中获取 Token 并附加到请求中。
-
首先我们修改登录的api函数
src\services\ant-design-pro\api.ts
中的登录api函数可以不修改,但是我们需要修改一下对应的数据类型,打开src\services\ant-design-pro\typings.d.ts
进行修改1
2
3
4
5
6
7
8
9
10
11
12
13type LoginParams = {
username?: string;
password?: string;
autoLogin?: boolean;
type?: string;
};
type LoginResult = {
status?: string;
type?: string;
currentAuthority?: string;
token: string;
}; -
修改请求函数文件,使得登录的时候从服务器中获取token
修改
src\pages\User\Login\index.tsx
文件,在handleSubmit
函数中,获取 Token 并将其存储在 localStorage 中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 添加获取token
const handleSubmit = async (values: API.LoginParams) => {
try {
const msg = await login({ ...values, type });
if (msg.status === 'ok' && msg.token) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
localStorage.setItem('authToken', msg.token); // 存储 token
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
setUserLoginState(msg);
} catch (error) {
const defaultLoginFailureMessage = intl.formatMessage({
id: 'pages.login.failure',
defaultMessage: '登录失败,请重试!',
});
message.error(defaultLoginFailureMessage);
}
}; -
在请求中附加 Token
在
src/requestErrorConfig.ts
中的请求拦截器中附加 Token,使得后续每一次请求的时候,都从 localStorage 中提取出来token,放置到请求头的Authorization字段中。1
2
3
4
5
6
7
8
9
10
11
12requestInterceptors: [
(config: RequestOptions) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
],antd 源代码中硬编码token且作为参数传递,正式部署上线一般都是将token放在请求头中的Authorization字段
-
修改mock模拟服务端返回token
这里对
mock\user.ts
文件进行修改。antd源代码方法是从环境变量中读取用户权限,指的是通过读取环境变量MY_CUSTOM_ENV_VARIABLE
来设置当前用户的权限1
2const { MY_CUSTOM_ENV_VARIABLE } = process.env;
let access = MY_CUSTOM_ENV_VARIABLE === 'site' ? 'admin' : '';我们将access定义为前面设置的三种权限,并新定义生成token和验证token的方法,分别在login和currentUser的api下执行。
这里写的比较粗糙,并没有应用动态的token,就不贴例子了,可以直接去代码中看
-
修改退出登录api
既然登录后获取到了api并且存储在 localStorage 中,在我们退出登录的时候就需要清除用户会话或 token
-
服务端:确保在用户退出时使其Token无效。
-
客户端:确保客户端在退出登录时删除本地存储的Token。
注意,antd源代码中硬编码token,所以退出登录中并没有使用到token,这里的逻辑其实是点击就直接返回success,这个退出登录其实完全没有退出
点击退出登录后,客户端并没有删除掉本地存储的Token,虽然重定向到了登录页面,但我们其实可以不需要再次登录,就可以访问原本登录的用户有权限访问的页面。
我们尝试一下,登录amin后退出登录,不再次登录直接访问页面
http://localhost:8000/search/logindata
,可以看到加载出了页面,随便输入一个domain进行查询可以看到可以直接访问,请求头中包含token,因此我们需要在点击退出登录后删除掉本地存储的token
修改
src\components\RightContent\AvatarDropdown.tsx
文件中的loginOut
函数,这个函数的功能是描述了点击退出登录后,调用退出登录的api并进行的操作,我们修改后加上删除本地存储的token1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const loginOut = async () => {
try {
await outLogin();
// 删除本地存储的Token
localStorage.removeItem('authToken');
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
const redirect = urlParams.get('redirect');
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: pathname + search,
}),
});
}
} catch (error) {
console.error('Logout failed:', error);
}
};现在我们再次尝试登录,退出登录后直接访问后面的页面
http://localhost:8000/search/logindata
,可以看到请求直接被拦截,因为请求头中不再包含token,验证无法通过到这里我们前端的token功能添加就完成了!
在退出登录的请求中,通常只需要在请求头中包含Token即可,参数可以省略。这样可以确保Token在服务器端被正确识别和处理以使其失效。
-