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 }) => (
<Alert
message={content}
type="error"
showIcon
style={{
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 或 mobile
const [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.data
if (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}>
<ProForm
form={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' && (
<>
<ProFormText
name="username"
placeholder='请输入用户名'
rules={[...fieldRuls['required']]}
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
/>
<ProFormText.Password
name="password"
placeholder='请输入密码'
rules={[...fieldRuls['required']]}
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
}}
/>
</>
)}
{!userLoginState?.success && userLoginState?.type === 'mobile'
&& <LoginMessage content="验证码错误" />}
{loginType === 'mobile' && (
<>
<ProFormText
name="mobile"
placeholder='输入手机号码'
rules={[...fieldRuls['required'], ...fieldRuls['mobile']]}
normalize={fidleNormalizes['mobile']}
fieldProps={{
size: 'large',
autoComplete: 'off',
prefix: <MobileOutlined className={styles.prefixIcon} />,
}}
/>
<ProFormCaptcha
name="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-request
export 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 = undefined
function makeToken(username = 'user'): string {
if(username === 'admin')
userToken = adminToken
else
userToken = notmalToken
return 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 = '验证码错误'
} else
result.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.url
return 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() : key
mockList[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%;
版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。