本文通过一个GitHub仓库列表项目,详细介绍如何用React全家桶开发一个单页应用(SPA)。项目使用React Router v6实现路由懒加载,Context API + useReducer管理全局状态,并封装自定义Hook复用逻辑。
项目概述
我们要实现的功能如下:
- 首页(Home)
- 用户仓库列表页(RepoList)
- 仓库详情页(RepoDetail)
- 404页面(NotFound)
项目目录结构如下:
src
├── api // 接口封装
│ └── repos.js
├── components // 组件
│ └── Loading.jsx
├── context // 全局状态
│ └── GlobalContext.jsx
├── hooks // 自定义Hook
│ └── useRepos.js
├── pages // 页面组件
│ ├── Home
│ │ └── index.jsx
│ ├── RepoList
│ │ └── index.jsx
│ ├── RepoDetail
│ │ └── index.jsx
│ └── NotFound
│ └── index.jsx
├── reducers // 状态reducer
│ └── repoReducer.js
├── App.jsx // 根组件
└── main.jsx // 入口文件
技术亮点
1. 路由懒加载
使用React.lazy和Suspense实现路由懒加载,减少首屏资源加载体积:
// App.jsx
import { Suspense, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import Loading from './components/Loading'
const RepoList = lazy(() => import('./pages/RepoList'))
const RepoDetail = lazy(() => import('./pages/RepoDetail'))
const Home = lazy(() => import('./pages/Home'))
const NotFound = lazy(() => import('./pages/NotFound'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id/repos" element={<RepoList />} />
<Route path="/users/:id/repos/:repoId" element={<RepoDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
)
}
2. 全局状态管理(Context + useReducer)
使用Context和useReducer管理仓库列表相关的全局状态(包括加载状态、错误信息等):
Reducer设计:
// reducers/repoReducer.js
export const repoReducer = (state, action) => {
switch(action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, repos: action.payload, error: null }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload }
default:
return state
}
}
创建Context:
// context/GlobalContext.jsx
import { useReducer, createContext } from 'react'
import { repoReducer } from '@/reducers/repoReducer'
export const GlobalContext = createContext()
const initialState = {
repos: [],
loading: false,
error: null
}
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(repoReducer, initialState)
return (
<GlobalContext.Provider value={{ state, dispatch }}>
{children}
</GlobalContext.Provider>
)
}
在入口文件中包裹应用:
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { GlobalProvider } from '@/context/GlobalContext'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<GlobalProvider>
<App />
</GlobalProvider>
</BrowserRouter>
</React.StrictMode>
)
3. 自定义Hook(useRepos)
将数据获取逻辑封装成自定义Hook,简化组件代码:
// hooks/useRepos.js
import { useEffect, useContext } from 'react'
import { GlobalContext } from '@/context/GlobalContext'
import { getRepos } from '@/api/repos'
export const useRepos = (id) => {
const { state, dispatch } = useContext(GlobalContext)
useEffect(() => {
dispatch({ type: 'FETCH_START' })
const fetchRepos = async () => {
try {
const res = await getRepos(id)
dispatch({ type: 'FETCH_SUCCESS', payload: res.data })
} catch (err) {
dispatch({ type: 'FETCH_ERROR', payload: err.message })
}
}
fetchRepos()
}, [id, dispatch])
return state
}
4. API请求封装
将GitHub API请求封装成独立模块:
// api/repos.js
import axios from 'axios'
const BASE_URL = 'https://api.github.com/'
export const getRepos = (username) => {
return axios.get(`${BASE_URL}users/${username}/repos`)
}
export const getRepoDetail = async (username, repoName) => {
return await axios.get(`${BASE_URL}repos/${username}/${repoName}`)
}
5. 仓库列表页(RepoList)实现
在RepoList组件中使用自定义Hook获取数据:
// pages/RepoList/index.jsx
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useEffect } from 'react'
import { useRepos } from '@/hooks/useRepos'
const RepoList = () => {
const { id } = useParams()
const navigate = useNavigate()
const { repos, loading, error } = useRepos(id)
useEffect(() => {
if (!id?.trim()) {
navigate('/')
}
}, [id, navigate])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<>
<h2>Repositories for {id}</h2>
{repos.map(repo => (
<Link key={repo.id} to={`/users/${id}/repos/${repo.name}`}>
{repo.name}
</Link>
))}
</>
)
}
export default RepoList
性能优化点
- 路由懒加载:减少首屏加载时间
- 状态管理精细化:避免不必要的重渲染
- API请求封装:统一错误处理和基础URL
- 用户输入校验:在RepoList组件中检查id是否有效
踩坑记录
1. 路由参数获取
useParams必须在Router的上下文中使用,因此确保组件在路由匹配的组件树中。
2. 依赖数组问题
在useEffect中使用dispatch时,注意将其加入依赖数组(ESLint会提示)。由于dispatch是稳定的,不会引起重复执行。
3. 用户输入校验
在RepoList组件中,我们检查id是否为空,如果为空则重定向到首页。这是必要的,因为GitHub API不允许空用户名。
总结
通过这个项目,实践了React Router v6的路由配置和懒加载、使用Context+useReducer管理全局状态、封装自定义Hook复用逻辑、以及API请求的封装。这些技术点可以帮助我们构建一个结构清晰、性能良好的React应用。