原文地址:https://javascript.plainenglish.io/stop-using-react-router-30-lines-does-the-same-thing-174999058825
原文作者: Ignatius Sani
你知道吗?React Router v6 在 gzip 压缩后的大小仍有大约 18KB。这个数字看似不大,但对于许多功能简单的单页应用来说,其代码量甚至超过了应用本身的业务逻辑。
想象一下你的应用:可能只有五个路由,或者野心大一点,有十个。一个着陆页、一个关于页面、一个联系表单,或许再加一个小型仪表盘。但很多时候,我们在一开始就会不假思索地引入完整的大型路由框架,却从未停下来问一个简单的问题:我们真的需要它吗?
上个月,我从三个生产环境的 React 应用中移除了 React Router。这些并非玩具项目,而是拥有真实用户的真实应用。结果呢?打包体积减少了 18KB。用户行为没有任何变化,没有引入新的 Bug,代码反而变得更容易理解。
这并非在贬低 React Router。它是一个成熟、维护良好的库,确实解决了复杂问题。当你的应用确实需要它时,我仍然推荐使用。
但本文的观点是,大多数 React 应用其实并不需要如此重量级的路由方案。这不是一篇“永远不要用 React Router”的檄文,而是帮助你理解:什么时候你并不需要它,以及当你选择“简单优先”时,一个可行的替代方案是什么样子。
React Router 到底提供了什么?
在替换任何工具之前,先诚实地看看它究竟解决了哪些问题。从本质上讲,React Router 提供了几个核心能力:
- 将 URL 映射到 React 组件。
- 保持 UI 与浏览器历史记录同步。
- 在不刷新页面的情况下进行导航。
- 访问查询参数 (query parameters)。
- 提供 404 页面作为后备。
这些都是合理的需求,路由也确实是一个需要解决的问题。但这些问题并非神秘莫测或尚未被攻克。浏览器本身就提供了强大的 History API,而 React 本身也已经具备了 state 管理和 context 机制。当你把这两者结合起来看,React Router 所做的很多事情,本质上就是一层“胶水代码”。
这并不意味着 React Router 糟糕,只是说明:它并非总是必要的。
路由本质上就是共享 state
如果我们把路由问题拆解到最基础的层面,其实就一句话:当前的 URL 决定了应该渲染哪个组件。
仅此而已。而在 React 的世界里,这本质上就是一个共享 state 的问题。我们早就知道如何处理共享 state:使用 context。
一旦你从这个角度思考路由,问题就变小了。我们不再需要引入一个完整的框架,而是可以利用 React 的状态管理和浏览器的 History API,自己构建一个极简的路由器。
一个仅30行的最小化 Router 实现
下面就是一个完整、最小化实现的路由器。它没有任何外部依赖,没有多余的抽象,只有 React 和浏览器在做它们本来就擅长的事情。
import { useState, useEffect, useContext, createContext } from 'react';
const RouterContext = createContext(null);
function RouterProvider({ children }) {
const [currentPath, setCurrentPath] = useState(
typeof window !== 'undefined'
? window.location.pathname
: '/'
);
useEffect(() => {
const handlePopState = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () =>
window.removeEventListener('popstate', handlePopState);
}, []);
const navigate = (path) => {
window.history.pushState({}, '', path);
setCurrentPath(path);
};
return (
<RouterContext.Provider value={{ currentPath, navigate }}>
{children}
</RouterContext.Provider>
);
}
function useRouter() {
const context = useContext(RouterContext);
if (!context) {
throw new Error(
'useRouter must be used inside RouterProvider'
);
}
return context;
}
function Router({ routes }) {
const { currentPath } = useRouter();
return routes[currentPath] || routes['/404'] || null;
}
function Link({ to, children, ...props }) {
const { navigate } = useRouter();
const handleClick = (e) => {
e.preventDefault();
navigate(to);
};
return (
<a href={to} onClick={handleClick} {...props}>
{children}
</a>
);
}
export { RouterProvider, Router, Link };
这就是一个完整路由器的核心。 它没有复杂的配置文件,没有层层嵌套的抽象,也没有大版本升级带来的破坏性变更。
在真实应用中的使用方式
在实际项目中,你可以这样使用上面实现的路由器:
import { RouterProvider, Router, Link } from './router';
function Home() {
return (
<>
<h1>Home</h1>
<Link to="/about">About</Link>
</>
);
}
function About() {
return (
<>
<h1>About</h1>
<Link to="/">Home</Link>
</>
);
}
function NotFound() {
return <h1>404 — Page Not Found</h1>;
}
const routes = {
'/': <Home />,
'/about': <About />,
'/404': <NotFound />
};
function App() {
return (
<RouterProvider>
<Router routes={routes} />
</RouterProvider>
);
}
这样,你的应用就具备了基本的前端路由功能:
- 后退按钮可用。
- 前进按钮可用。
- 链接点击即时响应,没有整页刷新。
从用户的视角来看,这完全是一个标准的单页应用 (SPA) —— 因为它确实就是。
如何处理 Query Parameters?
一个常见的疑问是:查询参数 (query parameters) 怎么办?
React Router 为此提供了专门的辅助工具,但查询字符串本身也是浏览器状态的一部分。我们可以用一个非常简洁的自定义 Hook 来处理它:
function useQueryParams() {
const [params, setParams] = useState(
new URLSearchParams(window.location.search)
);
useEffect(() => {
const handlePopState = () => {
setParams(
new URLSearchParams(window.location.search)
);
};
window.addEventListener('popstate', handlePopState);
return () =>
window.removeEventListener('popstate', handlePopState);
}, []);
return params;
}
使用方式非常直观:
function SearchPage() {
const params = useQueryParams();
const query = params.get('q');
return <h1>Searching for: {query}</h1>;
}
现在,访问 /search?q=javascript 这样的 URL 将完全符合预期,组件可以正确地获取并显示查询参数。
权衡取舍
当然,这种极简方案并非没有代价。选择之前,你需要清楚地了解自己放弃了什么。
你会失去什么
- 嵌套路由:这个路由器只支持扁平的路由结构。
- 动态路由参数:像
/users/:id 这样的路径模式,需要你额外编写路径匹配逻辑。
- 自动滚动恢复:React Router 已经帮你处理好了页面切换时的滚动位置。
- 代码分割的便利性:你仍然可以使用
React.lazy(),但 React Router 与之集成更顺手。
- 久经考验的成熟方案:React Router 被数百万应用使用和测试过,而这个方案没有。
如果你的应用确实需要上述能力,那么 React Router 绝对是正确的、值得使用的工具。
本文的目的在于提供一种思考方式:对于许多中低复杂度的项目,路由的本质可以如此简单。理解这一点,能帮助你在技术选型时做出更明智的决定,避免不必要的依赖和复杂度。希望这篇文章能为你构建更轻量、更高效的 Web 应用带来启发。欢迎在云栈社区与更多开发者交流前端工程化实践。