目前我们的功能只是实现了调用登录接口,并返回数据. 针对接下来的逻辑需要继续完善.
暂存用户信息和token
思路和 vue-element-admin 基本类似: 登录返回token,存储token, 根据是否有token判断是否允许访问后台页面.
和之前的项目一样, 创建工具类 utils/myAuth.ts
:
import type {UserInfoType} from '@/pages/Login/model';
// 定义key
const USER_INFO_KEY = 'REACT_DEMO_USER';
const USER_TOKEN_KEY = 'REACT_DEMO_TOKEN';
/**
* 把用户信息存入localstorage
* @param userInfo
*/
export function saveUserInfo(userInfo: UserInfoType){
const userInfoStr: string = JSON.stringify(userInfo);
window.localStorage.setItem(USER_INFO_KEY,userInfoStr);
}
/**
* 删除用户信息
*/
export function removeUserInfo(){
window.localStorage.removeItem(USER_INFO_KEY);
}
/**
* 获取用户信息
*/
export function getUserInfo(){
const userInfoStr = window.localStorage.getItem(USER_INFO_KEY);
if(userInfoStr == null){
return null;
}
const userInfo: UserInfoType = JSON.parse(userInfoStr);
return userInfo;
}
/**
* 保存token
* @param token
*/
export function saveToken(token: string){
window.localStorage.setItem(USER_TOKEN_KEY,token);
}
/**
* 删除token
*/
export function removeToken(){
window.localStorage.removeItem(USER_TOKEN_KEY);
}
/**
* 获取token
*/
export function getToken(){
const tokenStr = window.localStorage.getItem(USER_TOKEN_KEY);
return tokenStr;
}
/**
* 清楚用户登录信息
*/
export function clearUserLoginInfo(){
removeUserInfo();
removeToken();
}
因为具体的登录调用和结果处理是在 pages/Login/model.ts
中的,所以在这里把用户信息和token存入localstorage:
import {saveUserInfo,saveToken } from '@/utils/myAuth';
...
*doLogin(action, {call,put}){
// 打印action,查看对象结构
// console.log(action);
const {payload} = action;
// message与引入模块冲突 重命名为: errMsg
const {data,success,message:errMsg} = yield call(loginApi,payload);
if(success){
// 存入state
// put专门用于调用 reducer, 而且需要添加 迭代器 yield修饰符
yield put({
type: 'initUserInfo',
payload: data
})
// 存入localstorage
// 思考: 为什么不用state?
const {token,userInfo} = data;
saveUserInfo(userInfo);
saveToken(token);
message.success('登录成功!');
// 跳转后台页面
window.location.href = '/';
}else{
// 返回错误 提示错误信息 不跳转
message.error(errMsg);
}
导航守卫?
但是在antd中没有vue中的导航守卫, 我们需要在 src\layouts\SecurityLayout.tsx
中对是否登录做权限控制.
通过自定义 isLogin
的逻辑, 我们就可以判断是否允许访问位于 SecurityLayout
内部的组件. 实际开发中,后台除了登录页(排除个别声明页)其他页面都应该在登录权限的保护下访问. 所以 SecurityLayout
组件可以理解为antd项目的 导航守卫 .
注意: 至于页面权限,比如用户角色能否访问某个页面的需求,暂且不讨论,我们会后期在权限篇给大家说明.
import React from 'react';
import { PageLoading } from '@ant-design/pro-layout';
import type { ConnectProps } from 'umi';
import { Redirect, connect } from 'umi';
import { stringify } from 'querystring';
// import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
import {getToken} from '@/utils/myAuth';
type SecurityLayoutProps = {
loading?: boolean;
currentUser?: CurrentUser;
} & ConnectProps;
type SecurityLayoutState = {
isReady: boolean;
};
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
state: SecurityLayoutState = {
isReady: false,
};
// react的声明周期函数 组件渲染完毕后触发,类似于vue的mounted
componentDidMount() {
this.setState({
isReady: true,
});
// 这里注销掉 已经不需要框架本身的 登录 方法
// const { dispatch } = this.props;
// if (dispatch) {
// dispatch({
// type: 'user/fetchCurrent',
// });
// }
}
render() {
const { isReady } = this.state;
const { children, loading } = this.props;
// You can replace it to your authentication rule (such as check token exists)
// 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
// const isLogin = currentUser && currentUser.userid;
// 这里换成判断token是否存在
const isLogin = getToken();
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading/>;
}
if (!isLogin && window.location.pathname !== '/user/login') {
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
// #https://github.com/dvajs/dva/tree/master/packages/dva-loading
// # https://umijs.org/zh-CN/plugins/plugin-dva
// 这里 loading是dva-loading 的插件,可以监听某个model的effects是否出于异步进行中
const mapStateToProps = (state: any) =>{
return {
loading: state.loading.models.myLogin as boolean
}
}
export default connect(mapStateToProps)(SecurityLayout);
登录成功!
我们成功的登录到了后台,进入默认欢迎页,但页发现了几个问题:
- header的用户中心为什么是loading状态?
- 左边导航应该有的admin(权限)页面跑哪了?
- 因为我们使用了自己的登录逻辑,并没有标识当前用户的角色,导航菜单是根据角色判断的
- 如何退出?
退出
剩下两个问题很容易通过阅读 src\components\GlobalHeader\AvatarDropdown.tsx
代码来理解.
退出功能就比较简单了,我们需要清空用户登录信息和token,然后跳转首页:
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin } from 'antd';
import type { Dispatch } from 'react';
import React from 'react';
import type { ConnectProps } from 'umi';
import { history ,connect} from 'umi';
// import type { ConnectState } from '@/models/connect';
import type { CurrentUser } from '@/models/user';
// 引入封装的获取用户信息的方法
import {getUserInfo} from '@/utils/myAuth';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export type GlobalHeaderRightProps = {
currentUser?: CurrentUser;
menu?: boolean;
} & Partial<ConnectProps>;
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
onMenuClick = (event: {
key: React.Key;
keyPath: React.Key[];
item: React.ReactInstance;
domEvent: React.MouseEvent<HTMLElement>;
}) => {
const { key } = event;
// 判断如果是退出按钮 则执行退出逻辑
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'myLogin/doLogout',
});
}
return;
}
history.push(`/account/${key}`);
};
render(): React.ReactNode {
const { menu} = this.props;
// 根据自己的业务逻辑 从localStorage中获取用户信息
const currentUser = getUserInfo();
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{menu && (
<Menu.Item key="center">
<UserOutlined />
个人中心
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<SettingOutlined />
个人设置
</Menu.Item>
)}
{menu && <Menu.Divider />}
<Menu.Item key="logout">
<LogoutOutlined />
退出登录
</Menu.Item>
</Menu>
);
return currentUser && currentUser.username ? (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.icon} alt="avatar" />
<span className={`${styles.name} anticon`}>{currentUser.nickname}</span>
</span>
</HeaderDropdown>
) : (
<span className={`${styles.action} ${styles.account}`}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
}
}
// 没有用到mode 所以这里暂时注销
// export default connect(({ user }: ConnectState) => ({
// currentUser: user.currentUser,
// }))(AvatarDropdown);
const mapDispatchToProps = (dispatch)=>({
dispatch
})
export default connect(mapDispatchToProps)(AvatarDropdown);
修改 pages/Login/model.ts
:
import { history } from 'umi';
import {clearUserLoginInfo} from '@/utils/myAuth';
export type MType = {
...
effects: {
doLogin: Effect;
doLogout: Effect;
};
...
}
effects: {
...
// 添加doLogout方法
doLogout(){
// 清空登录用户信息和token
clearUserLoginInfo();
history.replace({
pathname: '/user/login'
});
}
}
总结
在不考虑菜单访问权限的前提下,我们已经实现了基本的登录与退出逻辑,而且获取到了用户的基本信息和之后访问api所需要的token . 我们可以快乐的开始接下来的开发了.