目前我们的功能只是实现了调用登录接口,并返回数据. 针对接下来的逻辑需要继续完善.

暂存用户信息和token

思路和 vue-element-admin 基本类似: 登录返回token,存储token, 根据是否有token判断是否允许访问后台页面.
和之前的项目一样, 创建工具类 utils/myAuth.ts :

  1. import type {UserInfoType} from '@/pages/Login/model';
  2. // 定义key
  3. const USER_INFO_KEY = 'REACT_DEMO_USER';
  4. const USER_TOKEN_KEY = 'REACT_DEMO_TOKEN';
  5. /**
  6. * 把用户信息存入localstorage
  7. * @param userInfo
  8. */
  9. export function saveUserInfo(userInfo: UserInfoType){
  10. const userInfoStr: string = JSON.stringify(userInfo);
  11. window.localStorage.setItem(USER_INFO_KEY,userInfoStr);
  12. }
  13. /**
  14. * 删除用户信息
  15. */
  16. export function removeUserInfo(){
  17. window.localStorage.removeItem(USER_INFO_KEY);
  18. }
  19. /**
  20. * 获取用户信息
  21. */
  22. export function getUserInfo(){
  23. const userInfoStr = window.localStorage.getItem(USER_INFO_KEY);
  24. if(userInfoStr == null){
  25. return null;
  26. }
  27. const userInfo: UserInfoType = JSON.parse(userInfoStr);
  28. return userInfo;
  29. }
  30. /**
  31. * 保存token
  32. * @param token
  33. */
  34. export function saveToken(token: string){
  35. window.localStorage.setItem(USER_TOKEN_KEY,token);
  36. }
  37. /**
  38. * 删除token
  39. */
  40. export function removeToken(){
  41. window.localStorage.removeItem(USER_TOKEN_KEY);
  42. }
  43. /**
  44. * 获取token
  45. */
  46. export function getToken(){
  47. const tokenStr = window.localStorage.getItem(USER_TOKEN_KEY);
  48. return tokenStr;
  49. }
  50. /**
  51. * 清楚用户登录信息
  52. */
  53. export function clearUserLoginInfo(){
  54. removeUserInfo();
  55. removeToken();
  56. }

因为具体的登录调用和结果处理是在 pages/Login/model.ts 中的,所以在这里把用户信息和token存入localstorage:

  1. import {saveUserInfo,saveToken } from '@/utils/myAuth';
  2. ...
  3. *doLogin(action, {call,put}){
  4. // 打印action,查看对象结构
  5. // console.log(action);
  6. const {payload} = action;
  7. // message与引入模块冲突 重命名为: errMsg
  8. const {data,success,message:errMsg} = yield call(loginApi,payload);
  9. if(success){
  10. // 存入state
  11. // put专门用于调用 reducer, 而且需要添加 迭代器 yield修饰符
  12. yield put({
  13. type: 'initUserInfo',
  14. payload: data
  15. })
  16. // 存入localstorage
  17. // 思考: 为什么不用state?
  18. const {token,userInfo} = data;
  19. saveUserInfo(userInfo);
  20. saveToken(token);
  21. message.success('登录成功!');
  22. // 跳转后台页面
  23. window.location.href = '/';
  24. }else{
  25. // 返回错误 提示错误信息 不跳转
  26. message.error(errMsg);
  27. }

image.png

导航守卫?

但是在antd中没有vue中的导航守卫, 我们需要在 src\layouts\SecurityLayout.tsx 中对是否登录做权限控制.
通过自定义 isLogin 的逻辑, 我们就可以判断是否允许访问位于 SecurityLayout 内部的组件. 实际开发中,后台除了登录页(排除个别声明页)其他页面都应该在登录权限的保护下访问. 所以 SecurityLayout 组件可以理解为antd项目的 导航守卫 .
注意: 至于页面权限,比如用户角色能否访问某个页面的需求,暂且不讨论,我们会后期在权限篇给大家说明.
image.png

  1. import React from 'react';
  2. import { PageLoading } from '@ant-design/pro-layout';
  3. import type { ConnectProps } from 'umi';
  4. import { Redirect, connect } from 'umi';
  5. import { stringify } from 'querystring';
  6. // import type { ConnectState } from '@/models/connect';
  7. import type { CurrentUser } from '@/models/user';
  8. import {getToken} from '@/utils/myAuth';
  9. type SecurityLayoutProps = {
  10. loading?: boolean;
  11. currentUser?: CurrentUser;
  12. } & ConnectProps;
  13. type SecurityLayoutState = {
  14. isReady: boolean;
  15. };
  16. class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
  17. state: SecurityLayoutState = {
  18. isReady: false,
  19. };
  20. // react的声明周期函数 组件渲染完毕后触发,类似于vue的mounted
  21. componentDidMount() {
  22. this.setState({
  23. isReady: true,
  24. });
  25. // 这里注销掉 已经不需要框架本身的 登录 方法
  26. // const { dispatch } = this.props;
  27. // if (dispatch) {
  28. // dispatch({
  29. // type: 'user/fetchCurrent',
  30. // });
  31. // }
  32. }
  33. render() {
  34. const { isReady } = this.state;
  35. const { children, loading } = this.props;
  36. // You can replace it to your authentication rule (such as check token exists)
  37. // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
  38. // const isLogin = currentUser && currentUser.userid;
  39. // 这里换成判断token是否存在
  40. const isLogin = getToken();
  41. const queryString = stringify({
  42. redirect: window.location.href,
  43. });
  44. if ((!isLogin && loading) || !isReady) {
  45. return <PageLoading/>;
  46. }
  47. if (!isLogin && window.location.pathname !== '/user/login') {
  48. return <Redirect to={`/user/login?${queryString}`} />;
  49. }
  50. return children;
  51. }
  52. }
  53. // #https://github.com/dvajs/dva/tree/master/packages/dva-loading
  54. // # https://umijs.org/zh-CN/plugins/plugin-dva
  55. // 这里 loading是dva-loading 的插件,可以监听某个model的effects是否出于异步进行中
  56. const mapStateToProps = (state: any) =>{
  57. return {
  58. loading: state.loading.models.myLogin as boolean
  59. }
  60. }
  61. export default connect(mapStateToProps)(SecurityLayout);

登录成功!

我们成功的登录到了后台,进入默认欢迎页,但页发现了几个问题:

  • header的用户中心为什么是loading状态?
  • 左边导航应该有的admin(权限)页面跑哪了?
    • 因为我们使用了自己的登录逻辑,并没有标识当前用户的角色,导航菜单是根据角色判断的
  • 如何退出?

image.png

退出

剩下两个问题很容易通过阅读 src\components\GlobalHeader\AvatarDropdown.tsx 代码来理解.
退出功能就比较简单了,我们需要清空用户登录信息和token,然后跳转首页:

  1. import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
  2. import { Avatar, Menu, Spin } from 'antd';
  3. import type { Dispatch } from 'react';
  4. import React from 'react';
  5. import type { ConnectProps } from 'umi';
  6. import { history ,connect} from 'umi';
  7. // import type { ConnectState } from '@/models/connect';
  8. import type { CurrentUser } from '@/models/user';
  9. // 引入封装的获取用户信息的方法
  10. import {getUserInfo} from '@/utils/myAuth';
  11. import HeaderDropdown from '../HeaderDropdown';
  12. import styles from './index.less';
  13. export type GlobalHeaderRightProps = {
  14. currentUser?: CurrentUser;
  15. menu?: boolean;
  16. } & Partial<ConnectProps>;
  17. class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
  18. onMenuClick = (event: {
  19. key: React.Key;
  20. keyPath: React.Key[];
  21. item: React.ReactInstance;
  22. domEvent: React.MouseEvent<HTMLElement>;
  23. }) => {
  24. const { key } = event;
  25. // 判断如果是退出按钮 则执行退出逻辑
  26. if (key === 'logout') {
  27. const { dispatch } = this.props;
  28. if (dispatch) {
  29. dispatch({
  30. type: 'myLogin/doLogout',
  31. });
  32. }
  33. return;
  34. }
  35. history.push(`/account/${key}`);
  36. };
  37. render(): React.ReactNode {
  38. const { menu} = this.props;
  39. // 根据自己的业务逻辑 从localStorage中获取用户信息
  40. const currentUser = getUserInfo();
  41. const menuHeaderDropdown = (
  42. <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
  43. {menu && (
  44. <Menu.Item key="center">
  45. <UserOutlined />
  46. 个人中心
  47. </Menu.Item>
  48. )}
  49. {menu && (
  50. <Menu.Item key="settings">
  51. <SettingOutlined />
  52. 个人设置
  53. </Menu.Item>
  54. )}
  55. {menu && <Menu.Divider />}
  56. <Menu.Item key="logout">
  57. <LogoutOutlined />
  58. 退出登录
  59. </Menu.Item>
  60. </Menu>
  61. );
  62. return currentUser && currentUser.username ? (
  63. <HeaderDropdown overlay={menuHeaderDropdown}>
  64. <span className={`${styles.action} ${styles.account}`}>
  65. <Avatar size="small" className={styles.avatar} src={currentUser.icon} alt="avatar" />
  66. <span className={`${styles.name} anticon`}>{currentUser.nickname}</span>
  67. </span>
  68. </HeaderDropdown>
  69. ) : (
  70. <span className={`${styles.action} ${styles.account}`}>
  71. <Spin
  72. size="small"
  73. style={{
  74. marginLeft: 8,
  75. marginRight: 8,
  76. }}
  77. />
  78. </span>
  79. );
  80. }
  81. }
  82. // 没有用到mode 所以这里暂时注销
  83. // export default connect(({ user }: ConnectState) => ({
  84. // currentUser: user.currentUser,
  85. // }))(AvatarDropdown);
  86. const mapDispatchToProps = (dispatch)=>({
  87. dispatch
  88. })
  89. export default connect(mapDispatchToProps)(AvatarDropdown);

修改 pages/Login/model.ts :

  1. import { history } from 'umi';
  2. import {clearUserLoginInfo} from '@/utils/myAuth';
  3. export type MType = {
  4. ...
  5. effects: {
  6. doLogin: Effect;
  7. doLogout: Effect;
  8. };
  9. ...
  10. }
  11. effects: {
  12. ...
  13. // 添加doLogout方法
  14. doLogout(){
  15. // 清空登录用户信息和token
  16. clearUserLoginInfo();
  17. history.replace({
  18. pathname: '/user/login'
  19. });
  20. }
  21. }

image.png

总结

在不考虑菜单访问权限的前提下,我们已经实现了基本的登录与退出逻辑,而且获取到了用户的基本信息和之后访问api所需要的token . 我们可以快乐的开始接下来的开发了.