昨天看到 React Router 发了 v8.3.1,想着项目还停在 v7 的某个中间版本,趁周末没排需求,不如先升了试试。 changelog 写得很乐观:"all of them are changes you can make in v7",意思是所有破坏性变更都能提前通过 future flags 适配。我当时觉得挺合理的------我们项目 v7 上了快半年,该有的 flag 应该都开了。 结果 pnpm i react-router@latest 装完一跑,直接炸了三个地方。 升级本身不难,但官方文档那句"boring release"有点误导人。对于一个 npm 周下载量 5000 万+ 的包来说,"无聊"这个词容易让人放松警惕。
react-router-dom 没了,换包名
第一个报错就是它。react-router-dom 在 v8 里彻底删了。 项目里所有 from "react-router-dom" 的导入全部标红。好在改动是纯机械性的:
javascript
// v7
import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
// v8
import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router";
如果是 DOM 特有的 API(比如 <ScrollRestoration />),需要从 react-router/dom 导入:
javascript
import { ScrollRestoration } from "react-router/dom";
这一步我写了个脚本全局替换,大概改了 40 多个文件,花了不到十分钟。 但这里有个坑。我们有个第三方 UI 库内部还在用 react-router-dom,直接报 Module not found。查了一圈才发现它的 peerDependencies 没更新。最后只能手动在 package.json 里加了个 alias 把它重定向到 react-router,算是个临时方案。
json
// vite.config.ts 里加 resolve alias
{
"resolve": {
"alias": {
"react-router-dom": "react-router"
}
}
}
如果你们项目也依赖了其他还在用 react-router-dom 的包,记得提前检查一下。
中间件------这才是真正的硬骨头
包名替换虽然报错,但至少报错信息清晰,改完就行。中间件这个问题是那种"项目能跑,但行为不对"的类型,排查起来最费时间。 v8 里 v8_middleware 这个 future flag 被移除了,中间件变成了默认行为。我们项目在 v7 的时候没开这个 flag,所以从来没有配置过中间件。 升级后第一次访问受保护页面,直接跳到了登录页------不是我期望的行为。
排了半天才搞清楚。v8 默认启用了中间件管道,而我们的鉴权逻辑之前写在 loader 里。loader 在中间件管道之前执行了一次未鉴权的路由加载,中间件又做了一次鉴权判断,两个逻辑打架了。loader 拿到的是空 token,中间件拦截后又 redirect,结果就是页面先闪一下再跳走。 解决方案是把鉴权从 loader 里拆出来,放到中间件层:
dart
// app/middleware/auth.ts
import type { Middleware } from "react-router";
export const authMiddleware: Middleware = async ({ request, context }) => {
const url = new URL(request.url);
// 白名单路由直接放行
const publicPaths = ["/login", "/register", "/public"];
if (publicPaths.some(p => url.pathname.startsWith(p))) {
return;
}
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
throw new Response(null, {
status: 302,
headers: { Location: "/login" },
});
}
// 验证 token 并存入 context,后续 loader 可以直接用
try {
const user = await verifyToken(token);
context.set("user", user);
} catch {
throw new Response(null, {
status: 302,
headers: { Location: "/login" },
});
}
};
然后在路由配置里注册:
javascript
// app/routes.ts
import { authMiddleware } from "./middleware/auth";
export const routes = [
{
path: "/",
middleware: [authMiddleware],
children: [
{ index: true, file: "./routes/dashboard.tsx" },
{ path: "settings", file: "./routes/settings.tsx" },
// ...
],
},
{ path: "/login", file: "./routes/login.tsx" },
];
loader 里就干净了,直接从 context 拿用户信息:
javascript
// routes/dashboard.tsx
export async function loader({ context }: LoaderFunctionArgs) {
const user = context.get("user");
return { userName: user.name, role: user.role };
}
改完之后我松了口气,在本地多点了几个页面,都没问题。心想这升级也没网上说的那么恐怖嘛。 然后部署到测试环境,CI 挂了。
Node 版本问题:CI 还停在 20
v8 要求 Node 22.22.0+,React 19.2.7+,Vite 7+。 我们的 CI 跑的是 Node 20 LTS。本地开发环境倒是已经切到 22 了,但 CI 配置文件写死了版本。 这个改动本身不大,就是改一下 GitHub Actions 的 workflow:
sql
# .github/workflows/ci.yml
- uses: actions/setup-node@v4
with:
node-version: "22.22" # 从 "20" 改成 "22.22"
cache: "pnpm"
改完之后还有第二个问题------Docker 基础镜像。我们的测试容器用的是 node:20-slim,也得换。换成 node:22-slim 之后,跑了一轮测试全过了。 不过有个细节值得注意。Node 22 里有个 --experimental-webstorage 默认行为变了,我们有一个依赖 localStorage 的工具库在测试环境里直接报错。后来加了 NODE_OPTIONS=--no-experimental-webstorage 才绕过去。如果你们的测试用例里有 mock localStorage 或 sessionStorage 的逻辑,升 Node 22 后记得跑一遍看看。 到这里其实技术上的问题已经解决了,但还有个更隐蔽的坑。 我们有个预渲染的静态页面,用了 v8_passThroughRequests flag。v8 里这个 flag 被移除并变成了默认行为。理论上应该无缝衔接,但实际跑预渲染时,部分页面的 loader 数据没有被正确序列化到 HTML 里。 查了 v8.3.0 的 changelog 才发现这是个已知问题,在 v8.3.1(就是昨天刚发的版本)里修了。所以如果你也碰到预渲染相关的问题,确保升级到 v8.3.1+。
ESM-only:我们没踩坑,但看到不少人踩了
我们项目全用 Vite,两年前就全面 ESM 了,所以这步基本没遇到问题。 但 v8 的 ESM-only 对老项目杀伤力很大。tsconfig 的 target 和 lib 强制 ES2022,还在用 require() 或者 jest.config.js 写 CommonJS 语法的项目,会直接跑不起来。掘金社区好几个人吐槽这步卡了一整天,特别是 webpack 4 + Jest 27 的老组合。 如果你属于这种情况,建议先把构建工具链升级到位------Webpack 5+ 或 Vite 7+,Jest 29+ 或换 Vitest------再动 React Router。这个前置工作量和升 v8 本身差不多,但做完之后后续升级就顺了。
还有一些零散的坑
splitRouteModules 在 v8 里变成了顶层配置且默认开启。我们项目路由模块本来就很小,没感受到明显变化。但如果你发现升级后构建产物变大了或者代码分割行为和之前不一样,查一下这个配置。 有个同事的项目遇到了 data 参数废弃的问题。v8 把 loader 的 data 参数改成了 loaderData,meta API 里也得跟着改。他的项目里有个自定义的 useRouteData hook 直接引用了旧的 data 字段,全局搜了十几处才改完。 react-router/dom 里有些 API 的导入路径也变了。如果项目里用了 <ScrollRestoration> 或者 <Form>,确保是从 react-router/dom 而不是 react-router 导入。这个改动不大,但 IDE 不会自动提示,得自己一个个检查。
另外 v6 和 Remix v2 正式 EOL 了,不再有安全更新。如果项目还在这两个版本上,这次升级就不是"想不想升"的问题,是"必须升"。React Router 官方明确说了,后续安全补丁只会打到 v7 和 v8 上。 还有个事值得一提:部分开发者已经开始迁移到 TanStack Router 了。v8 发布后 Reddit 上有人发帖吐槽 breaking changes,评论区不少人说正好趁机换。TanStack Router 的端到端类型安全和内置的 stale-while-revalidate 缓存确实不错,但如果你项目已经深度使用 React Router 的 loader/action 模式,换路由库的成本比升版本高太多了。
花了多久
换包名 + 修导入路径:半小时。中间件改造 + 鉴权逻辑迁移:大半天。CI Docker 镜像 + Node 版本:1 小时。预渲染问题排查(发现是 v8.3.1 修的):半天。回归测试 + 手动点页面:2 小时。 两天,比我预期的半天长得多。
回头看官方那句"it's not a major version if nothing broke",说得确实没错。但对一个有真实业务逻辑的项目来说,"没 break"和"改起来很顺滑"是两回事。中间件的迁移不算复杂,但它要求你重新思考鉴权逻辑放在哪一层,这种架构层面的调整本来就不可能半小时搞定。
如果你们项目也准备升,几个建议:
先在本地跑一遍 pnpm i react-router@latest,看报错有多少。如果只是 import 路径的问题,半天能搞定。如果涉及中间件和 ESM 兼容,预留两天。另外就是直接上 v8.3.1,别停在之前的版本,预渲染的 bug 刚修。