老前端项目别急着重写:用渐进现代化更稳
老前端项目别急着重写:用渐进现代化更稳
July 13, 2026
维护老前端时最诱人的话是「不如重写」。最贵的结果也常常是这句话——需求没冻住、人没齐、新栈第一次也很难一次做对。更稳的是绞杀者模式:新旧并存,按边界替换。
1. 三种推进方式(带适用条件)
| 方式 | 做法 | 适用 |
|---|---|---|
| Outside-In | 整页切到新栈 | 路由清晰、可独立验收 |
| Inside-Out | 旧页里嵌新组件 | 局部体验债重 |
| 绞杀者 | 新功能只写新栈 | 线上持续迭代产品 |
2. Outside-In:Nginx 按路径分流
# /etc/nginx/sites-available/app.conf
upstream legacy {
server 127.0.0.1:3000;
}
upstream nextgen {
server 127.0.0.1:3001;
}
server {
server_name app.example.com;
# 已迁移页面走新栈
location ^~ /settings {
proxy_pass http://nextgen;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /billing {
proxy_pass http://nextgen;
proxy_set_header Host $host;
}
# 其余仍旧栈
location / {
proxy_pass http://legacy;
proxy_set_header Host $host;
}
}发布节奏:先切内测域名 → 看错误率 → 再切正式。回滚 = 改回 proxy_pass,比「整站切换 git 分支」安全得多。
3. Inside-Out:旧 HTML 里挂一个 React 岛
旧模板只留挂载点:
<!-- legacy settings 页片段 -->
<div id="legacy-shell">
<h1>账户设置(旧壳)</h1>
<div id="react-profile-root"></div>
</div>
<script type="module" src="/nextgen/profile-island.js"></script>新栈打包成 island:
// profile-island.tsx
import { createRoot } from "react-dom/client";
import { ProfileForm } from "./ProfileForm";
const el = document.getElementById("react-profile-root");
if (el) {
createRoot(el).render(<ProfileForm />);
}// ProfileForm.tsx
import { useState } from "react";
import { updateProfile } from "./api";
export function ProfileForm() {
const [name, setName] = useState("");
const [msg, setMsg] = useState("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
await updateProfile({ name });
setMsg("已保存");
}
return (
<form onSubmit={onSubmit}>
<label>
显示名
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<button type="submit">保存</button>
<p>{msg}</p>
</form>
);
}CSS 冲突时:给岛根节点加独立 class 前缀,或把岛封成 Web Component(Shadow DOM)。
4. 迁移前先抽 API service(新旧共用)
// api/client.ts —— 框架无关
export type ApiError = { status: number; message: string };
export async function api<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
const res = await fetch(path, {
...init,
headers: {
"Content-Type": "application/json",
...(init.headers || {}),
},
credentials: "same-origin",
});
if (!res.ok) {
const message = await res.text();
throw { status: res.status, message } satisfies ApiError;
}
return res.json() as Promise<T>;
}
export function updateProfile(body: { name: string }) {
return api<{ ok: true }>("/api/profile", {
method: "PUT",
body: JSON.stringify(body),
});
}旧 jQuery 页和新 React 页都调同一套,避免「UI 迁完了,请求逻辑还复制三份」。
5. 案例:后台「设置页」先切,报表页后切
背景:Vue2 老后台,构建链脆弱,全量重写要停需求两个月——不可接受。
执行:
- 新建 Vite + React 子应用,只实现
/settings/* - Nginx 分流,Cookie/Session 仍走原域
- 约定:新需求禁止合进 Vue2
- 两个迭代后再切
/billing - 报表因 ECharts 历史包袱最后处理
结果:用户无感知大迁移;团队在真功能里学会新栈,而不是在隔离 demo 里学。
6. 纪律比框架重要
[ ] 列出路由清单与依赖图
[ ] 抽 API / 鉴权到共享层
[ ] 选定第一个 Outside-In 页面(高价值、低耦合)
[ ] 旧栈冻结新功能
[ ] 每个切面有回滚开关(Nginx / Feature Flag)
[ ] 共享组件库随迁随建,避免两套按钮永久共存能持续交付的现代化,远比完美的一次性重写更接近真实工程。