跳至内容
老前端项目别急着重写:用渐进现代化更稳

老前端项目别急着重写:用渐进现代化更稳

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 老后台,构建链脆弱,全量重写要停需求两个月——不可接受。

执行:

  1. 新建 Vite + React 子应用,只实现 /settings/*
  2. Nginx 分流,Cookie/Session 仍走原域
  3. 约定:新需求禁止合进 Vue2
  4. 两个迭代后再切 /billing
  5. 报表因 ECharts 历史包袱最后处理

结果:用户无感知大迁移;团队在真功能里学会新栈,而不是在隔离 demo 里学。

6. 纪律比框架重要

[ ] 列出路由清单与依赖图
[ ] 抽 API / 鉴权到共享层
[ ] 选定第一个 Outside-In 页面(高价值、低耦合)
[ ] 旧栈冻结新功能
[ ] 每个切面有回滚开关(Nginx / Feature Flag)
[ ] 共享组件库随迁随建,避免两套按钮永久共存

能持续交付的现代化,远比完美的一次性重写更接近真实工程。