在现代前端开发中,一个完整的 React 项目不仅仅只是写几个组件、调几个接口,而是要从项目结构设计、状态管理、路由配置、接口封装、组件拆分等多个维度出发,构建一个可维护、可扩展、性能良好的大型应用。
本文将带你从零开始,一步步构建一个基于 React 全家桶 的 GitHub 仓库展示系统(Repos 项目) ,实现如下功能:
- 使用
react-router-dom实现动态路由/repos/:username和/repos/:id - 使用
axios封装统一的 API 接口 - 使用
useContext + useReducer实现全局状态管理 - 使用自定义 Hook 管理数据逻辑
- 组件拆分粒度合理,便于复用与维护
- 项目结构清晰,符合企业级开发规范
一、项目背景与目标
我们希望通过该项目:
- 学会构建一个完整的 React 应用
- 掌握 React 项目中路由、状态、接口、组件的协作方式
- 实践大型项目开发中常见的架构设计
- 为将来开发更复杂的项目打下坚实基础
项目功能简述:
- 输入 GitHub 用户名,展示该用户的公开仓库列表
- 支持通过用户名
/repos/:username或仓库 ID/repos/:id查看仓库详情 - 展示仓库信息(名称、描述、语言、星星数等)
- 错误处理(用户不存在、网络异常等)
- 使用懒加载提升首屏加载速度
二、技术栈(React 全家桶)
| 技术 | 说明 |
|---|---|
| React 18 | 主框架 |
| React Router DOM v6 | 路由管理 |
| Axios | 网络请求 |
| useContext / useReducer | 全局状态管理 |
| 自定义 Hooks | 逻辑复用 |
| Vite / Webpack | 构建工具 |
| Stylus / CSS Modules | 样式管理 |
| PropTypes / TypeScript(可选) | 类型检查 |
三、项目目录结构设计
合理的目录结构是项目可维护性的基础。我们采用如下结构:
src/
├── api/ // 所有接口统一管理
│ └── github.js
├── components/ // 通用组件
│ ├── RepoList.jsx
│ ├── RepoItem.jsx
│ └── Loading.jsx
├── hooks/ // 自定义 Hook
│ └── useRepos.js
├── context/ // 状态管理
│ ├── RepoContext.js
│ └── repoReducer.js
├── routes/ // 路由配置
│ └── AppRouter.jsx
├── pages/ // 页面组件
│ ├── Home.jsx
│ └── RepoDetail.jsx
├── App.jsx // 根组件
└── main.jsx // 入口文件
四、路由设计与懒加载
使用 react-router-dom 实现 SPA 路由,支持懒加载和动态参数。
1. 安装依赖
npm install react-router-dom
2. 路由配置(AppRouter.jsx)
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import React from 'react';
const Home = React.lazy(() => import('../pages/Home'));
const RepoDetail = React.lazy(() => import('../pages/RepoDetail'));
function AppRouter() {
return (
<Router>
<React.Suspense fallback="Loading...">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/repos/:username" element={<RepoDetail />} />
<Route path="/repos/:id" element={<RepoDetail />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</React.Suspense>
</Router>
);
}
export default AppRouter;
3. 获取动态参数(useParams)
在 RepoDetail.jsx 中使用 useParams 获取路由参数:
import { useParams } from 'react-router-dom';
function RepoDetail() {
const { username, id } = useParams();
if (username) {
// 根据 username 获取仓库列表
} else if (id) {
// 根据 id 获取仓库详情
}
return (
<div>Repo Detail</div>
);
}
⚠️ 注意:不要把
useParams写在useEffect里,它应该在组件函数体中直接调用。
五、接口封装与 API 管理
1. 使用 Axios 封装 GitHub 接口
// api/github.js
import axios from 'axios';
const githubApi = axios.create({
baseURL: 'https://api.github.com',
});
export async function getUserRepos(username) {
const res = await githubApi.get(`/users/${username}/repos`);
return res.data;
}
export async function getRepoById(id) {
const res = await githubApi.get(`/repositories/${id}`);
return res.data;
}
2. 在组件中使用 API
import { getUserRepos } from '../api/github';
function RepoDetail({ username }) {
const [repos, setRepos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getUserRepos(username)
.then(data => {
setRepos(data);
setLoading(false);
})
.catch(err => {
console.error(err);
setLoading(false);
});
}, [username]);
return (
<div>
{loading ? <Loading /> : <RepoList repos={repos} />}
</div>
);
}
六、状态管理:useContext + useReducer
1. 创建全局状态管理 Context
// context/RepoContext.js
import React, { createContext, useReducer } from 'react';
import repoReducer from './repoReducer';
const initialState = {
repos: [],
loading: false,
error: null,
};
const RepoContext = createContext();
function RepoProvider({ children }) {
const [state, dispatch] = useReducer(repoReducer, initialState);
return (
<RepoContext.Provider value={{ state, dispatch }}>
{children}
</RepoContext.Provider>
);
}
export { RepoContext, RepoProvider };
2. 定义 Reducer(repoReducer.js)
export default function repoReducer(state, action) {
switch (action.type) {
case 'FETCH_REPOS_REQUEST':
return { ...state, loading: true, error: null };
case 'FETCH_REPOS_SUCCESS':
return { ...state, loading: false, repos: action.payload };
case 'FETCH_REPOS_FAILURE':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
3. 在入口文件中引入 Provider
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { RepoProvider } from './context/RepoContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<RepoProvider>
<App />
</RepoProvider>
);
七、自定义 Hook:useRepos
将获取仓库数据的逻辑封装到自定义 Hook 中:
// hooks/useRepos.js
import { useContext } from 'react';
import { RepoContext } from '../context/RepoContext';
import { getUserRepos } from '../api/github';
export default function useRepos(username) {
const { dispatch } = useContext(RepoContext);
useEffect(() => {
if (!username) return;
dispatch({ type: 'FETCH_REPOS_REQUEST' });
getUserRepos(username)
.then(repos => {
dispatch({ type: 'FETCH_REPOS_SUCCESS', payload: repos });
})
.catch(err => {
dispatch({ type: 'FETCH_REPOS_FAILURE', payload: err.message });
});
}, [username, dispatch]);
return useContext(RepoContext);
}
八、组件拆分与开发模式
1. UI 组件(展示型组件)
// components/RepoList.jsx
function RepoList({ repos }) {
return (
<ul>
{repos.map(repo => (
<RepoItem key={repo.id} repo={repo} />
))}
</ul>
);
}
// components/RepoItem.jsx
function RepoItem({ repo }) {
return (
<li>
<h3>{repo.name}</h3>
<p>{repo.description}</p>
</li>
);
}
2. 容器组件(逻辑型组件)
// pages/RepoDetail.jsx
import useRepos from '../hooks/useRepos';
function RepoDetail() {
const { username } = useParams();
const { state } = useRepos(username);
return (
<div>
{state.loading && <Loading />}
{state.error && <div>{state.error}</div>}
{!state.loading && !state.error && <RepoList repos={state.repos} />}
</div>
);
}
九、性能优化建议
| 优化点 | 实现方式 |
|---|---|
| 懒加载路由 | 使用 React.lazy + Suspense |
| 避免重复渲染 | 使用 React.memo 包裹子组件 |
| 缓存计算值 | 使用 useMemo |
| 缓存回调函数 | 使用 useCallback |
| 接口请求防抖 | 在输入搜索时使用防抖 |
| 接口缓存 | 使用 localStorage 或 SWR/React Query |
十、安全与健壮性建议
- 永远不要相信用户的输入:对
username和id做合法性校验 - 错误边界:使用
ErrorBoundary组件捕获渲染错误 - 接口错误处理:统一拦截 404、500 等错误,提升用户体验
- 登录验证(后续扩展):添加登录页,使用路由守卫保护
/repos页面
十一、总结:React 项目开发的核心思想
React 项目的本质,是组件 + 状态 + 数据 + 路由 的协同。
| 模块 | 关键技术 | 作用 |
|---|---|---|
| 路由 | react-router-dom | 实现页面跳转与懒加载 |
| 接口 | axios + api 目录 | 统一管理网络请求 |
| 状态 | useContext + useReducer | 实现全局状态管理 |
| 组件 | 拆分粒度 + 自定义 Hook | 提升复用性与可维护性 |
| 架构 | 清晰目录结构 + 模块化 | 便于团队协作与扩展 |
十二、后续扩展方向
- ✅ 添加登录页 + 路由守卫
- ✅ 支持仓库收藏功能(本地存储)
- ✅ 使用
React Query替代自定义状态管理 - ✅ 使用 TypeScript 提升类型安全性
- ✅ 部署到 GitHub Pages / Vercel / Netlify