2026-04-29 22:19:51 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [React, Migration, Upgrade, Frontend]
|
|
|
|
|
|
create time: 2026-04-29 22:17
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 迁移与升级
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
大型 React 项目不可避免地面临技术债务和版本升级。本文档提供从 Class 组件到函数组件、从旧版 Router 到 v7、以及应对 React Major Version 变更的系统化迁移策略。
|
|
|
|
|
|
|
|
|
|
|
|
## Class → Function + Hooks 迁移
|
|
|
|
|
|
|
|
|
|
|
|
### 逐步对照表
|
|
|
|
|
|
|
|
|
|
|
|
| Class 特性 | Hook 等效写法 |
|
|
|
|
|
|
|-----------|--------------|
|
|
|
|
|
|
| `this.state` | `useState` |
|
|
|
|
|
|
| `componentDidMount` | `useEffect(() => { ... }, [])` |
|
|
|
|
|
|
| `componentDidUpdate(prev)` | `useEffect(() => { ... }, [dep])` |
|
|
|
|
|
|
| `componentWillUnmount` | `useEffect(() => { return () => { cleanup } })` |
|
|
|
|
|
|
| `this.setState(fn)` | `setState(prev => ({ ... }))` |
|
|
|
|
|
|
| `ref = React.createRef()` | `useRef` |
|
|
|
|
|
|
| `shouldComponentUpdate` | `React.memo` / `useMemo` / `useCallback` |
|
|
|
|
|
|
| Context.Consumer | `useContext` |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| 派生计算值 | `useMemo(() => expensiveCalc(data), [data])` |
|
|
|
|
|
|
| 传递函数避免重渲染 | `useCallback(fn, deps)` |
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
### 性能相关 Hooks 迁移
|
|
|
|
|
|
|
|
|
|
|
|
在实际迁移中,Class 组件往往隐含了 `shouldComponentUpdate` 或手动优化逻辑。用 Hooks 重写时需要显式声明:
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
```tsx
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// ❌ Before: Class Component — 隐式全量渲染
|
|
|
|
|
|
class TodoList extends React.Component {
|
|
|
|
|
|
state = { filter: "", todos: [] };
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// shouldComponentUpdate 需要手动写,否则每次都全量渲染
|
2026-04-29 22:19:51 +08:00
|
|
|
|
render() {
|
2026-04-29 23:36:32 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<input onChange={e => this.setState({ filter: e.target.value })} />
|
|
|
|
|
|
{/* todos 每次都会重新创建新数组引用,子列表无法优化 */}
|
|
|
|
|
|
<TodoItems todos={this.state.todos.filter(t => t.text.includes(this.state.filter))} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// ✅ After: useMemo + useCallback 显式控制
|
|
|
|
|
|
function TodoList() {
|
|
|
|
|
|
const [filter, setFilter] = useState("");
|
|
|
|
|
|
const todos = useTodos(); // custom hook
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// 只在 todos/filter 变化时重新过滤
|
|
|
|
|
|
const filteredTodos = useMemo(
|
|
|
|
|
|
() => todos.filter(t => t.text.includes(filter)),
|
|
|
|
|
|
[todos, filter]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// setFilter 引用稳定,不会导致子组件无效重渲染
|
|
|
|
|
|
const handleFilter = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
setFilter(e.target.value);
|
|
|
|
|
|
}, []);
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<input onChange={handleFilter} />
|
|
|
|
|
|
<TodoItems todos={filteredTodos} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!tip] 何时使用 useMemo vs useCallback
|
|
|
|
|
|
> - **memorize value** → `useMemo`: 用于昂贵计算结果或防止子组件 props 引用变化
|
|
|
|
|
|
> - **memorize function** → `useCallback`: 用于传递给子组件回调或作为 effect 依赖
|
|
|
|
|
|
> - **不要过早优化**: 没有 profiling 数据支持时,默认不用 memo——可读性优先
|
|
|
|
|
|
|
|
|
|
|
|
### 迁移工具推荐
|
|
|
|
|
|
|
|
|
|
|
|
> [!note] react-codemod 自动化迁移
|
|
|
|
|
|
> Facebook 官方提供的 codemod 可以批量转换大部分 Class → Function 代码:
|
|
|
|
|
|
> ```bash
|
|
|
|
|
|
> npx jscodeshift -t node_modules/react-codemod/transforms/class-to-hook.js src/
|
|
|
|
|
|
> ```
|
|
|
|
|
|
> ⚠️ Codemod 只做机械转换,`useEffect` cleanup、`useCallback` 依赖数组等仍需人工审查。
|
|
|
|
|
|
> 建议将 `git diff --name-only` 结果加入 Code Review。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
> [!warning] 迁移 Checklist
|
|
|
|
|
|
> - [ ] 所有 `componentDidMount/Update/Unmount` 转译为 useEffect
|
|
|
|
|
|
> - [ ] State 合并:将互不相关的 state 拆分,减少不必要的重渲染
|
|
|
|
|
|
> - [ ] 移除 `this.` —— 检查所有闭包中的变量引用
|
|
|
|
|
|
> - [ ] Props 类型从 class props 改为 interface 解构
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> - [ ] 审查 `useEffect` 依赖数组,避免 stale closure
|
|
|
|
|
|
> - [ ] 为传给子组件的函数添加 `useCallback`(如经 profiling 确认需要)
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## React Router v6 → v7 迁移
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
graph LR
|
2026-04-29 23:36:32 +08:00
|
|
|
|
A["v6 BrowserRouter + Switch"] -->|"Routes + createBrowserRouter"| B["v7 Data Router"]
|
|
|
|
|
|
C["v6 <br/>Route path prop"] -->|"简化"| D["v7 route config object"]
|
|
|
|
|
|
E["v6 <br/>Redirect"] -->|"Navigate replace"| F["v7 Navigate"]
|
|
|
|
|
|
G["v6 Outlet"] -->|"保留,位置不变"| H["v7 Outlet"]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
style A fill:#F4DBD6,color:#000
|
|
|
|
|
|
style B fill:#61DAFB,color:#000
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### API 映射速查
|
|
|
|
|
|
|
|
|
|
|
|
| v6 | v7 | 备注 |
|
|
|
|
|
|
|----|----|------|
|
|
|
|
|
|
| `<BrowserRouter>` | `<RouterProvider>` + `createBrowserRouter` | data router 模式 |
|
|
|
|
|
|
| `Switch` | `Routes` | v6 已有 |
|
|
|
|
|
|
| `<Redirect to="/x" />` | `<Navigate to="/x" />` | 语义更清晰 |
|
|
|
|
|
|
| `useHistory` | `useNavigate` | 名称统一 |
|
|
|
|
|
|
| `<Route component={Page}>` | `<Route element={<Page />} >` | JSX 方式 |
|
|
|
|
|
|
| `match.params` | `useParams()` | hook 方式 |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| `useLocation()` | 同 v6 | `location.key` 不再等于 `"default"` |
|
|
|
|
|
|
|
|
|
|
|
|
### Data Router 配置示例
|
|
|
|
|
|
|
|
|
|
|
|
v7 引入 **Data Router** 概念,路由配置从 JSX 变为纯数据驱动:
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// ❌ v6: JSX 声明式路由
|
|
|
|
|
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<BrowserRouter>
|
|
|
|
|
|
<Routes>
|
|
|
|
|
|
<Route path="/" element={<Layout />}>
|
|
|
|
|
|
<Route index element={<Home />} />
|
|
|
|
|
|
<Route path="users/:id" element={<Profile />} />
|
|
|
|
|
|
</Route>
|
|
|
|
|
|
</Routes>
|
|
|
|
|
|
</BrowserRouter>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ v7: createBrowserRouter 数据驱动
|
|
|
|
|
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
|
|
const router = createBrowserRouter([
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/",
|
|
|
|
|
|
element: <Layout />,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ index: true, element: <Home /> },
|
|
|
|
|
|
{ path: "users/:id", element: <Profile /> },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
return <RouterProvider router={router} />;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!note] 为什么 Data Router?
|
|
|
|
|
|
> Data Router 将路由状态与 React 状态分离,使得导航、加载、错误处理可以脱离组件树独立管理。这为后续 Loader / Action / Error Boundary 等数据功能奠定了基础。如果需要保留 JSX 写法,可以使用 `createRoutesFromElements` 桥接。
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// 折中方案:用 JSX 创建 route config
|
|
|
|
|
|
import { createBrowserRouter, createRoutesFromElements, Route } from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
|
|
const router = createBrowserRouter(
|
|
|
|
|
|
createRoutesFromElements(
|
|
|
|
|
|
<Route path="/" element={<App />}>
|
|
|
|
|
|
<Route index element={<Home />} />
|
|
|
|
|
|
<Route path="about" element={<About />} />
|
|
|
|
|
|
</Route>
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!warning] 常见陷阱
|
|
|
|
|
|
> - `useParams()` 在 route 未匹配时返回空对象 `{}` 而非 undefined,需要显式判断
|
|
|
|
|
|
> - `useSearchParams()` 行为与 v6 一致,但 URL 解析基于 Data Router
|
|
|
|
|
|
> - 嵌套路由的 Outlet 必须渲染在父元素中,否则子路由不会显示
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## React 17 → 18 兼容性
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
React 18 是一个 **渐进式** 升级,不需要整体重写。核心变更如下:
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
| 注意事项 | 说明 |
|
|
|
|
|
|
|----------|------|
|
|
|
|
|
|
| `ReactDOM.render` 已废弃 | 改用 `createRoot().render()` |
|
|
|
|
|
|
| `ReactDOM.unmountComponentAtNode` 已废弃 | 改用 `root.unmount()` |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| 事件处理在微任务中执行 | 批处理行为改变;`event.persist()` 已移除 |
|
|
|
|
|
|
| StrictMode 双重渲染 | 开发环境 effect 先 mount → unmount → remount(用于检测不安全 effect) |
|
|
|
|
|
|
| 自动批处理扩展 | React 事件、promise、setTimeout 等全部自动批处理 |
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// React 17
|
|
|
|
|
|
import ReactDOM from "react-dom";
|
|
|
|
|
|
ReactDOM.render(<App />, document.getElementById("root"));
|
|
|
|
|
|
|
|
|
|
|
|
// React 18
|
|
|
|
|
|
import { createRoot } from "react-dom/client";
|
|
|
|
|
|
const root = createRoot(document.getElementById("root"));
|
|
|
|
|
|
root.render(<App />);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
### Suspense on Waterfall & Streaming SSR
|
|
|
|
|
|
|
|
|
|
|
|
React 18 的 Suspense 可以配合 `SuspenseList` 和 streaming SSR 实现渐进式加载:
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// Server Component 场景(Next.js App Router)
|
|
|
|
|
|
// 外层 UI 立即呈现,内部大组件用 Suspense 包裹懒加载
|
|
|
|
|
|
export default function Page() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<main>
|
|
|
|
|
|
<Header /> // 立即渲染
|
|
|
|
|
|
<Navigation /> // 立即渲染
|
|
|
|
|
|
<Suspense fallback={<DashboardSkeleton />}>
|
|
|
|
|
|
<HeavyChart data={data} /> // 等待数据加载后渲染
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### useTransition & useDeferredValue
|
|
|
|
|
|
|
|
|
|
|
|
React 18 引入两个新的 concurrency primitives,用于优化用户体验:
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { useState, useTransition, useDeferredValue } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
function SearchPage() {
|
|
|
|
|
|
const [query, setQuery] = useState("");
|
|
|
|
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
|
|
|
|
|
|
|
|
// 输入更新是"普通"优先级,结果渲染是"低"优先级
|
|
|
|
|
|
const handleInput = (value: string) => {
|
|
|
|
|
|
setQuery(value); // 保持输入响应
|
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
|
doExpensiveSearch(value); // 不阻塞输入
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<input value={query} onChange={e => handleInput(e.target.value)} />
|
|
|
|
|
|
{isPending && <Spinner />}
|
|
|
|
|
|
<SearchResults query={query} />
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### createPortal 变更
|
|
|
|
|
|
|
|
|
|
|
|
v18 起 `createPortal` 的宿主容器不再要求必须是 DOM 树中的元素——这意味着可以在 off-screen 容器中创建 portal,延迟挂载到 DOM:
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// v17: 必须先将容器添加到 DOM
|
|
|
|
|
|
const container = document.createElement("div");
|
|
|
|
|
|
document.body.appendChild(container);
|
|
|
|
|
|
const portal = createRoot(container);
|
|
|
|
|
|
|
|
|
|
|
|
// v18: 可以直接创建,稍后再挂载
|
|
|
|
|
|
const container = document.createElement("div");
|
|
|
|
|
|
const portal = createRoot(container);
|
|
|
|
|
|
// ... later ...
|
|
|
|
|
|
document.body.appendChild(container.children[0]);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### React 18 升级 Checklist
|
|
|
|
|
|
|
|
|
|
|
|
> [!note] React 17 → 18 升级清单
|
|
|
|
|
|
> - [ ] `ReactDOM.render` → `createRoot().render()`
|
|
|
|
|
|
> - [ ] `StrictMode` 下确认无副作用泄漏(effect unmount/remount 测试)
|
|
|
|
|
|
> - [ ] 检查第三方库是否声明了 `react@^18` peerDep
|
|
|
|
|
|
> - [ ] `useTransition` / `useDeferredValue` 可逐步引入到高延迟场景
|
|
|
|
|
|
> - [ ] 流式 SSR 项目需验证 hydration 兼容性
|
|
|
|
|
|
> - [ ] `act()` 警告更严格,确保测试中 await act()
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
## Next.js Pages → App Router 迁移
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
### 目录与 API 对照表
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
| Pages Router | App Router |
|
|
|
|
|
|
|-------------|-----------|
|
|
|
|
|
|
| `pages/` 目录 | `app/` 目录 |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| `_document.tsx` | **不再需要**,由框架自动生成 HTML |
|
2026-04-29 22:19:51 +08:00
|
|
|
|
| `_app.tsx` | `app/layout.tsx`(根布局) |
|
|
|
|
|
|
| `getServerSideProps` | async Page 组件 |
|
|
|
|
|
|
| `getStaticProps` | async Page + `revalidate` |
|
|
|
|
|
|
| `next/router` | `next/navigation` |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| `getInitialProps` | **不支持**,需用 Server Component 替代 |
|
2026-04-29 22:19:51 +08:00
|
|
|
|
| `_middleware.ts` | `middleware.ts` |
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| Dynamic Route `[slug]` | `[slug]/page.tsx` |
|
|
|
|
|
|
|
|
|
|
|
|
### 核心差异:Server Components vs Client Components
|
|
|
|
|
|
|
|
|
|
|
|
App Router 中默认页面组件是 **React Server Component (RSC)**:
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
```tsx
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// ❌ Pages Router: getServerSideProps — 必须显式导出
|
2026-04-29 22:19:51 +08:00
|
|
|
|
export async function getServerSideProps() {
|
|
|
|
|
|
const data = await api.getData();
|
|
|
|
|
|
return { props: { data } };
|
|
|
|
|
|
}
|
2026-04-29 23:36:32 +08:00
|
|
|
|
function Page({ data }) {
|
|
|
|
|
|
return <View data={data} />;
|
|
|
|
|
|
}
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// ✅ App Router: async 组件直接运行在服务端
|
2026-04-29 22:19:51 +08:00
|
|
|
|
async function Page() {
|
|
|
|
|
|
const data = await api.getData();
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// data 不会序列化发送到客户端,减少 bundle size
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <View data={data} />;
|
|
|
|
|
|
}
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
// ⚡ 需要交互的组件用 "use client" 标记为 Client Component
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
function Counter() {
|
|
|
|
|
|
const [count, setCount] = useState(0);
|
|
|
|
|
|
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Loading UI & Error Boundary
|
|
|
|
|
|
|
|
|
|
|
|
Pages Router 需要手动实现 loading 和 error 页面;App Router 内置支持:
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// app/dashboard/loading.tsx — 过渡 UI 自动展示
|
|
|
|
|
|
export default function Loading() {
|
|
|
|
|
|
return <div className="animate-pulse">Loading dashboard...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// app/dashboard/error.tsx — boundary 捕获错误,无需 try/catch
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2>Something went wrong!</h2>
|
|
|
|
|
|
<button onClick={() => reset()}>Try again</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Middleware 变化
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
// ❌ Pages Router: _middleware.ts — 匹配规则有限
|
|
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
|
export function middleware(req) {
|
|
|
|
|
|
if (!req.cookies.get("auth")) {
|
|
|
|
|
|
return NextResponse.redirect("/login");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ App Router: middleware.ts — 更强大的匹配和响应能力
|
|
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
|
export function middleware(req) {
|
|
|
|
|
|
const token = req.cookies.get("auth")?.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!token && !req.nextUrl.pathname.startsWith("/login")) {
|
|
|
|
|
|
return NextResponse.redirect(new URL("/login", req.url));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return NextResponse.next(); // 放行
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const config = { matcher: "/dashboard/:path*" }; // 只匹配需要的路由
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
### 迁移路径建议
|
|
|
|
|
|
|
|
|
|
|
|
> [!tip] 渐进式迁移策略
|
|
|
|
|
|
> - **方案 A(推荐)**:在 `next.config.js` 中设置 `experimental.appDir = true`,双模式并行,按页面逐个迁移到 `app/`
|
|
|
|
|
|
> - **方案 B**:新项目直接使用 App Router,老项目保持 Pages Router 不动
|
|
|
|
|
|
> - 注意:`getInitialProps` **无法**迁移到 App Router,需要用 fetch + 缓存替代
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
## Major Version 升级通用流程
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
2026-04-29 23:36:32 +08:00
|
|
|
|
A["New Version Released"] --> B["Read Changelog / Breaking Changes"]
|
|
|
|
|
|
B --> C{"Impact Assessment"}
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
C --> D["Minor version bump"]
|
|
|
|
|
|
C --> E["API changes / new config"]
|
|
|
|
|
|
F["Architecture change"]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
D --> G["Upgrade + CI verify"]
|
|
|
|
|
|
E --> H["Fix per file"]
|
|
|
|
|
|
F --> I["Phased migration + parallel test"]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
G --> J["Update deps + run tests"]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
H --> J
|
|
|
|
|
|
I --> J
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
J --> K["Staging verification"]
|
|
|
|
|
|
K --> L["Canary release / Feature Flag"]
|
|
|
|
|
|
L --> M["Full rollout"]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
style F fill:#F5A87D,color:#000
|
|
|
|
|
|
style I fill:#F5A87D,color:#000
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
### 通用升级最佳实践
|
|
|
|
|
|
|
|
|
|
|
|
> [!quote] 经验法则
|
|
|
|
|
|
> **永远不要在同一次 PR 中升级依赖 + 重构业务逻辑。** 如果测试失败,你无法判断是版本破坏还是你的代码有问题。分步进行:先升级跑通 CI,再逐步优化。
|
|
|
|
|
|
|
|
|
|
|
|
| 实践 | 说明 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| **锁定大版本** | `package.json` 中使用 `"^18.2.0"` 而非 `"*"`,防止意外拉取 patch 外的变更 |
|
|
|
|
|
|
| **CI 前置** | 升级后首先确保所有测试通过(含 snapshot test) |
|
|
|
|
|
|
| **逐个升级** | 先修 core(React),再修生态(Router、Redux 等),最后修 UI 库 |
|
|
|
|
|
|
| **Feature Flag** | 对新路由或新页面用 feature flag 控制,观察指标后再全量开放 |
|
|
|
|
|
|
| **监控告警** | 上线后关注 Error Boundary 捕获的异常数量和性能指标(LCP、FCP) |
|
|
|
|
|
|
|
|
|
|
|
|
## 常见陷阱汇总
|
|
|
|
|
|
|
|
|
|
|
|
> [!danger] Class → Function 常见陷阱
|
|
|
|
|
|
> - **stale closure**: `useEffect` 中没有把依赖放入数组,导致闭包中读取到旧值
|
|
|
|
|
|
> ```tsx
|
|
|
|
|
|
> // ❌ name 变化时 effect 不会重新执行
|
|
|
|
|
|
> useEffect(() => { console.log(name); }, []);
|
|
|
|
|
|
> // ✅ 正确写法
|
|
|
|
|
|
> useEffect(() => { console.log(name); }, [name]);
|
|
|
|
|
|
> ```
|
|
|
|
|
|
> - **过度使用 memo**: `React.memo` / `useMemo` / `useCallback` 本身有比较成本, profiling 确认瓶颈后再加
|
|
|
|
|
|
> - **忽略 cleanup**: `useEffect` 返回的 cleanup 函数必须清除定时器、取消 fetch、移除 listener
|
|
|
|
|
|
|
|
|
|
|
|
> [!danger] Router 常见陷阱
|
|
|
|
|
|
> - Data Router 下 `navigate()` 不会自动停止正在进行的 loader fetch,需要 `AbortController`
|
|
|
|
|
|
> - v7 的 `future.v7_relativeSplatPath` 改变了 splat route 的路径解析规则
|
|
|
|
|
|
> - 嵌套路由中忘记在父元素渲染 `<Outlet />` 导致子路由不显示
|
|
|
|
|
|
|
|
|
|
|
|
> [!danger] React 18 常见陷阱
|
|
|
|
|
|
> - StrictMode 双重渲染不是 bug,是有意为之的检测机制——利用它发现 effect cleanup 问题
|
|
|
|
|
|
> - `ReactDOM.render` 移除后老代码直接报错,需要用 codemod 批量替换
|
|
|
|
|
|
> - `act()` 包装缺失会导致测试异步状态更新丢失,务必 `await act(async () => {...})`
|
|
|
|
|
|
|
|
|
|
|
|
> [!danger] Next.js 常见陷阱
|
|
|
|
|
|
> - Client Component 中不能直接 `import` Server Component
|
|
|
|
|
|
> - 动态路由组 `(group)` 只影响 URL 路径,不影响目录结构
|
|
|
|
|
|
> - App Router 中的 layout 会跨页面保持状态,需用 `key` 强制重建
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
## 关联笔记
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
- [[hhs/REACT/2. Hooks 篇/05-核心 Hooks]]
|
|
|
|
|
|
- [[hhs/REACT/3. 生态工具篇/08-路由管理]]
|
|
|
|
|
|
- [[hhs/REACT/4. 进阶篇/13-并发特性]]
|
|
|
|
|
|
- [[hhs/REACT/4. 进阶篇/14-Serverside Rendering]]
|
|
|
|
|
|
- [[hhs/REACT/5. 工程实践篇/15-性能优化]]
|
|
|
|
|
|
- [[hhs/REACT/5. 工程实践篇/16-测试]]
|