Ant Design Pro 生成的代码中包含了用户认证部分的设计,但这部分只是基本的示意,并不完整,不能拿来直接使用。并且有些代码可以比较明显的看出是不同工程师编写的,质量不理想。

16.1 公共的Token函数

虽然保存和读取Token是很简单的操作,但我们还是要把它设计成公共函数放在src/utils/utils.ts,这样做的理由是如果修改存储Token的方案,只需要修改这两个函数。下面是用sessionStorage保存Token的代码

  1. export function saveToken(token: string) {
  2. sessionStorage.setItem('token',token)
  3. }
  4. export function loadToken(): string | null {
  5. return sessionStorage.getItem("token");
  6. }

16.2 重新定义数据类型

删除src/services/ant-design-pro/typings.d.ts中的CurrentUser、LoginResult、FakeCaptcha、LoginParams。

在src/services/type.d.ts中增加定义

  1. type User = {
  2. name?: string;
  3. authorization?: any;
  4. avatar?: string;
  5. userid?: string;
  6. email?: string;
  7. signature?: string;
  8. title?: string;
  9. group?: string;
  10. tags?: { key?: string; label?: string }[];
  11. notifyCount?: number;
  12. unreadCount?: number;
  13. country?: string;
  14. geographic?: {
  15. province?: { label?: string; key?: string };
  16. city?: { label?: string; key?: string };
  17. };
  18. address?: string;
  19. phone?: string;
  20. };
  21. type LoginParams = {
  22. username?: string;
  23. password?: string;
  24. mobile?: string;
  25. captcha?: string;
  26. type?: string;
  27. };
  28. type LoginResult = {
  29. success: boolean;
  30. type?: string;
  31. data?: any;
  32. };

把app.tsx中的API.CurrentUser修改成TYPE.User。

16.3 定义新的用户信息接口

删除src/services/ant-design-pro/login.ts文件,修改src/services/ant-design-pro/index.ts

  1. import * as api from './api';
  2. -import * as login from './login';
  3. export default {
  4. api,
  5. - login,
  6. };

src/services/ant-design-pro/api.ts中删除下面的内容

  1. /** 获取当前的用户 GET /api/currentUser */
  2. export async function currentUser(options?: { [key: string]: any }) {
  3. return request<API.CurrentUser>('/api/currentUser', {
  4. method: 'GET',
  5. ...(options || {}),
  6. });
  7. }
  8. /** 退出登录接口 POST /api/login/outLogin */
  9. export async function outLogin(options?: { [key: string]: any }) {
  10. return request<Record<string, any>>('/api/login/outLogin', {
  11. method: 'POST',
  12. ...(options || {}),
  13. });
  14. }
  15. /** 登录接口 POST /api/login/account */
  16. export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
  17. return request<API.LoginResult>('/api/login/account', {
  18. method: 'POST',
  19. headers: {
  20. 'Content-Type': 'application/json',
  21. },
  22. data: body,
  23. ...(options || {}),
  24. });
  25. }/** 获取当前的用户 GET /api/currentUser */
  26. export async function currentUser(options?: { [key: string]: any }) {
  27. return request<API.CurrentUser>('/api/currentUser', {
  28. method: 'GET',
  29. ...(options || {}),
  30. });
  31. }
  32. /** 登录接口 POST /api/login/outLogin */
  33. export async function outLogin(options?: { [key: string]: any }) {
  34. return request<Record<string, any>>('/api/login/outLogin', {
  35. method: 'POST',
  36. ...(options || {}),
  37. });
  38. }
  39. /** 登录接口 POST /api/login/account */
  40. export async function login(body: TYPE.LoginParams, options?: { [key: string]: any }) {
  41. return request<TYPE.LoginResult>('/api/login/account', {
  42. method: 'POST',
  43. headers: {
  44. 'Content-Type': 'application/json',
  45. },
  46. data: body,
  47. ...(options || {}),
  48. });
  49. }

创建新文件services/api/user.ts

  1. // @ts-ignore
  2. /* eslint-disable */
  3. import { request } from 'umi';
  4. export async function login(body: TYPE.LoginParams, options?: { [key: string]: any }) {
  5. return request<TYPE.LoginResult>('/api/user/login', {
  6. method: 'POST',
  7. data: body,
  8. ...(options || {}),
  9. });
  10. }
  11. export async function logout(options?: { [key: string]: any }) {
  12. return request<Record<string, any>>('/api/user/logout', {
  13. method: 'POST',
  14. ...(options || {}),
  15. });
  16. }
  17. export async function sendCaptcha( phone: string) {
  18. return request('/api/user/sendCaptcha', {
  19. method: 'POST',
  20. params: {
  21. phone
  22. },
  23. });
  24. }
  25. export async function currentUser(options?: { [key: string]: any }) {
  26. return request<API.CurrentUser>('/api/user/getCurrentUser', {
  27. method: 'GET',
  28. ...(options || {}),
  29. });
  30. }

16.4 修改用户登出的代码

src/components/RightContent/AvatarDropdown.tsx

  1. -import { outLogin } from '@/services/ant-design-pro/api';
  2. +import { logout } from '@/services/api/user';
  1. -const loginOut = async () => {
  2. - await outLogin();
  3. +const handleLogout = async () => {
  4. + try {
  5. + await logout();
  6. + } catch(e) {
  7. + }
  1. setInitialState({ ...initialState, currentUser: undefined });
  2. - loginOut()
  3. + handleLogout();

替logout问一句:loginOut、outLogin,这都是些什么鬼?

16.5 全面修订用户登录页

src/pages/user/Login/index.tsx原有内容全部删除,置入如下代码

  1. import React, { useState } from 'react';
  2. import { Link, history, useModel } from 'umi';
  3. import { Alert, Space, message, Tabs, Form } from 'antd';
  4. import ProForm, { ProFormCaptcha, ProFormText } from '@ant-design/pro-form';
  5. import { LockOutlined, MobileOutlined, UserOutlined } from '@ant-design/icons';
  6. import Footer from '@/components/Footer';
  7. import { login, sendCaptcha } from '@/services/api/user';
  8. import { fieldRuls, fidleNormalizes } from '@/utils/form-validator'
  9. import { saveToken } from '@/utils/utils'
  10. import styles from './index.less';
  11. const LoginMessage: React.FC<{
  12. content: string;
  13. }> = ({ content }) => (
  14. <Alert
  15. message={content}
  16. type="error"
  17. showIcon
  18. style={{
  19. marginBottom: 24,
  20. }}
  21. />
  22. );
  23. /** 此方法会跳转到 redirect 参数所在的位置 */
  24. const redirect = () => {
  25. if (!history) return;
  26. setTimeout(() => {
  27. const { query } = history.location;
  28. const { redirect } = query as {
  29. redirect: string;
  30. };
  31. history.push(redirect || '/');
  32. }, 10);
  33. };
  34. const LoginPage: React.FC = () => {
  35. //指示是否正在提交登录的数据
  36. const [submitting, setSubmitting] = useState(false);
  37. //登录的方式:account 或 mobile
  38. const [loginType, setLoginType] = useState<string>('mobile');
  39. //申请登录的结果
  40. const [userLoginState, setUserLoginState] = useState<TYPE.LoginResult>();
  41. //注意这个写法可以获得app.tsx中定义的全局共享数据
  42. const { initialState, setInitialState } = useModel('@@initialState');
  43. const [form] = Form.useForm();
  44. const [captchaDisabled, setCaptchaDisabled] = useState(true)
  45. //向服务器请求用户信息,如果成功了就把它放到全局共享数据中
  46. const fetchUserInfo = async () => {
  47. const userInfo = await initialState?.fetchUserInfo?.();
  48. if (userInfo) {
  49. setInitialState({ ...initialState, currentUser: userInfo });
  50. }
  51. };
  52. //执行登录请求的函数,用户输入的信息被透传给后端,由后端进行判断
  53. const handleSubmit = async (values: TYPE.LoginParams) => {
  54. setSubmitting(true);
  55. try {
  56. const loginResult = await login({ ...values, type: loginType });
  57. const { token=undefined } = loginResult.data
  58. if (loginResult.success && token) {
  59. saveToken(token)
  60. await fetchUserInfo();
  61. redirect();
  62. return;
  63. }
  64. // 如果失败去设置用户错误信息
  65. setUserLoginState(loginResult);
  66. } catch (error) {
  67. //这里是因为其它原因失败
  68. message.error('登录失败,请重试!');
  69. }
  70. setSubmitting(false);
  71. };
  72. return (
  73. <div className={styles.container}>
  74. <div className={styles.content}>
  75. <div className={styles.top}>
  76. <div className={styles.header}>
  77. <Link to="/">
  78. <img alt="logo" className={styles.logo} src="/logo.png" />
  79. </Link>
  80. </div>
  81. <div className={styles.desc}>基层工会工作云软件</div>
  82. </div>
  83. <div className={styles.main}>
  84. <ProForm
  85. form={form}
  86. initialValues={{
  87. //这里是为了调试方便,正式发布的时候必须删除
  88. username: 'admin',
  89. password:'admin',
  90. }}
  91. submitter={{
  92. searchConfig: {
  93. submitText: '登录',
  94. },
  95. render: (_, dom) => dom.pop(),
  96. submitButtonProps: {
  97. loading: submitting,
  98. size: 'large',
  99. style: {
  100. width: '100%',
  101. },
  102. },
  103. }}
  104. onFinish={handleSubmit}
  105. >
  106. <Tabs activeKey={loginType} onChange={setLoginType}>
  107. <Tabs.TabPane key="account" tab='账户密码登录' />
  108. <Tabs.TabPane key="mobile" tab='手机号登录' />
  109. </Tabs>
  110. {!userLoginState?.success && userLoginState?.type === 'account'
  111. && <LoginMessage content='账户或密码错误' /> }
  112. {loginType === 'account' && (
  113. <>
  114. <ProFormText
  115. name="username"
  116. placeholder='请输入用户名'
  117. rules={[...fieldRuls['required']]}
  118. fieldProps={{
  119. size: 'large',
  120. prefix: <UserOutlined className={styles.prefixIcon} />,
  121. }}
  122. />
  123. <ProFormText.Password
  124. name="password"
  125. placeholder='请输入密码'
  126. rules={[...fieldRuls['required']]}
  127. fieldProps={{
  128. size: 'large',
  129. prefix: <LockOutlined className={styles.prefixIcon} />,
  130. }}
  131. />
  132. </>
  133. )}
  134. {!userLoginState?.success && userLoginState?.type === 'mobile'
  135. && <LoginMessage content="验证码错误" />}
  136. {loginType === 'mobile' && (
  137. <>
  138. <ProFormText
  139. name="mobile"
  140. placeholder='输入手机号码'
  141. rules={[...fieldRuls['required'], ...fieldRuls['mobile']]}
  142. normalize={fidleNormalizes['mobile']}
  143. fieldProps={{
  144. size: 'large',
  145. autoComplete: 'off',
  146. prefix: <MobileOutlined className={styles.prefixIcon} />,
  147. }}
  148. />
  149. <ProFormCaptcha
  150. name="captcha"
  151. phoneName="mobile"
  152. placeholder='请输入验证码'
  153. rules={[...fieldRuls['required'], ...fieldRuls['captcha']]}
  154. normalize={fidleNormalizes['digit']}
  155. disabled={captchaDisabled}
  156. fieldProps={{
  157. size: 'large',
  158. autoComplete: 'off',
  159. maxLength: 6,
  160. prefix: <LockOutlined className={styles.prefixIcon} />,
  161. }}
  162. captchaProps={{
  163. size: 'large',
  164. }}
  165. captchaTextRender={(timing, count) => {
  166. if (timing) return `${count} ${'获取验证码'}`;
  167. return '获取验证码';
  168. }}
  169. onGetCaptcha={async (phone) => {
  170. try {
  171. setCaptchaDisabled(true)
  172. const result = await sendCaptcha( phone );
  173. if (result.success) {
  174. setCaptchaDisabled(false)
  175. form.setFieldsValue({captcha: ''})
  176. //注意:
  177. // antd 4.4.0中的getFieldInstance方法后来又失效了
  178. // 虽然可在fieldProps中强行使用ref,但会得运行时的警告
  179. // 所以只好用老办法得到实例
  180. document.getElementById('captcha')?.focus()
  181. message.success('已经把验证码发送到你的手机上');
  182. }
  183. } catch(e) {
  184. }
  185. }}
  186. />
  187. </>
  188. )}
  189. <div style={{ marginBottom: 12, textAlign: 'right'}} >
  190. <a>忘记密码 ?</a>
  191. </div>
  192. </ProForm>
  193. <Space className={styles.other}></Space>
  194. </div>
  195. </div>
  196. <Footer />
  197. </div>
  198. );
  199. };
  200. export default LoginPage;

16.6 增加Request全局拦截器

app.tsx中增加一个Request的全局拦截器,在所有网络请求的头部都附加上Token。
首先是修改引用定义

  1. -import type { ResponseError } from 'umi-request';
  2. +import type { ResponseError, RequestOptionsInit } from 'umi-request';
  3. +import { loadToken } from '@/utils/utils'
  4. -import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
  5. +import { currentUser as queryCurrentUser } from './services/api/user';
  6. -import { BookOutlined, LinkOutlined } from '@ant-design/icons';
  7. +import { BookOutlined } from '@ant-design/icons';

修改当前的类型定义

  1. - currentUser?: API.CurrentUser;
  2. + currentUser?: TYPE.User;
  3. - fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
  4. + fetchUserInfo?: () => Promise<TYPE.User | undefined>;

定义一个设置HTTP Request头的函数

  1. function setHeaders(url: string, options:RequestOptionsInit) {
  2. const { headers={}, ...rest } = options;
  3. const jwtToken = loadToken();
  4. if (jwtToken) {
  5. headers['Token'] = jwtToken;
  6. };
  7. return {
  8. url,
  9. options: { ...rest, headers },
  10. }
  11. }
  12. // https://umijs.org/zh-CN/plugins/plugin-request
  13. export const request: RequestConfig = {
  14. errorHandler,
  15. requestInterceptors: [setHeaders],
  16. };

定义全局拦截器

  1. export const request: RequestConfig = {
  2. + //请求拦截器
  3. + requestInterceptors: [setHeaders],
  4. + //异常处理程序
  5. errorHandler: (error: ResponseError) => {

16.7 全面修订Mock代码

把mock/user.ts原有内容全部删除,置入如下代码

  1. import { Request, Response } from 'express';
  2. const waitTime = (time: number = 100) => {
  3. return new Promise((resolve) => {
  4. setTimeout(() => {
  5. resolve(true);
  6. }, time);
  7. });
  8. };
  9. const adminToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyIn0.ib58asBJGXFLPNuu1nb8BAibo-Zi0luZt6UynBaVyX0'
  10. const notmalToken = '123.456.789'
  11. let userToken:string | undefined = undefined
  12. function makeToken(username = 'user'): string {
  13. if(username === 'admin')
  14. userToken = adminToken
  15. else
  16. userToken = notmalToken
  17. return userToken
  18. }
  19. function clearToken() {
  20. userToken = undefined
  21. }
  22. function checkToken(token: string | undefined): boolean {
  23. return userToken != undefined && token === userToken
  24. }
  25. let currentCaptcha: string | undefined;
  26. async function getFakeCaptcha(req: Request, res: Response) {
  27. currentCaptcha = Math.floor(Math.random() * 10000000) % 1000000 + '';
  28. currentCaptcha = currentCaptcha.padStart(6, '0');
  29. console.log("The captcha is ", currentCaptcha);
  30. return res.json({
  31. success: true,
  32. });
  33. }
  34. async function login(req: Request, res: Response) {
  35. const { password, username, captcha, type } = req.body;
  36. const result:TYPE.QueryResult & {type:string} = {
  37. success: true,
  38. type,
  39. data: {}
  40. }
  41. console.log("body = ", req.body)
  42. await waitTime(2000);
  43. if(type === 'account') {
  44. if (password === 'admin' && username === 'admin') {
  45. result.data['token'] = makeToken(username)
  46. }
  47. if (password === 'user' && username === 'user') {
  48. result.data['token'] = makeToken(username)
  49. }
  50. } else {
  51. if(currentCaptcha != captcha) {
  52. result.success = false;
  53. result.errorMessage = '验证码错误'
  54. } else
  55. result.data['token'] = makeToken()
  56. }
  57. return res.json(result);
  58. }
  59. function getUserInfo(req: Request, res: Response) {
  60. res.send({
  61. name: 'Serati Ma',
  62. avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
  63. userid: '00000001',
  64. email: 'antdesign@alipay.com',
  65. signature: '海纳百川,有容乃大',
  66. title: '交互专家',
  67. group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
  68. tags: [
  69. {
  70. key: '3',
  71. label: '大长腿',
  72. },
  73. {
  74. key: '5',
  75. label: '海纳百川',
  76. },
  77. ],
  78. notifyCount: 12,
  79. unreadCount: 11,
  80. country: 'China',
  81. geographic: {
  82. province: {
  83. label: '浙江省',
  84. key: '330000',
  85. },
  86. city: {
  87. label: '杭州市',
  88. key: '330100',
  89. },
  90. },
  91. address: '西湖区工专路 77 号',
  92. phone: '0752-268888888',
  93. });
  94. }
  95. function logout(req: Request, res: Response) {
  96. clearToken()
  97. res.send({ data: {}, success: true });
  98. }
  99. const mockFunction = {}
  100. const mockList = {}
  101. function filter(req: Request, res: Response) {
  102. if(!checkToken(req.headers.token as string)) {
  103. return res.json({
  104. success: false,
  105. errorCode: 401,
  106. errorMessage: '请登录',
  107. })
  108. }
  109. const index = req.url.indexOf('?')
  110. const url = index > 0? req.url.substring(0,index) : req.url
  111. return mockFunction[url](req, res);
  112. }
  113. function parseFilter() {
  114. var keys = Object.keys(filters)
  115. for(let key of keys) {
  116. const index = key.indexOf(' ')
  117. const url = index > 0? key.substring(index).trimLeft() : key
  118. mockList[key] = filter,
  119. mockFunction[url] = filters[key]
  120. }
  121. }
  122. const filters = {
  123. 'GET /api/user/getCurrentUser': getUserInfo,
  124. }
  125. parseFilter()
  126. export default {
  127. 'POST /api/user/login': login,
  128. 'POST /api/user/logout': logout,
  129. 'GET /api/user/sendCaptcha': getFakeCaptcha,
  130. ...mockList,
  131. };

请注意在上面的代码中,我们给Mock设计了一个自动化的过滤器的机制,所有需要检查Token状态的API都定义在在filters中,这些API会在被正式调用前执行自动检查。这个的作用和实际开发中后端的请求过滤器近似。

需要说明的是,这里的Mock代码只是对后端行为的简单模拟,请勿照搬Mock的逻辑来开发后端应用。后端应该严格执行所有的数据检查,不能默认相信任何前端传递的数据。

16.8 图标及页面风格

user/Login/index.tsx中用到的logo.png放到public目录下。修改user/Login/index.less

  1. .header {
  2. - height: 44px;
  3. - line-height: 44px;
  4. + height: 120px;
  5. + line-height: 1em;
  1. .logo {
  2. - height: 44px;
  3. + height: 100%;

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。