兄弟,还在为秋招发愁?今天咱们来聊聊 React Router 这个”老司机”。从 v5 的稳重大叔到 v6 的激进青年,再到 v7 的王者归来(直接吞并了 Remix),这个故事比宫斗剧还精彩。系好安全带,咱们发车了!
序章:为什么前端需要路由?
还记得上古时代的网页吗?每点一个链接,整个页面”唰”地白屏,然后慢慢加载。用户体验?不存在的。
然后 SPA(单页应用)横空出世,页面不刷新了,但新问题来了:
- 浏览器的前进后退按钮废了
- 刷新页面就 404 了
- 分享链接?对不起,都是同一个 URL
这时候,前端路由站出来说:”这活儿我来!”
第一章:React Router v5 —— 稳重的老大哥
v5 的基本使用
React Router v5 就像个稳重的老司机,虽然有点啰嗦,但靠谱:
import { BrowserRouter as Router, Route, Switch, Link, useHistory, useParams } from 'react-router-dom';
function App() { return ( <Router> <nav> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Link to="/user/123">用户详情</Link> </nav> {/* Switch 确保只渲染第一个匹配的路由 */} <Switch> <Route exact path="/"> <Home /> </Route> <Route path="/about"> <About /> </Route> <Route path="/user/:id"> <UserDetail /> </Route> <Route path="*"> <NotFound /> </Route> </Switch> </Router> ); }
function UserDetail() { const { id } = useParams(); const history = useHistory(); const handleGoBack = () => { history.push('/'); }; return ( <div> <h1>用户 {id} 的详情页</h1> <button onClick={handleGoBack}>返回首页</button> </div> ); }
|
v5 的高级技巧
const routes = [ { path: '/', component: Home, exact: true }, { path: '/about', component: About }, { path: '/user/:id', component: UserDetail } ];
function App() { return ( <Router> <Switch> {routes.map(route => ( <Route key={route.path} exact={route.exact} path={route.path} component={route.component} /> ))} </Switch> </Router> ); }
function PrivateRoute({ children, ...rest }) { const isAuthenticated = useAuth(); return ( <Route {...rest}> {isAuthenticated ? children : <Redirect to="/login" />} </Route> ); }
function Users() { const { path, url } = useRouteMatch(); return ( <div> <Link to={`${url}/profile`}>个人资料</Link> <Link to={`${url}/settings`}>设置</Link> <Switch> <Route exact path={path}> <h3>请选择一个选项</h3> </Route> <Route path={`${path}/profile`}> <Profile /> </Route> <Route path={`${path}/settings`}> <Settings /> </Route> </Switch> </div> ); }
|
第二章:React Router v6 —— 激进的革命者
v6 来了,带着破坏性更新,社区炸锅了:”你们是要搞事情吗?”
v6 的巨变
import { BrowserRouter, Routes, Route, Link, useNavigate, useParams, Outlet } from 'react-router-dom';
function App() { return ( <BrowserRouter> <Routes> {/* 不再需要 exact,默认就是精确匹配 */} <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> {/* 嵌套路由的新写法,超级优雅! */} <Route path="/users" element={<Users />}> <Route index element={<UserList />} /> <Route path=":id" element={<UserDetail />} /> <Route path="settings" element={<Settings />} /> </Route> {/* 404 页面的新写法 */} <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }
function Users() { return ( <div> <h1>用户中心</h1> <nav> <Link to="">用户列表</Link> <Link to="settings">设置</Link> </nav> {/* Outlet 就像 Vue 的 router-view */} <Outlet /> </div> ); }
function SomeComponent() { const navigate = useNavigate(); const handleClick = () => { navigate('/about'); navigate(-1); navigate('/user/123', { replace: true }); }; return <button onClick={handleClick}>走你!</button>; }
|
v6 的杀手锏功能
function UserProfile() { return ( <div> {/* 相对于当前路由 */} <Link to="..">返回上级</Link> <Link to="../settings">设置</Link> <Link to="edit">编辑</Link> </div> ); }
const routes = [ { path: '/', element: <Layout />, children: [ { index: true, element: <Home /> }, { path: 'about', element: <About /> }, { path: 'users', element: <Users />, children: [ { index: true, element: <UserList /> }, { path: ':id', element: <UserDetail /> } ] } ] } ];
function App() { const element = useRoutes(routes); return element; }
const LazyAbout = React.lazy(() => import('./About'));
<Route path="/about" element={ <React.Suspense fallback={<Loading />}> <LazyAbout /> </React.Suspense> } />
|
第三章:底层原理 —— 路由的魔法是怎么实现的?
来,让我们扒开 React Router 的外衣,看看它的真面目!
History API:一切的基础
window.history.pushState(state, title, url); window.history.replaceState(state, title, url); window.history.back(); window.history.forward();
window.addEventListener('popstate', (event) => { console.log('路由变化了!', location.pathname); });
|
简化版 React Router 实现
让我们自己造个轮子,你就明白了:
const RouterContext = React.createContext();
function BrowserRouter({ children }) { const [location, setLocation] = useState(window.location.pathname); useEffect(() => { const handlePopState = () => { setLocation(window.location.pathname); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); const navigate = (to, options = {}) => { if (options.replace) { window.history.replaceState(null, '', to); } else { window.history.pushState(null, '', to); } setLocation(to); }; return ( <RouterContext.Provider value={{ location, navigate }}> {children} </RouterContext.Provider> ); }
function Route({ path, element }) { const { location } = useContext(RouterContext); const match = matchPath(path, location); return match ? element : null; }
function Link({ to, children }) { const { navigate } = useContext(RouterContext); const handleClick = (e) => { e.preventDefault(); navigate(to); }; return <a href={to} onClick={handleClick}>{children}</a>; }
function matchPath(pattern, pathname) { const regexPattern = pattern .replace(/:[^/]+/g, '([^/]+)') .replace(/\*/g, '.*'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(pathname); }
|
Hash Router vs Browser Router
class HashRouter { constructor() { window.addEventListener('hashchange', this.handleHashChange); } handleHashChange = () => { const path = window.location.hash.slice(1); this.updateView(path); } push(path) { window.location.hash = path; } }
class BrowserRouter { constructor() { window.addEventListener('popstate', this.handlePopState); } handlePopState = () => { this.updateView(window.location.pathname); } push(path) { window.history.pushState(null, '', path); this.updateView(path); } }
|
面试加分点:
- Hash Router 兼容性好,但 URL 有个丑陋的 #
- Browser Router 需要服务器配置支持(所有路由都返回 index.html)
- Hash Router 不会发送到服务器,Browser Router 会
第四章:React Router v7 —— 王者归来(融合 Remix)
2024 年底,React Router v7 震撼发布,直接把 Remix 给”吞并”了!
v7 的重磅特性:SSR 支持
export async function loader({ params }) { const user = await fetchUser(params.id); return { user }; }
export default function UserProfile() { const { user } = useLoaderData(); return <h1>欢迎,{user.name}!</h1>; }
export async function action({ request }) { const formData = await request.formData(); const email = formData.get('email'); await updateEmail(email); return redirect('/profile'); }
export default function Settings() { return ( <Form method="post"> <input name="email" type="email" /> <button type="submit">更新邮箱</button> </Form> ); }
export function ErrorBoundary() { const error = useRouteError(); return ( <div> <h1>哎呀,出错了!</h1> <p>{error.message}</p> </div> ); }
|
v7 的完整 SSR 示例
import { useLoaderData } from 'react-router-dom';
export async function loader({ params }) { const post = await db.post.findUnique({ where: { slug: params.slug } }); if (!post) { throw new Response('Not Found', { status: 404 }); } return { post }; }
export function meta({ data }) { return [ { title: data.post.title }, { name: 'description', content: data.post.excerpt }, { property: 'og:title', content: data.post.title } ]; }
export default function BlogPost() { const { post } = useLoaderData(); return ( <article> <h1>{post.title}</h1> <time>{post.publishedAt}</time> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ); }
export async function loader() { return defer({ critical: await getCriticalData(), comments: getComments() }); }
export default function Post() { const { critical, comments } = useLoaderData(); return ( <div> <h1>{critical.title}</h1> <Suspense fallback={<LoadingComments />}> <Await resolve={comments}> {(comments) => <Comments data={comments} />} </Await> </Suspense> </div> ); }
|
第五章:版本迁移指南 —— 从 v5 到 v6/v7
主要变化对照表
| 特性 |
v5 |
v6/v7 |
| 路由容器 |
<Switch> |
<Routes> |
| 路由组件 |
component={Component} |
element={<Component />} |
| 嵌套路由 |
手动拼接路径 |
自动处理,使用 <Outlet> |
| 导航 Hook |
useHistory |
useNavigate |
| 路由匹配 |
需要 exact |
默认精确匹配 |
| 相对链接 |
不支持 |
完全支持 |
迁移示例
function OldApp() { const history = useHistory(); return ( <Switch> <Route exact path="/" component={Home} /> <Route path="/users/:id" component={UserDetail} /> </Switch> ); }
function NewApp() { const navigate = useNavigate(); return ( <Routes> <Route path="/" element={<Home />} /> <Route path="/users/:id" element={<UserDetail />} /> </Routes> ); }
function Users() { const { path, url } = useRouteMatch(); return ( <Switch> <Route exact path={path} component={UserList} /> <Route path={`${path}/:id`} component={UserDetail} /> </Switch> ); }
<Route path="/users" element={<Users />}> <Route index element={<UserList />} /> <Route path=":id" element={<UserDetail />} /> </Route>
|
第六章:实战技巧与性能优化
1. 路由懒加载
const LazyDashboard = lazy(() => import( './Dashboard') );
function App() { return ( <Routes> <Route path="/dashboard/*" element={ <Suspense fallback={<Loading />}> <LazyDashboard /> </Suspense> } /> </Routes> ); }
|
2. 路由预加载
function PreloadableLink({ to, children }) { const handleMouseEnter = () => { import(`./pages${to}`); }; return ( <Link to={to} onMouseEnter={handleMouseEnter}> {children} </Link> ); }
|
3. 路由过渡动画
import { CSSTransition, TransitionGroup } from 'react-transition-group';
function AnimatedRoutes() { const location = useLocation(); return ( <TransitionGroup> <CSSTransition key={location.pathname} timeout={300} classNames="fade" > <Routes location={location}> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </CSSTransition> </TransitionGroup> ); }
.fade-enter { opacity: 0; } .fade-enter-active { opacity: 1; transition: opacity 300ms; } .fade-exit { opacity: 1; } .fade-exit-active { opacity: 0; transition: opacity 300ms; }
|
4. 路由级别的状态管理
function ProductList() { const navigate = useNavigate(); const handleEdit = (product) => { navigate('/edit', { state: { product, from: '/products' } }); }; }
function EditProduct() { const location = useLocation(); const navigate = useNavigate(); const product = location.state?.product; const handleSave = async (data) => { await saveProduct(data); navigate(location.state?.from || '/'); }; }
|
第七章:面试必考题
面试官:React Router 的原理是什么?
标准答案: React Router 基于浏览器的 History API,通过监听 URL 变化来渲染对应的组件。核心原理:
- 使用 Context 在组件树中传递路由状态
- Route 组件根据当前 URL 匹配 path 来决定是否渲染
- Link 组件阻止默认行为,调用 history.pushState 更新 URL
- 监听 popstate 事件处理浏览器前进后退
面试官:如何实现路由守卫?
function RequireAuth({ children }) { const auth = useAuth(); const location = useLocation(); if (!auth.user) { return <Navigate to="/login" state={{ from: location }} replace />; } return children; }
<Route path="/protected" element={ <RequireAuth> <ProtectedPage /> </RequireAuth> } />
|
面试官:Hash Router 和 Browser Router 的区别?
加分回答:
- Hash Router:URL 带 #,不需要服务器配置,不支持 SSR
- Browser Router:更美观的 URL,需要服务器配置,支持 SSR
- Memory Router:URL 不变化,用于 React Native 或测试
- Static Router:用于 SSR,不会改变 URL
面试官:React Router v6 为什么要做 Breaking Changes?
高情商回答: v6 的改动虽然很大,但带来了:
- 更小的包体积(减少约 50%)
- 更好的 TypeScript 支持
- 更直观的嵌套路由
- 相对路径支持
- 为 SSR 做准备(v7 实现了)
结语:路由的未来
从 v5 的稳重,到 v6 的激进,再到 v7 的全栈化,React Router 的进化史就是前端发展的缩影。记住:
- v5 还在广泛使用,很多老项目没有升级
- v6 是过渡,新项目建议直接上
- v7 是未来,SSR 成为标配
最后给秋招的你一些建议:
- 面试时能讲清楚版本差异会很加分
- 理解原理比记 API 重要 100 倍
- 如果面试官问到 v7,说明这是家技术很新的公司
- 准备一个路由相关的项目亮点,比如”我用路由懒加载优化了 70% 的首屏加载时间”
记住:技术是在不断进化的,保持学习的心态比掌握某个版本的 API 更重要。今天的 v7,可能就是明天的 “legacy code”。
P.S. 如果这篇文章帮到了你,记得收藏起来。毕竟,面试前临时抱佛脚的时候,你会感谢现在认真看完的自己的!
P.P.S. React Router 团队:求求你们,别再搞破坏性更新了,我们学不动了… 😭