当前位置: 首页 > news >正文

实战演练(二):结合路由与状态管理,构建一个小型博客前台

实战演练(二):结合路由与状态管理,构建一个小型博客前台

作者:码力无边

各位React全栈探险家,欢迎来到《React奇妙之旅》的第十九站!我是你们的首席架构师码力无边。我们已经航行了很长的距离,从基础的JSX语法到高级的状态管理,从组件的样式美化到代码的质量保障。我们收集了满船的“宝藏”:React Router, Redux Toolkit, 自定义Hooks, CSS方案, 甚至自动化测试。

现在,是时候将所有这些宝藏融会贯通,建造一艘真正属于我们自己的、能够远航的“旗舰”了!

今天的任务,将是我们迄今为止最全面、最接近真实世界项目的一次挑战。我们将从零开始,综合运用之前学到的所有核心技能,构建一个功能完备的小型博客前台应用。这个应用将不仅仅是一个组件的堆砌,它将是一个拥有多页面导航、全局状态管理、真实API数据交互、漂亮UI以及高质量代码的完整SPA。

这篇文章将是你从“学习者”向“构建者”转变的毕业典礼。它将检验你是否真正理解并能灵活运用React生态的全貌。准备好迎接这次终极挑战,将你的知识锻造成真正的产品了吗?让我们开始这次激动人心的构建之旅!

第一章:项目蓝图与技术选型

在动工之前,让我们先画好蓝图。

核心功能

  1. 文章列表页 (/posts): 显示所有文章的标题列表。
  2. 文章详情页 (/posts/:postId): 显示单篇文章的完整内容,包括标题、正文和作者信息。
  3. 用户详情页 (/users/:userId): 显示单个用户的详细信息(姓名、邮箱等)。
  4. 全局加载指示器: 在任何API请求进行中时,显示一个全局的加载提示。
  5. 数据缓存: 避免对相同的数据进行重复的网络请求。

技术栈选型

  • 脚手架: Vite
  • 核心框架: React
  • 路由: React Router v6
  • 状态管理: Redux Toolkit (RTK)
  • 数据请求: Axios (封装在RTK的createAsyncThunk中)
  • 样式: Tailwind CSS (因为它能让我们快速构建出不错的UI)
  • API源: JSONPlaceholder (一个绝佳的免费Mock API)

第二章:项目初始化与目录结构规划

  1. 创建项目:

    npm create vite@latest my-react-blog -- --template react
    cd my-react-blog
    
  2. 安装依赖:

    npm install react-router-dom @reduxjs/toolkit react-redux axios
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p # 初始化Tailwind配置
    
  3. 配置Tailwind CSS:

    • 根据Tailwind CSS官方文档为Vite项目进行配置。这主要涉及修改tailwind.config.jscontent字段和在主CSS文件(如index.css)中引入Tailwind的指令。
  4. 规划目录结构:

    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: 作为我们强大的“中央数据仓库”,通过createSlicecreateAsyncThunk优雅地管理着应用的所有状态和异步逻辑。
  • Axios: 担当了我们与后端API通信的可靠“信使”。
  • 功能优先的目录结构: 让我们的代码库逻辑清晰,易于扩展和维护。
  • Tailwind CSS: 快速地为我们的应用穿上了专业、一致的“外衣”。
  • Hooks (useSelector, useDispatch, useEffect, useParams): 如同瑞士军刀,灵活地将UI、状态和路由连接在一起。

这个项目已经非常接近一个真实的生产环境应用的雏形。你可以以此为基础,继续添加更多功能,比如文章创建/编辑、用户认证、评论系统等。

在专栏的最后一篇文章中,我们将进行一次全面的总结和展望,回顾我们的整个学习路径,并为你指出在精通React之后,下一步可以探索的更广阔的技术天地。

我是码力无边,我们终点站再见!

http://www.xdnf.cn/news/1450819.html

相关文章:

  • Java基础知识点汇总(五)
  • 修订版!Uniapp从Vue3编译到安卓环境踩坑记录
  • 新手向:AI IDE+AI 辅助编程
  • 开源视频剪辑工具推荐
  • 经典资金安全案例分享:支付系统开发的血泪教训
  • Hadoop(七)
  • 数说故事 | 2025年运动相机数据报告,深挖主流品牌运营策略及行业趋势​
  • HarmonyOS路由导航方案演进:HMRouter基于Navigation封装,使用更方便
  • 【软考架构】嵌入式系统及软件
  • Shadcn UI – 开发者首选的高性能、高定制化 React 组件库
  • Flutter之riverpod状态管理详解
  • 第1章 Jenkins概述与架构
  • ⸢ 肆 ⸥ ⤳ 默认安全:安全建设方案 ➭ b.安全资产建设
  • HTTP性能优化
  • Rust 文件操作终极实战指南:从基础读写到进阶锁控,一文搞定所有 IO 场景
  • 设计模式3 创建模式之Singleton模式
  • 大数据工程师认证推荐项目:基于Spark+Django的学生创业分析可视化系统技术价值解析
  • 基于 EasyExcel + 线程池 解决 POI 导出时的内存溢出与超时问题
  • 如何简单理解状态机、流程图和时序图
  • Docker学习记录
  • 记一次 Nuxt 3 + pnpm Monorepo 中的依赖地狱:`@unhead/vue` 引发的致命错误
  • 封边机高级设置密码解锁指南:技术解析与安全操作建议
  • k8s基础(未完待续)
  • doubletrouble: 1靶场渗透
  • ubuntu-24.04.3-live-server连接不上xhell
  • 当数据库宕机时,PostgreSQL 高可用在背后做了什么?
  • 探索 PostgreSQL 和 MySQL 之间的主要差异和相似之处,找到满足您项目需求的最佳数据库解决方案。
  • jQuery的$.Ajax方法分析
  • 低代码高效搭建应用,轻松应对多场景需求
  • 低代码选型避坑指南:告别封闭与绑定,星图云开发者平台定义开放灵活新标准