【React】入门Day04 —— 项目搭建及登录与表单校验、token 管理、路由鉴权实现
项目搭建
-
创建项目
# 使用npx创建项目 npx create-react-app my-react-app # 进入项目目录 cd my-react-app # 创建项目目录结构 mkdir -p src/{apis,assets,components,pages,store,utils} touch src/{App.js,index.css,index.js}
- 使用
npx create-react-app
创建项目,进入项目目录后通过npm start
启动。 - 调整项目目录结构,包括
apis
、assets
、components
、pages
等多个文件夹。
- 使用
-
使用技术
- 接入
scss
预处理器,安装sass
工具,创建全局样式文件index.scss
。# 安装sass工具 npm i sass -D
// 在src/index.scss中设置全局样式 body { font-family: Arial, sans-serif; background-color: #f4f4f4; }
- 引入组件库
antd
,安装后在Login
页面测试Button
组件。# 安装antd组件库 npm i antd
// 在src/pages/Login/index.jsx中使用Button组件 import React from 'react'; import { Button } from 'antd'; const Login = () => { return ( <div> <Button type='primary'>登录</Button> </div> ); }; export default Login;
- 使用
react-router-dom
配置基础路由,创建Layout
和Login
组件并配置路由规则。# 安装react-router-dom npm i react-router-dom
// 在src/router/index.js中配置路由 import { createBrowserRouter } from 'react-router-dom'; import Login from '../pages/Login'; import Layout from '../pages/Layout'; const router = createBrowserRouter([ { path: '/', element: <Layout />, }, { path: '/login', element: <Login />, }, ]); export default router;
- 通过
craco
工具包配置别名路径,在craco.config.js
中设置webpack
别名,并在jsconfig.json
中配置VsCode
提示。# 安装craco工具包 npm i @craco/craco -D
// 在craco.config.js中配置别名 const path = require('path'); module.exports = { webpack: { alias: { '@': path.resolve(__dirname,'src') } } };
// 在package.json中修改scripts命令 "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" }
// 在src/router/index.js中使用别名 import { createBrowserRouter } from 'react-router-dom'; import Login from '@/pages/Login'; import Layout from '@/pages/Layout'; const router = createBrowserRouter([ { path: '/', element: <Layout />, }, { path: '/login', element: <Login />, }, ]); export default router;
// 在jsconfig.json中配置VsCode提示 { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } } }
- 接入
-
功能模块实现
-
登录模块
-
基本结构搭建
-
-
- 在
Login/index.js
创建登录页面结构,引入antd
组件,使用@/assets
路径引入图片,在Login/index.scss
中设置样式。 -
import React from 'react'; import { Card, Form, Input, Button } from 'antd'; import logo from '@/assets/logo.png'; import './index.scss'; const Login = () => { return ( <div className="login"> <Card className="login-container"> <img className="login-logo" src={logo} alt="" /> <Form> <Form.Item> <Input size="large" placeholder="请输入手机号" /> </Form.Item> <Form.Item> <Input size="large" placeholder="请输入验证码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" size="large" block> 登录 </Button> </Form.Item> </Form> </Card> </div> ); }; export default Login;
-
表单校验实现
- 为
Form
组件设置validateTrigger
,为Form.Item
组件设置name
和rules
属性进行表单校验。import React from 'react'; import { Form, Input, Button } from 'antd'; const Login = () => { return ( <Form validateTrigger={['onBlur']}> <Form.Item name="mobile" rules={[ { required: true, message: '请输入手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号码格式不对' } ]} > <Input size="large" placeholder="请输入手机号" /> </Form.Item> <Form.Item name="code" rules={[ { required: true, message: '请输入验证码' }, ]} > <Input size="large" placeholder="请输入验证码" maxLength={6} /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" size="large" block> 登录 </Button> </Form.Item> </Form> ); }; export default Login;
- 为
-
获取登录表单数据
- 为
Form
组件设置onFinish
属性,在点击登录按钮时触发获取表单数据的函数。import React from 'react'; import { Form, Input, Button } from 'antd'; const Login = () => { const onFinish = formValue => { console.log(formValue); }; return ( <Form onFinish={onFinish}> <Form.Item> <Input size="large" placeholder="请输入手机号" /> </Form.Item> <Form.Item> <Input size="large" placeholder="请输入验证码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" size="large" block> 登录 </Button> </Form.Item> </Form> ); }; export default Login;
- 为
-
封装 request 工具模块
- 安装
axios
,在utils/request.js
中创建axios
实例,配置baseURL
、请求拦截器和响应拦截器。# 安装axios npm i axios
import axios from 'axios'; const http = axios.create({ baseURL: 'http://example.com/api', timeout: 5000 }); // 请求拦截器 http.interceptors.request.use(config => { return config; }, error => { return Promise.reject(error); }); // 响应拦截器 http.interceptors.response.use(response => { return response.data; }, error => { return Promise.reject(error); }); export { http };
- 安装
-
使用 Redux 管理 token
- 安装
react-redux
和@reduxjs/toolkit
,在store
中创建userStore
切片,设置token
初始状态和setUserInfo
等reducers
,封装fetchLogin
异步方法。# 安装react-redux和@reduxjs/toolkit npm i react-redux @reduxjs/toolkit
import { createSlice } from '@reduxjs/toolkit'; import { http } from '@/utils'; const userStore = createSlice({ name: 'user', initialState: { token: '' }, reducers: { setUserInfo(state, action) { state.token = action.payload; } } }); const { setUserInfo } = userStore.actions; const userReducer = userStore.reducer; const fetchLogin = loginForm => { return async dispatch => { const res = await http.post('/authorizations', loginForm); dispatch(setUserInfo(res.data.token)); }; }; export { fetchLogin }; export default userReducer;
- 安装
-
实现登录逻辑
- 在
Login
组件中调用fetchLogin
方法,登录成功后跳转到首页并提示。import React from 'react'; import { message } from 'antd'; import { useDispatch } from 'react-redux'; import { fetchLogin } from '@/store/modules/user'; const Login = () => { const dispatch = useDispatch(); const onFinish = async formValue => { await dispatch(fetchLogin(formValue)); message.success('登录成功'); }; return ( <div> <form onSubmit={onFinish}> {/* 登录表单字段 */} </form> </div> ); }; export default Login;
- 在
-
token 持久化
- 封装
setToken
、getToken
和clearToken
方法,在userStore
中setUserInfo
时将token
存入本地。// 在@/utils/token.js中封装存取方法 const TOKENKEY = 'token_key'; function setToken(token) { return localStorage.setItem(TOKENKEY, token); } function getToken() { return localStorage.getItem(TOKENKEY); } function clearToken() { return localStorage.removeItem(TOKENKEY); } export { setToken, getToken, clearToken };
// 在userStore中使用token持久化方法 import { createSlice } from '@reduxjs/toolkit'; import { http } from '@/utils'; import { getToken, setToken } from '@/utils/token'; const userStore = createSlice({ name: 'user', initialState: { token: getToken() || '' }, reducers: { setUserInfo(state, action) { state.token = action.payload; setToken(state.token); } } }); export default userStore;
- 封装
-
请求拦截器注入 token
- 在
request.js
的请求拦截器中,判断是否有token
,有则添加到请求头Authorization
中。// 在utils/request.js中注入token import axios from 'axios'; const http = axios.create({ baseURL: 'http://example.com/api', timeout: 5000 }); http.interceptors.request.use(config => { const token = getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); }); http.interceptors.response.use(response => { return response.data; }, error => { return Promise.reject(error); }); export { http };
- 在
-
路由鉴权实现
- 在
components/AuthRoute/index.jsx
中创建路由鉴权高阶组件,判断本地是否有token
,决定是否重定向到登录页面。import React from 'react'; import { Navigate } from 'react-router-dom'; import { getToken } from '@/utils'; const AuthRoute = ({ children }) => { const isToken = getToken(); if (isToken) { return <>{children}</>; } else { return <Navigate to="/login" replace />; } }; export default AuthRoute;
// 在src/router/index.js中使用AuthRoute组件 import { createBrowserRouter } from 'react-router-dom'; import Login from '@/pages/Login'; import Layout from '@/pages/Layout'; import AuthRoute from '@/components/AuthRoute'; const router = createBrowserRouter([ { path: '/', element: <AuthRoute><Layout /></AuthRoute>, }, { path: '/login', element: <Login />, }, ]); export default router;
- 在
-
Layout 模块
-
基本结构和样式 reset
- 在
pages/Layout/index.js
中使用antd/Layout
组件创建页面结构,引入antd
的Menu
和Popconfirm
等组件,设置样式并安装normalize.css
进行样式 reset。import React from 'react'; import { Layout, Menu, Popconfirm } from 'antd'; import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons'; import './index.scss'; import 'normalize.css'; const { Header, Sider } = Layout; const items = [ { label: '首页', key: '1', icon: <HomeOutlined />, }, { label: '文章管理', key: '2', icon: <DiffOutlined />, }, { label: '创建文章', key: '3', icon: <EditOutlined />, }, ]; const GeekLayout = () => { return ( <Layout> <Header className="header"> <div className="logo" /> <div className="user-info"> <span className="user-name">用户名</span> <span className="user-logout"> <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"> <LogoutOutlined /> 退出 </Popconfirm> </span> </div> </Header> <Layout> <Sider width={200} className="site-layout-background"> <Menu mode="inline" theme="dark" defaultSelectedKeys={['1']} items={items} style={{ height: '100%', borderRight: 0 }} ></Menu> </Sider> <Layout className="layout-content" style={{ padding: 20 }}> 内容 </Layout> </Layout> </Layout> ); }; export default GeekLayout;
- 在
-
二级路由配置
- 在
pages
目录创建Home
、Article
、Publish
页面文件夹,在router/index.js
中配置嵌套子路由,在Layout
中配置二级路由出口,使用Link
修改左侧菜单内容实现路由切换。// 在pages目录创建Home.jsx import React from 'react'; const Home = () => { return <div>首页内容</div>; }; export default Home;
// 在pages目录创建Article.jsx import React from 'react'; const Article = () => { return <div>文章管理内容</div>; }; export default Article;
// 在pages目录创建Publish.jsx import React from 'react'; const Publish = () => { return <div>发布文章内容</div>; }; export default Publish;
// 在src/router/index.js中配置二级路由 import { createBrowserRouter } from 'react-router-dom'; import Login from '@/pages/Login'; import Layout from '@/pages/Layout'; import Publish from '@/pages/Publish'; import Article from '@/pages/Article'; import Home from '@/pages/Home'; import { AuthRoute } from '@/components/AuthRoute'; const router = createBrowserRouter([ { path: '/', element: ( <AuthRoute> <Layout /> </AuthRoute> ), children: [ { index: true, element: <Home />, }, { path: 'article', element: <Article />, }, { path: 'publish', element: <Publish />, }, ], }, { path: '/login', element: <Login />, }, ]); export default router;
// 在Layout组件中配置二级路由出口 import React from 'react'; import { Outlet } from 'react-router-dom'; const GeekLayout = () => { return ( <Layout className="layout-content" style={{ padding: 20 }}> <Outlet /> </Layout> ); }; export default GeekLayout;
- 在
-
路由菜单点击交互实现
- 为
Menu
组件设置onClick
属性实现点击菜单跳转路由,通过useLocation
获取当前路由路径实现菜单反向高亮。import React from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons'; const items = [ { label: '首页', key: '/', icon: <HomeOutlined />, }, { label: '文章管理', key: '/article', icon: <DiffOutlined />, }, { label: '创建文章', key: '/publish', icon: <EditOutlined />, }, ]; const GeekLayout = () => { const navigate = useNavigate(); const menuClick = route => { navigate(route.key); }; return ( <Layout> <Header className="main-header"> <div className="logo" /> <div className="user-info"> <span className="user-name">用户名</span> <span className="user-logout"> <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"> <LogoutOutlined /> 退出 </Popconfirm> </span> </div> </Header> <Layout> <Sider width={200} className="site-layout-background"> <Menu mode="inline" theme="dark" selectedKeys={['1']} items={items} style={{ height: '100%', borderRight: 0 }} onClick={menuClick} ></Menu> </Sider> <Layout className="layout-content" style={{ padding: 20 }}> <Outlet /> </Layout> </Layout> ); }; export default GeekLayout;
// 菜单反向高亮实现 import React from 'react'; import { Outlet, useLocation } from 'react-router-dom'; import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons'; const items = [ { label: '首页', key: '/', icon: <HomeOutlined />, }, { label: '文章管理', key: '/article', icon: <DiffOutlined />, }, { label: '创建文章', key: '/publish', icon: <EditOutlined />, }, ]; const GeekLayout = () => { const location = useLocation(); const selectedKey = location.pathname; return ( <Layout> <Header className="main-header"> <div className
- 为
-
展示个人信息
- 在
store/userStore.js
中编写获取用户信息的逻辑,在Layout
组件中触发fetchUserInfo
方法获取信息并渲染用户名。// store/userStore.js import { createSlice } from '@reduxjs/toolkit'; import { http } from '@/utils'; import { getToken, setToken } from '@/utils'; const userStore = createSlice({ name: 'user', initialState: { token: getToken() || '', userInfo: {} }, reducers: { setUserToken(state, action) { state.token = action.payload; setToken(state.token); }, setUserInfo(state, action) { state.userInfo = action.payload; }, clearUserInfo(state) { state.token = ''; state.userInfo = {}; clearToken(); } } }); // 解构出actionCreater const { setUserToken, setUserInfo, clearUserInfo } = userStore.actions; // 获取reducer函数 const userReducer = userStore.reducer; const fetchLogin = (loginForm) => { return async (dispatch) => { const res = await http.post('/authorizations', loginForm); dispatch(setUserToken(res.data.token)); }; }; const fetchUserInfo = () => { return async (dispatch) => { const res = await http.get('/user/profile'); dispatch(setUserInfo(res.data)); }; }; export { fetchLogin, fetchUserInfo, clearUserInfo }; export default userReducer;
- 在
-
退出登录实现
- 为
Popconfirm
添加确认回调事件,在store/userStore.js
中新增clearUserInfo
方法删除token
和用户信息,在回调事件中调用该方法并返回登录页面。// pages/Layout/index.js import React, { useEffect } from 'react'; import { Layout, Menu, Popconfirm } from 'antd'; import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined, } from '@ant-design/icons'; import { useDispatch, useSelector } from 'react-redux'; import { fetchUserInfo } from '@/store/modules/user'; const { Header, Sider } = Layout; const items = [ // 菜单配置项 ]; const GeekLayout = () => { const dispatch = useDispatch(); const name = useSelector(state => state.user.userInfo.name); useEffect(() => { dispatch(fetchUserInfo()); }, [dispatch]); const loginOut = () => { dispatch(clearUserInfo()); // 假设这里有合适的导航函数,替换为实际的导航逻辑 // navigate('/login'); }; return ( <Layout> <Header className="header"> <div className="logo" /> <div className="user-info"> <span className="user-name">{name}</span> <span className="user-logout"> <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={loginOut} > <LogoutOutlined /> 退出 </Popconfirm> </span> </div> </Header> <Layout> <Sider width={200} className="site-layout-background"> <Menu mode="inline" theme="dark" defaultSelectedKeys={['1']} items={items} style={{ height: '100%', borderRight: 0 }} ></Menu> </Sider> <Layout className="layout-content" style={{ padding: 20 }}> {/* 页面内容 */} </Layout> </Layout> </Layout> ); }; export default GeekLayout;
- 为
-
处理 Token 失效
- 在
http.interceptors.response
中判断响应状态码为401
时,清除token
,跳转到登录页面并刷新页面。// 在http.js(假设是配置axios请求相关的文件)中处理Token失效 import axios from 'axios'; const http = axios.create({ baseURL: 'http://example.com/api', timeout: 5000 }); http.interceptors.response.use((response) => { return response.data; }, (error) => { if (error.response && error.response.status === 401) { // 假设这里有合适的获取和清除token的函数,替换为实际的逻辑 const token = getToken(); if (token) { clearToken(); } // 假设这里有合适的导航函数,替换为实际的导航逻辑 // navigate('/login'); window.location.reload(); } return Promise.reject(error); }); export { http };
- 在
-