页面效果
实现使用手机号+验证码登录,自动登录
页面结构
使用Ant Design Pro Components
的LoginFormPage组件
js
pnpm i @ant-design/pro-components --save
- 登录使用login接口、发生短信使用sendCodeMsg接口
- 勾选自动登录,将token保存到localStorage,否则保存到sessionStorage(关闭标签页会清除)
src\views\login\index.tsx
ts
import React from 'react';
import { login, sendCodeMsg } from '@/apis/login';
import { LockOutlined, MobileOutlined } from '@ant-design/icons';
import {
LoginFormPage,
ProConfigProvider,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { message, Tabs, theme } from 'antd';
import { useNavigate } from 'react-router-dom';
import styles from './index.module.less';
interface IValue {
tel: string
code: string
autoLogin: boolean
}
const Login: React.FC = () => {
const { token } = theme.useToken();
const nav = useNavigate();
const handleOnFinish = async (values: IValue) => {
try {
const res = await login(values);
if (res.data) {
const { userInfo, refreshToken, accessToken } = res.data;
if (values.autoLogin) {
localStorage.setItem('accessToken', refreshToken);
localStorage.setItem('refreshToken', accessToken);
localStorage.setItem('userInfo', JSON.stringify(userInfo));
localStorage.setItem('autoLogin', JSON.stringify(values.autoLogin));
sessionStorage.setItem('accessToken', '');
sessionStorage.setItem('refreshToken', '');
sessionStorage.setItem('userInfo', '');
} else {
sessionStorage.setItem('accessToken', refreshToken);
sessionStorage.setItem('refreshToken', accessToken);
sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
localStorage.setItem('accessToken', '');
localStorage.setItem('refreshToken', '');
localStorage.setItem('userInfo', '');
localStorage.setItem('autoLogin', '');
}
message.success('登录成功');
nav('/');
}
} catch (error) {
console.log(error);
}
};
return (
<div className={styles.container}>
<LoginFormPage
logo="https://github.githubassets.com/images/modules/logos_page/Octocat.png"
backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
onFinish={handleOnFinish}
>
<Tabs
centered
items={[
{
key: 'phone',
label: '手机号登录',
},
]}
/>
<>
<ProFormText
fieldProps={{
size: 'large',
prefix: (
<MobileOutlined
style={{
color: token.colorText,
}}
className="prefixIcon"
/>
),
}}
name="tel"
placeholder="手机号"
rules={[
{
required: true,
message: '请输入手机号!',
},
{
pattern: /^1\d{10}$/,
message: '手机号格式错误!',
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: 'large',
prefix: (
<LockOutlined
style={{
color: token.colorText,
}}
className="prefixIcon"
/>
),
}}
captchaProps={{
size: 'large',
}}
placeholder="请输入验证码"
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${'获取验证码'}`;
}
return '获取验证码';
}}
name="code"
rules={[
{
required: true,
message: '请输入验证码!',
},
]}
phoneName="tel"
onGetCaptcha={async (tel: string) => {
await sendCodeMsg(tel);
message.success('获取验证码成功!');
}}
/>
</>
<div
style={{
marginBlockEnd: 24,
}}
>
<ProFormCheckbox noStyle name="autoLogin">
自动登录
</ProFormCheckbox>
</div>
</LoginFormPage>
</div>
);
};
export default () => (
<ProConfigProvider dark>
<Login />
</ProConfigProvider>
);
接口
封装axios
src\apis\index.ts
- 在请求拦截器添加accessToken
- 在响应拦截器添加失败回调,accessToken失效需要跳转到登录页面、用refreshToken重新请求获得新的accessToken、refreshToken
ts
import { message } from 'antd';
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
interface IResult<T> {
code: string
data: T
message: string
}
interface PendingTask {
config: AxiosRequestConfig
resolve: Function
}
const instance: AxiosInstance = axios.create({
baseURL: 'http://localhost:3000/', // 替换为你的 API 基础 URL
timeout: 5000, // 请求超时时间(毫秒)
});
// 请求拦截器
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error),
);
let refreshing = false;
const queue: PendingTask[] = [];
// 响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status === 200) {
return Promise.resolve(response.data);
}
return Promise.reject(response);
},
async (error) => {
if (!error.response) {
return Promise.reject(error);
}
const { data, config } = error.response;
if (refreshing) {
return new Promise((resolve) => {
queue.push({
config,
resolve,
});
});
}
if (data.code === 401 && !config.url.includes('/auth/refresh')) {
refreshing = true;
const res = await refreshToken();
refreshing = false;
if (res.code === '200') {
queue.forEach(({ config, resolve }) => {
resolve(instance(config));
});
return instance(config);
}
message.error(error.response.data.data);
setTimeout(() => {
window.location.href = '/login';
}, 1500);
} else {
message.error(error.response.data.data);
return Promise.reject(error);
}
return Promise.reject(error);
},
);
// 封装 GET 请求方法
export const get = async <T = any>(
url: string,
params?: object,
): Promise<IResult<T>> => {
try {
const response = (await instance.get(url, { params })) as IResult<T>;
return response;
} catch (error) {
return Promise.reject(error);
}
};
// 封装 POST 请求方法
export const post = async <T = any>(
url: string,
data?: object,
): Promise<IResult<T>> => {
try {
const response = (await instance.post(url, data)) as IResult<T>;
return response;
} catch (error) {
return Promise.reject(error);
}
};
async function refreshToken() {
const res = await get('/auth/refresh', {
params: {
refresh_token: localStorage.getItem('refreshToken'),
},
});
const autoLogin = localStorage.getItem('autoLogin');
if (autoLogin) {
localStorage.setItem('accessToken', res.data.accessToken || '');
localStorage.setItem('refreshToken', res.data.refreshToken || '');
} else {
sessionStorage.setItem('accessToken', res.data.accessToken || '');
sessionStorage.setItem('refreshToken', res.data.refreshToken || '');
}
return res;
}
src\apis\types.ts
ts
export type TloginParams = {
tel: string
code: string,
};
export interface IUserInfo {
id: string
desc: string
name: string
tel: string
avatar: string
}
export interface ILoginRes {
accessToken:string
refreshToken: string
userInfo: IUserInfo
}
src\apis\login.ts
ts
import { post } from '.';
import { ILoginRes, TloginParams } from './types';
// 发送验证码消息
export const sendCodeMsg = (tel: string) => post('auth/sendCodeMsg', { tel });
// 登录
export const login = (params:TloginParams) => post<ILoginRes>('auth/login', params);
路由
src\router\index.tsx
定义路由组件数组
ts
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';
const Login = lazy(() => import('@/views/login'));
const Home = lazy(() => import('@/views/home'));
const Page404 = lazy(() => import('@/views/404'));
export const routes: RouteObject[] = [
{
path: '/',
element: <Home />,
},
{
path: '/login',
element: <Login />,
},
{
path: '*',
element: <Page404 />,
},
];
src\App.tsx
在App
中用useRoutes
注册
ts
import { useRoutes } from 'react-router-dom';
import { routes } from './router';
function App() {
return useRoutes(routes);
}
export default App;
src\main.tsx
使用BrowserRouter
包裹App
ts
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'normalize.css';
import AuthRoute from './components/AuthRoute';
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
路由守卫
由于react-router-dom没有路由守卫,通过高阶组件实现 src\components\AuthRoute.tsx
- 如果token存在且有值,禁止用户回到登录页,重定向到首页
- 如果token不存在且访问的路径在白名单中可以跳转,不在白名单则重定向至登录页
ts
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
type RouteProps = {
children?: React.ReactNode
};
const loginRoute = '/login';
const indexRoute = '/';
// 路由表白名单
const allowList = ['/login', '/register'];
const AuthRoute: React.FC<RouteProps> = (props) => {
const location = useLocation();
// children 为子组件
const { children } = props;
const token = localStorage.getItem('accessToken') || sessionStorage.getItem('accessToken');
if (token && token !== 'undefined') {
// 有 token 的状态下禁止用户回到登录页,重定向到首页
if (location.pathname === loginRoute) {
return <Navigate to={indexRoute} />;
}
// 其他路由均可正常跳转
return <>{children}</>;
}
// 无 token 的状态下,如果要跳转的路由是白名单中的路由,正常跳转
if (allowList.includes(location.pathname || '')) {
return <>{children}</>;
}
// 无 token 且非白名单路由,重定向至登录页
return <Navigate to={loginRoute} />;
};
export default AuthRoute;
src\main.tsx
用AuthRoute组件
包裹App组件
ts
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'normalize.css';
import AuthRoute from './components/AuthRoute';
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<AuthRoute>
<App />
</AuthRoute>
</BrowserRouter>,
);
结果
发送短信成功
未勾选自动登录,把token保存到sessionStorage 勾选自动登录,把token保存到localStorage