实战演练(二):结合路由与状态管理,构建一个小型博客前台
摘要: 本文是《React奇妙之旅》系列的第19站,指导读者综合运用React Router、Redux Toolkit、Axios和Tailwind CSS等技术,构建一个功能完整的小型博客前台应用。项目包含文章列表页、详情页、用户页,并实现全局加载状态与数据缓存。通过模块化目录结构设计,结合RTK的异步请求管理,最终完成一个接近真实场景的SPA应用。文章从技术选型到代码实现逐步展开,帮助开发者
实战演练(二):结合路由与状态管理,构建一个小型博客前台
作者:码力无边
各位React全栈探险家,欢迎来到《React奇妙之旅》的第十九站!我是你们的首席架构师码力无边。我们已经航行了很长的距离,从基础的JSX语法到高级的状态管理,从组件的样式美化到代码的质量保障。我们收集了满船的“宝藏”:React Router, Redux Toolkit, 自定义Hooks, CSS方案, 甚至自动化测试。
现在,是时候将所有这些宝藏融会贯通,建造一艘真正属于我们自己的、能够远航的“旗舰”了!
今天的任务,将是我们迄今为止最全面、最接近真实世界项目的一次挑战。我们将从零开始,综合运用之前学到的所有核心技能,构建一个功能完备的小型博客前台应用。这个应用将不仅仅是一个组件的堆砌,它将是一个拥有多页面导航、全局状态管理、真实API数据交互、漂亮UI以及高质量代码的完整SPA。
这篇文章将是你从“学习者”向“构建者”转变的毕业典礼。它将检验你是否真正理解并能灵活运用React生态的全貌。准备好迎接这次终极挑战,将你的知识锻造成真正的产品了吗?让我们开始这次激动人心的构建之旅!
第一章:项目蓝图与技术选型
在动工之前,让我们先画好蓝图。
核心功能:
- 文章列表页 (
/posts
): 显示所有文章的标题列表。 - 文章详情页 (
/posts/:postId
): 显示单篇文章的完整内容,包括标题、正文和作者信息。 - 用户详情页 (
/users/:userId
): 显示单个用户的详细信息(姓名、邮箱等)。 - 全局加载指示器: 在任何API请求进行中时,显示一个全局的加载提示。
- 数据缓存: 避免对相同的数据进行重复的网络请求。
技术栈选型:
- 脚手架: Vite
- 核心框架: React
- 路由: React Router v6
- 状态管理: Redux Toolkit (RTK)
- 数据请求: Axios (封装在RTK的
createAsyncThunk
中) - 样式: Tailwind CSS (因为它能让我们快速构建出不错的UI)
- API源: JSONPlaceholder (一个绝佳的免费Mock API)
第二章:项目初始化与目录结构规划
-
创建项目:
npm create vite@latest my-react-blog -- --template react cd my-react-blog
-
安装依赖:
npm install react-router-dom @reduxjs/toolkit react-redux axios npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p # 初始化Tailwind配置
-
配置Tailwind CSS:
- 根据Tailwind CSS官方文档为Vite项目进行配置。这主要涉及修改
tailwind.config.js
的content
字段和在主CSS文件(如index.css
)中引入Tailwind的指令。
- 根据Tailwind CSS官方文档为Vite项目进行配置。这主要涉及修改
-
规划目录结构:
src/ ├── api/ # API请求相关配置 (例如axios实例) ├── app/ # Redux store的配置 ├── components/ # 通用的、可复用的UI组件 (如Spinner, Layout) ├── features/ # 按功能组织的Redux Slices和相关组件 │ ├── posts/ │ │ ├── PostsList.jsx │ │ ├── SinglePostPage.jsx │ │ └── postsSlice.js │ └── users/ │ ├── UserPage.jsx │ └── usersSlice.js ├── App.jsx # 应用主路由和布局 └── main.jsx # 应用入口
这种按功能(Feature)组织的目录结构,是大型Redux应用推荐的最佳实践,它让相关的文件(slice, 组件)都放在一起,更易于维护。
第三章:搭建Redux状态管理层
我们将使用RTK来管理文章和用户的状态,并处理API请求。
1. 创建postsSlice.js
我们将使用createAsyncThunk
来处理异步获取文章的逻辑。
src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';
const initialState = {
posts: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
// createAsyncThunk接收两个参数:
// 1. Action type的前缀字符串
// 2. 一个返回Promise的"payload creator"回调函数
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await axios.get(POSTS_URL);
return response.data;
});
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {},
// extraReducers让我们能够响应由createAsyncThunk或其他slice触发的action
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.posts = action.payload; // 将获取到的数据存入state
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export default postsSlice.reducer;
createAsyncThunk
会自动为我们分发.pending
, .fulfilled
, .rejected
这三种action,我们可以在extraReducers
中监听它们,并相应地更新我们的加载和错误状态。
2. 创建usersSlice.js
(同理,用于获取用户数据)
src/features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
const initialState = {
users: [],
status: 'idle',
};
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await axios.get(USERS_URL);
return response.data;
});
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.users = action.payload;
});
},
});
export default usersSlice.reducer;
3. 配置Store
src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import postsReducer from '../features/posts/postsSlice';
import usersReducer from '../features/users/usersSlice';
export const store = configureStore({
reducer: {
posts: postsReducer,
users: usersReducer,
},
});
在main.jsx
中用Provider
包裹应用,这部分和上一篇一样,不再赘述。
第四章:构建UI组件与页面
1. 文章列表页 PostsList.jsx
src/features/posts/PostsList.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './postsSlice';
import { Link } from 'react-router-dom';
const PostsList = () => {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.posts);
const postStatus = useSelector((state) => state.posts.status);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
// 只有在空闲时才去获取数据,避免重复请求
if (postStatus === 'idle') {
dispatch(fetchPosts());
}
}, [postStatus, dispatch]);
let content;
if (postStatus === 'loading') {
content = <p>"Loading..."</p>;
} else if (postStatus === 'succeeded') {
content = posts.map((post) => (
<article key={post.id} className="border p-4 my-2 rounded-lg">
<h3 className="text-xl font-bold">{post.title}</h3>
<p className="truncate">{post.body}</p>
<Link to={`/posts/${post.id}`} className="text-blue-500 hover:underline">
View Post
</Link>
</article>
));
} else if (postStatus === 'failed') {
content = <p>{error}</p>;
}
return (
<section>
<h2 className="text-2xl font-bold mb-4">Posts</h2>
{content}
</section>
);
};
export default PostsList;
这个组件完美地展示了如何从Redux store中读取状态,并根据加载状态来渲染不同的UI。同时,通过检查postStatus
,我们实现了一个简单的数据缓存策略。
2. 文章详情页 SinglePostPage.jsx
这个页面需要根据URL中的postId
来从store中找到对应的文章。
src/features/posts/SinglePostPage.jsx
import React from 'react';
import { useSelector } from 'react-redux';
import { useParams, Link } from 'react-router-dom';
const SinglePostPage = () => {
const { postId } = useParams();
// 从store中根据ID查找文章
const post = useSelector((state) =>
state.posts.posts.find((p) => p.id === Number(postId))
);
// 从store中查找文章的作者
const author = useSelector((state) =>
post ? state.users.users.find((u) => u.id === post.userId) : null
);
if (!post) {
return (
<section>
<h2>Post not found!</h2>
</section>
);
}
return (
<article className="p-4">
<h2 className="text-3xl font-bold">{post.title}</h2>
<p className="mt-4">{post.body}</p>
<p className="mt-4 text-gray-600">
By {author ? <Link to={`/users/${author.id}`} className="text-blue-500">{author.name}</Link> : 'Unknown author'}
</p>
</article>
);
};
export default SinglePostPage;
3. 用户详情页 UserPage.jsx
(与SinglePostPage
类似,此处省略具体代码)
第五章:组装应用 —— 路由与全局布局
最后,我们在App.jsx
中把所有东西组装起来。
src/App.jsx
import React, { useEffect } from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { fetchUsers } from './features/users/usersSlice';
import PostsList from './features/posts/PostsList';
import SinglePostPage from './features/posts/SinglePostPage';
import UserPage from './features/users/UserPage';
function App() {
const dispatch = useDispatch();
// 在应用启动时,就获取所有用户数据,供后续使用
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
<div className="container mx-auto p-4">
<header className="mb-8">
<nav className="flex justify-between items-center p-4 bg-gray-100 rounded-lg">
<h1 className="text-2xl font-bold">
<Link to="/">React Blog</Link>
</h1>
<div className="space-x-4">
<Link to="/posts" className="text-lg hover:text-blue-500">Posts</Link>
</div>
</nav>
</header>
<main>
<Routes>
<Route path="/" element={<h2>Welcome to React Blog!</h2>} />
<Route path="/posts" element={<PostsList />} />
<Route path="/posts/:postId" element={<SinglePostPage />} />
<Route path="/users/:userId" element={<UserPage />} />
<Route path="*" element={<h2>404 - Page Not Found</h2>} />
</Routes>
</main>
</div>
);
}
export default App;
在这个App
组件中,我们定义了应用的整体布局(Header, Main)和所有的路由规则。我们还在应用启动时就分发了fetchUsers
action,这样当用户浏览到需要作者信息的页面时,数据已经准备好了。
总结:你的第一艘“旗舰级”应用
恭喜你!你已经成功地指挥并建造了一艘功能完善、架构清晰的“旗舰级”React应用。这次实战演练,是对你过去所有学习成果的一次全面整合和升华。
让我们盘点一下我们在这艘“旗舰”上集成的所有先进技术:
- React Router: 实现了流畅的、客户端的、多页面的导航体验。
- Redux Toolkit: 作为我们强大的“中央数据仓库”,通过
createSlice
和createAsyncThunk
优雅地管理着应用的所有状态和异步逻辑。 - Axios: 担当了我们与后端API通信的可靠“信使”。
- 功能优先的目录结构: 让我们的代码库逻辑清晰,易于扩展和维护。
- Tailwind CSS: 快速地为我们的应用穿上了专业、一致的“外衣”。
- Hooks (
useSelector
,useDispatch
,useEffect
,useParams
): 如同瑞士军刀,灵活地将UI、状态和路由连接在一起。
这个项目已经非常接近一个真实的生产环境应用的雏形。你可以以此为基础,继续添加更多功能,比如文章创建/编辑、用户认证、评论系统等。
在专栏的最后一篇文章中,我们将进行一次全面的总结和展望,回顾我们的整个学习路径,并为你指出在精通React之后,下一步可以探索的更广阔的技术天地。
我是码力无边,我们终点站再见!
更多推荐
所有评论(0)