Ant Design Pro 生成的代码中包含了用户认证部分的设计,但这部分只是基本的示意,并不完整,不能拿来直接使用。并且有些代码可以比较明显的看出是不同工程师编写的,质量不理想。
16.1 公共的Token函数
虽然保存和读取Token是很简单的操作,但我们还是要把它设计成公共函数放在src/utils/utils.ts,这样做的理由是如果修改存储Token的方案,只需要修改这两个函数。下面是用sessionStorage保存Token的代码
export function saveToken(token: string) {sessionStorage.setItem('token',token)}export function loadToken(): string | null {return sessionStorage.getItem("token");}
16.2 重新定义数据类型
删除src/services/ant-design-pro/typings.d.ts中的CurrentUser、LoginResult、FakeCaptcha、LoginParams。
在src/services/type.d.ts中增加定义
type User = {name?: string;authorization?: any;avatar?: string;userid?: string;email?: string;signature?: string;title?: string;group?: string;tags?: { key?: string; label?: string }[];notifyCount?: number;unreadCount?: number;country?: string;geographic?: {province?: { label?: string; key?: string };city?: { label?: string; key?: string };};address?: string;phone?: string;};type LoginParams = {username?: string;password?: string;mobile?: string;captcha?: string;type?: string;};type LoginResult = {success: boolean;type?: string;data?: any;};
把app.tsx中的API.CurrentUser修改成TYPE.User。
16.3 定义新的用户信息接口
删除src/services/ant-design-pro/login.ts文件,修改src/services/ant-design-pro/index.ts
import * as api from './api';-import * as login from './login';export default {api,- login,};
在src/services/ant-design-pro/api.ts中删除下面的内容
/** 获取当前的用户 GET /api/currentUser */export async function currentUser(options?: { [key: string]: any }) {return request<API.CurrentUser>('/api/currentUser', {method: 'GET',...(options || {}),});}/** 退出登录接口 POST /api/login/outLogin */export async function outLogin(options?: { [key: string]: any }) {return request<Record<string, any>>('/api/login/outLogin', {method: 'POST',...(options || {}),});}/** 登录接口 POST /api/login/account */export async function login(body: API.LoginParams, options?: { [key: string]: any }) {return request<API.LoginResult>('/api/login/account', {method: 'POST',headers: {'Content-Type': 'application/json',},data: body,...(options || {}),});}/** 获取当前的用户 GET /api/currentUser */export async function currentUser(options?: { [key: string]: any }) {return request<API.CurrentUser>('/api/currentUser', {method: 'GET',...(options || {}),});}/** 登录接口 POST /api/login/outLogin */export async function outLogin(options?: { [key: string]: any }) {return request<Record<string, any>>('/api/login/outLogin', {method: 'POST',...(options || {}),});}/** 登录接口 POST /api/login/account */export async function login(body: TYPE.LoginParams, options?: { [key: string]: any }) {return request<TYPE.LoginResult>('/api/login/account', {method: 'POST',headers: {'Content-Type': 'application/json',},data: body,...(options || {}),});}
创建新文件services/api/user.ts
// @ts-ignore/* eslint-disable */import { request } from 'umi';export async function login(body: TYPE.LoginParams, options?: { [key: string]: any }) {return request<TYPE.LoginResult>('/api/user/login', {method: 'POST',data: body,...(options || {}),});}export async function logout(options?: { [key: string]: any }) {return request<Record<string, any>>('/api/user/logout', {method: 'POST',...(options || {}),});}export async function sendCaptcha( phone: string) {return request('/api/user/sendCaptcha', {method: 'POST',params: {phone},});}export async function currentUser(options?: { [key: string]: any }) {return request<API.CurrentUser>('/api/user/getCurrentUser', {method: 'GET',...(options || {}),});}
16.4 修改用户登出的代码
在src/components/RightContent/AvatarDropdown.tsx中
-import { outLogin } from '@/services/ant-design-pro/api';+import { logout } from '@/services/api/user';
-const loginOut = async () => {- await outLogin();+const handleLogout = async () => {+ try {+ await logout();+ } catch(e) {+ }
setInitialState({ ...initialState, currentUser: undefined });- loginOut()+ handleLogout();
替logout问一句:loginOut、outLogin,这都是些什么鬼?
16.5 全面修订用户登录页
把src/pages/user/Login/index.tsx原有内容全部删除,置入如下代码
import React, { useState } from 'react';import { Link, history, useModel } from 'umi';import { Alert, Space, message, Tabs, Form } from 'antd';import ProForm, { ProFormCaptcha, ProFormText } from '@ant-design/pro-form';import { LockOutlined, MobileOutlined, UserOutlined } from '@ant-design/icons';import Footer from '@/components/Footer';import { login, sendCaptcha } from '@/services/api/user';import { fieldRuls, fidleNormalizes } from '@/utils/form-validator'import { saveToken } from '@/utils/utils'import styles from './index.less';const LoginMessage: React.FC<{content: string;}> = ({ content }) => (<Alertmessage={content}type="error"showIconstyle={{marginBottom: 24,}}/>);/** 此方法会跳转到 redirect 参数所在的位置 */const redirect = () => {if (!history) return;setTimeout(() => {const { query } = history.location;const { redirect } = query as {redirect: string;};history.push(redirect || '/');}, 10);};const LoginPage: React.FC = () => {//指示是否正在提交登录的数据const [submitting, setSubmitting] = useState(false);//登录的方式:account 或 mobileconst [loginType, setLoginType] = useState<string>('mobile');//申请登录的结果const [userLoginState, setUserLoginState] = useState<TYPE.LoginResult>();//注意这个写法可以获得app.tsx中定义的全局共享数据const { initialState, setInitialState } = useModel('@@initialState');const [form] = Form.useForm();const [captchaDisabled, setCaptchaDisabled] = useState(true)//向服务器请求用户信息,如果成功了就把它放到全局共享数据中const fetchUserInfo = async () => {const userInfo = await initialState?.fetchUserInfo?.();if (userInfo) {setInitialState({ ...initialState, currentUser: userInfo });}};//执行登录请求的函数,用户输入的信息被透传给后端,由后端进行判断const handleSubmit = async (values: TYPE.LoginParams) => {setSubmitting(true);try {const loginResult = await login({ ...values, type: loginType });const { token=undefined } = loginResult.dataif (loginResult.success && token) {saveToken(token)await fetchUserInfo();redirect();return;}// 如果失败去设置用户错误信息setUserLoginState(loginResult);} catch (error) {//这里是因为其它原因失败message.error('登录失败,请重试!');}setSubmitting(false);};return (<div className={styles.container}><div className={styles.content}><div className={styles.top}><div className={styles.header}><Link to="/"><img alt="logo" className={styles.logo} src="/logo.png" /></Link></div><div className={styles.desc}>基层工会工作云软件</div></div><div className={styles.main}><ProFormform={form}initialValues={{//这里是为了调试方便,正式发布的时候必须删除username: 'admin',password:'admin',}}submitter={{searchConfig: {submitText: '登录',},render: (_, dom) => dom.pop(),submitButtonProps: {loading: submitting,size: 'large',style: {width: '100%',},},}}onFinish={handleSubmit}><Tabs activeKey={loginType} onChange={setLoginType}><Tabs.TabPane key="account" tab='账户密码登录' /><Tabs.TabPane key="mobile" tab='手机号登录' /></Tabs>{!userLoginState?.success && userLoginState?.type === 'account'&& <LoginMessage content='账户或密码错误' /> }{loginType === 'account' && (<><ProFormTextname="username"placeholder='请输入用户名'rules={[...fieldRuls['required']]}fieldProps={{size: 'large',prefix: <UserOutlined className={styles.prefixIcon} />,}}/><ProFormText.Passwordname="password"placeholder='请输入密码'rules={[...fieldRuls['required']]}fieldProps={{size: 'large',prefix: <LockOutlined className={styles.prefixIcon} />,}}/></>)}{!userLoginState?.success && userLoginState?.type === 'mobile'&& <LoginMessage content="验证码错误" />}{loginType === 'mobile' && (<><ProFormTextname="mobile"placeholder='输入手机号码'rules={[...fieldRuls['required'], ...fieldRuls['mobile']]}normalize={fidleNormalizes['mobile']}fieldProps={{size: 'large',autoComplete: 'off',prefix: <MobileOutlined className={styles.prefixIcon} />,}}/><ProFormCaptchaname="captcha"phoneName="mobile"placeholder='请输入验证码'rules={[...fieldRuls['required'], ...fieldRuls['captcha']]}normalize={fidleNormalizes['digit']}disabled={captchaDisabled}fieldProps={{size: 'large',autoComplete: 'off',maxLength: 6,prefix: <LockOutlined className={styles.prefixIcon} />,}}captchaProps={{size: 'large',}}captchaTextRender={(timing, count) => {if (timing) return `${count} ${'获取验证码'}`;return '获取验证码';}}onGetCaptcha={async (phone) => {try {setCaptchaDisabled(true)const result = await sendCaptcha( phone );if (result.success) {setCaptchaDisabled(false)form.setFieldsValue({captcha: ''})//注意:// antd 4.4.0中的getFieldInstance方法后来又失效了// 虽然可在fieldProps中强行使用ref,但会得运行时的警告// 所以只好用老办法得到实例document.getElementById('captcha')?.focus()message.success('已经把验证码发送到你的手机上');}} catch(e) {}}}/></>)}<div style={{ marginBottom: 12, textAlign: 'right'}} ><a>忘记密码 ?</a></div></ProForm><Space className={styles.other}></Space></div></div><Footer /></div>);};export default LoginPage;
16.6 增加Request全局拦截器
在app.tsx中增加一个Request的全局拦截器,在所有网络请求的头部都附加上Token。
首先是修改引用定义
-import type { ResponseError } from 'umi-request';+import type { ResponseError, RequestOptionsInit } from 'umi-request';+import { loadToken } from '@/utils/utils'-import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';+import { currentUser as queryCurrentUser } from './services/api/user';-import { BookOutlined, LinkOutlined } from '@ant-design/icons';+import { BookOutlined } from '@ant-design/icons';
修改当前的类型定义
- currentUser?: API.CurrentUser;+ currentUser?: TYPE.User;- fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;+ fetchUserInfo?: () => Promise<TYPE.User | undefined>;
定义一个设置HTTP Request头的函数
function setHeaders(url: string, options:RequestOptionsInit) {const { headers={}, ...rest } = options;const jwtToken = loadToken();if (jwtToken) {headers['Token'] = jwtToken;};return {url,options: { ...rest, headers },}}// https://umijs.org/zh-CN/plugins/plugin-requestexport const request: RequestConfig = {errorHandler,requestInterceptors: [setHeaders],};
定义全局拦截器
export const request: RequestConfig = {+ //请求拦截器+ requestInterceptors: [setHeaders],+ //异常处理程序errorHandler: (error: ResponseError) => {
16.7 全面修订Mock代码
把mock/user.ts原有内容全部删除,置入如下代码
import { Request, Response } from 'express';const waitTime = (time: number = 100) => {return new Promise((resolve) => {setTimeout(() => {resolve(true);}, time);});};const adminToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyIn0.ib58asBJGXFLPNuu1nb8BAibo-Zi0luZt6UynBaVyX0'const notmalToken = '123.456.789'let userToken:string | undefined = undefinedfunction makeToken(username = 'user'): string {if(username === 'admin')userToken = adminTokenelseuserToken = notmalTokenreturn userToken}function clearToken() {userToken = undefined}function checkToken(token: string | undefined): boolean {return userToken != undefined && token === userToken}let currentCaptcha: string | undefined;async function getFakeCaptcha(req: Request, res: Response) {currentCaptcha = Math.floor(Math.random() * 10000000) % 1000000 + '';currentCaptcha = currentCaptcha.padStart(6, '0');console.log("The captcha is ", currentCaptcha);return res.json({success: true,});}async function login(req: Request, res: Response) {const { password, username, captcha, type } = req.body;const result:TYPE.QueryResult & {type:string} = {success: true,type,data: {}}console.log("body = ", req.body)await waitTime(2000);if(type === 'account') {if (password === 'admin' && username === 'admin') {result.data['token'] = makeToken(username)}if (password === 'user' && username === 'user') {result.data['token'] = makeToken(username)}} else {if(currentCaptcha != captcha) {result.success = false;result.errorMessage = '验证码错误'} elseresult.data['token'] = makeToken()}return res.json(result);}function getUserInfo(req: Request, res: Response) {res.send({name: 'Serati Ma',avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',userid: '00000001',email: 'antdesign@alipay.com',signature: '海纳百川,有容乃大',title: '交互专家',group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',tags: [{key: '3',label: '大长腿',},{key: '5',label: '海纳百川',},],notifyCount: 12,unreadCount: 11,country: 'China',geographic: {province: {label: '浙江省',key: '330000',},city: {label: '杭州市',key: '330100',},},address: '西湖区工专路 77 号',phone: '0752-268888888',});}function logout(req: Request, res: Response) {clearToken()res.send({ data: {}, success: true });}const mockFunction = {}const mockList = {}function filter(req: Request, res: Response) {if(!checkToken(req.headers.token as string)) {return res.json({success: false,errorCode: 401,errorMessage: '请登录',})}const index = req.url.indexOf('?')const url = index > 0? req.url.substring(0,index) : req.urlreturn mockFunction[url](req, res);}function parseFilter() {var keys = Object.keys(filters)for(let key of keys) {const index = key.indexOf(' ')const url = index > 0? key.substring(index).trimLeft() : keymockList[key] = filter,mockFunction[url] = filters[key]}}const filters = {'GET /api/user/getCurrentUser': getUserInfo,}parseFilter()export default {'POST /api/user/login': login,'POST /api/user/logout': logout,'GET /api/user/sendCaptcha': getFakeCaptcha,...mockList,};
请注意在上面的代码中,我们给Mock设计了一个自动化的过滤器的机制,所有需要检查Token状态的API都定义在在filters中,这些API会在被正式调用前执行自动检查。这个的作用和实际开发中后端的请求过滤器近似。
需要说明的是,这里的Mock代码只是对后端行为的简单模拟,请勿照搬Mock的逻辑来开发后端应用。后端应该严格执行所有的数据检查,不能默认相信任何前端传递的数据。
16.8 图标及页面风格
把user/Login/index.tsx中用到的logo.png放到public目录下。修改user/Login/index.less
.header {- height: 44px;- line-height: 44px;+ height: 120px;+ line-height: 1em;
.logo {- height: 44px;+ height: 100%;
版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。
