184 lines
5.3 KiB
Markdown
184 lines
5.3 KiB
Markdown
|
|
---
|
||
|
|
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` |
|
||
|
|
|
||
|
|
### 实际迁移示例
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// ❌ Before: Class Component
|
||
|
|
class UserProfile extends React.Component<{ userId: string }> {
|
||
|
|
state = { user: null, loading: true };
|
||
|
|
|
||
|
|
componentDidMount() {
|
||
|
|
fetchUser(this.props.userId).then(user => {
|
||
|
|
this.setState({ user, loading: false });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
componentDidUpdate(prevProps) {
|
||
|
|
if (prevProps.userId !== this.props.userId) {
|
||
|
|
this.setState({ loading: true });
|
||
|
|
fetchUser(this.props.userId).then(user => this.setState({ user, loading: false }));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
componentWillUnmount() {
|
||
|
|
// nothing to clean up here
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
const { user, loading } = this.state;
|
||
|
|
return loading ? <Spinner /> : <div>{user.name}</div>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ After: Functional Component + Hooks
|
||
|
|
function UserProfile({ userId }: { userId: string }) {
|
||
|
|
const [user, setUser] = useState<User | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let cancelled = false;
|
||
|
|
setLoading(true);
|
||
|
|
|
||
|
|
fetchUser(userId).then(u => {
|
||
|
|
if (!cancelled) setUser(u);
|
||
|
|
}).finally(() => {
|
||
|
|
if (!cancelled) setLoading(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
return () => { cancelled = true; }; // cleanup
|
||
|
|
}, [userId]); // userId 变化时自动重新 fetch
|
||
|
|
|
||
|
|
if (loading) return <Spinner />;
|
||
|
|
return <div>{user?.name}</div>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
> [!warning] 迁移 Checklist
|
||
|
|
> - [ ] 所有 `componentDidMount/Update/Unmount` 转译为 useEffect
|
||
|
|
> - [ ] State 合并:将互不相关的 state 拆分,减少不必要的重渲染
|
||
|
|
> - [ ] 移除 `this.` —— 检查所有闭包中的变量引用
|
||
|
|
> - [ ] Props 类型从 class props 改为 interface 解构
|
||
|
|
|
||
|
|
## React Router v6 → v7 迁移
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
graph LR
|
||
|
|
A["v6 BrowserRouter"] -->|"Switch → Routes"| B["v7 Routes"]
|
||
|
|
C["v6 <Route path='/' element={} />"] -->|"path prop 简化"| D["v7 { path: '/', element: {} }"]
|
||
|
|
E["v6 <Redirect/>"] -->|"navigate(-1)/replace"| F["v7 <Navigate replace />"]
|
||
|
|
G["v6 Outlet"] -->|"保留"| H["v7 Outlet — 位置不变"]
|
||
|
|
|
||
|
|
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 方式 |
|
||
|
|
|
||
|
|
## React 17 → 18 兼容性
|
||
|
|
|
||
|
|
| 注意事项 | 说明 |
|
||
|
|
|----------|------|
|
||
|
|
| `ReactDOM.render` 已废弃 | 改用 `createRoot().render()` |
|
||
|
|
| `ReactDOM.unmountComponentAtNode` 已废弃 | 改用 `root.unmount()` |
|
||
|
|
| 事件处理在微任务中执行 | `event.persist()` 已移除 |
|
||
|
|
| StrictMode 双重渲染 | 开发环境 effect 先 mount → unmount → remount(用于检测) |
|
||
|
|
|
||
|
|
```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 />);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Next.js Pages → App Router 迁移
|
||
|
|
|
||
|
|
| Pages Router | App Router |
|
||
|
|
|-------------|-----------|
|
||
|
|
| `pages/` 目录 | `app/` 目录 |
|
||
|
|
| `_document.tsx` | `app/layout.tsx` |
|
||
|
|
| `_app.tsx` | `app/layout.tsx`(根布局) |
|
||
|
|
| `getServerSideProps` | async Page 组件 |
|
||
|
|
| `getStaticProps` | async Page + `revalidate` |
|
||
|
|
| `next/router` | `next/navigation` |
|
||
|
|
| `_middleware.ts` | `middleware.ts` |
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// Pages Router
|
||
|
|
export async function getServerSideProps() {
|
||
|
|
const data = await api.getData();
|
||
|
|
return { props: { data } };
|
||
|
|
}
|
||
|
|
|
||
|
|
// App Router
|
||
|
|
async function Page() {
|
||
|
|
const data = await api.getData();
|
||
|
|
return <View data={data} />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Major Version 升级通用流程
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
flowchart TD
|
||
|
|
A["发现新版本发布"] --> B["阅读 Changelog / Breaking Changes"]
|
||
|
|
B --> C{"影响范围评估"}
|
||
|
|
|
||
|
|
C --> D["仅 minor version bump"]
|
||
|
|
C --> E["API 变更 / 新配置项"]
|
||
|
|
C --> F["架构性改动"]
|
||
|
|
|
||
|
|
D --> G["直接升级 + CI 验证"]
|
||
|
|
E --> H["逐个文件修复"]
|
||
|
|
F --> I["分阶段迁移 + 并行验证"]
|
||
|
|
|
||
|
|
G --> J["更新依赖 + 运行测试"]
|
||
|
|
H --> J
|
||
|
|
I --> J
|
||
|
|
|
||
|
|
J --> K["Staging 环境验证"]
|
||
|
|
K --> L["灰度发布 / Feature Flag"]
|
||
|
|
L --> M["全量上线"]
|
||
|
|
|
||
|
|
style F fill:#F5A87D,color:#000
|
||
|
|
style I fill:#F5A87D,color:#000
|
||
|
|
```
|
||
|
|
|
||
|
|
## 关联笔记
|