项目搭建
-
创建项目
# 使用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 axiosimport 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/toolkitimport { 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 };
- 在
-



















