实战演练(二):结合路由与状态管理,构建一个小型博客前台
实战演练(二):结合路由与状态管理,构建一个小型博客前台
作者:码力无边
各位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触发的actionextraReducers(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之后,下一步可以探索的更广阔的技术天地。
我是码力无边,我们终点站再见!