项目搭建
创建项目
javascript
# 使用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
。javascript# 安装sass工具 npm i sass -D
javascript// 在src/index.scss中设置全局样式 body { font-family: Arial, sans-serif; background-color: #f4f4f4; }
-
引入组件库
antd
,安装后在Login
页面测试Button
组件。javascript# 安装antd组件库 npm i antd
javascript// 在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
组件并配置路由规则。javascript# 安装react-router-dom npm i react-router-dom
javascript// 在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
提示。javascript# 安装craco工具包 npm i @craco/craco -D
javascript// 在craco.config.js中配置别名 const path = require('path'); module.exports = { webpack: { alias: { '@': path.resolve(__dirname,'src') } } };
javascript// 在package.json中修改scripts命令 "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" }
javascript// 在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;
javascript// 在jsconfig.json中配置VsCode提示 { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } } }
功能模块实现
### **登录模块**
*
#### **基本结构搭建**
- 在
Login/index.js
创建登录页面结构,引入antd
组件,使用@/assets
路径引入图片,在Login/index.scss
中设置样式。
javascript
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
属性进行表单校验。javascriptimport 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
属性,在点击登录按钮时触发获取表单数据的函数。javascriptimport 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
、请求拦截器和响应拦截器。javascript# 安装axios npm i axios
javascriptimport 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
异步方法。javascript# 安装react-redux和@reduxjs/toolkit npm i react-redux @reduxjs/toolkit
javascriptimport { 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
方法,登录成功后跳转到首页并提示。javascriptimport 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
存入本地。javascript// 在@/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 };
javascript// 在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
中。javascript// 在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
,决定是否重定向到登录页面。javascriptimport 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;
javascript// 在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。
```javascript
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`修改左侧菜单内容实现路由切换。
```javascript
// 在pages目录创建Home.jsx
import React from 'react';
const Home = () => {
return <div>首页内容</div>;
};
export default Home;
```
```javascript
// 在pages目录创建Article.jsx
import React from 'react';
const Article = () => {
return <div>文章管理内容</div>;
};
export default Article;
```
```javascript
// 在pages目录创建Publish.jsx
import React from 'react';
const Publish = () => {
return <div>发布文章内容</div>;
};
export default Publish;
```
```javascript
// 在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;
```
```javascript
// 在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`获取当前路由路径实现菜单反向高亮。
```javascript
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;
```
```javascript
// 菜单反向高亮实现
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`方法获取信息并渲染用户名。
```javascript
// 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`和用户信息,在回调事件中调用该方法并返回登录页面。
```javascript
// 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`,跳转到登录页面并刷新页面。
```javascript
// 在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 };
```