公司技术债堆积如山,我一人之力用 Vue3 偷换了整个前端架构

先叠甲:本文仅供技术交流,不建议在生产环境未经审批直接重构。但我确实这么干了。


一、事情是怎么开始的

公司有个 2016 年上线的后台管理系统,前端技术栈是 jQuery 1.12 + Bootstrap 3 + 手写模板引擎 。代码量大概 8 万行,没有模块化,没有构建工具,HTML 里直接 <script src="..."> 引入 30 多个 JS 文件,加载顺序全靠人工记忆。

我入职第一天,CTO 跟我说:"这个项目很稳定,你熟悉一下业务逻辑就行,别乱动。"

我打开代码一看,一个 app.js 文件 6000 多行,里面混着 DOM 操作、AJAX 请求、业务逻辑、工具函数,变量命名从 az 轮着用。最绝的是,里面有一行注释:

JavaScript

arduino 复制代码
// 2018.3.15 老王加的,别删,删了报表页面崩

我问老王是谁,同事说老王 2019 年就离职了。

那一刻我知道,这不是维护,这是考古。


二、为什么必须重构

真正让我下定决心的是一次生产事故。

业务部门提了个需求:在订单列表页加一个"批量导出"按钮。听起来很简单对吧?

我在 jQuery 代码里找了 40 分钟,终于定位到列表渲染逻辑------在一个 800 行的函数里,第 347 行到 512 行负责拼接 HTML 字符串。我小心翼翼地加了 3 行代码,本地测试通过,提交上线。

结果全站样式崩了。

原因是那个 800 行的函数里,第 203 行有一个全局变量 i 被循环复用,我的新代码在第 600 行又声明了一个 i,导致前面某个循环提前退出,DOM 结构错位。

排查花了 4 小时,回滚花了 20 分钟。CTO 在群里@我:"下次改动先评估风险。"

我盯着屏幕,心想:这不是风险问题,这是架构问题。


三、偷换计划:渐进式入侵

直接重写 8 万行代码不现实,CTO 不可能批。我决定走一条"地下路线":在 jQuery 项目里逐步嵌入 Vue3,让用户无感知切换。

核心策略就三条:

  1. 页面级替换:一次只重构一个页面,不碰其他模块;
  2. 路由劫持:用 Vue Router 接管部分路由,jQuery 页面和 Vue 页面共存;
  3. 数据层复用:Vue 组件直接调用现有的 jQuery AJAX 封装,后端接口零改动。

3.1 第一步:在 jQuery 里"种"一个 Vue 应用

我在项目根目录偷偷加了 vue-app/ 文件夹,用 Vite 初始化了一个 Vue3 项目。然后修改了入口 HTML:

HTML

xml 复制代码
<!-- 原来的 jQuery 入口 -->
<div id="legacy-app">
  <!-- 所有 jQuery 页面渲染在这里 -->
</div>

<!-- 新增的 Vue 挂载点,默认隐藏 -->
<div id="vue-app" style="display:none;"></div>

main.js 里加了一段路由判断逻辑:

JavaScript

ini 复制代码
// 路由映射表:哪些路径走 Vue,哪些走 jQuery
const VUE_ROUTES = ['/orders', '/users', '/dashboard-v2'];

const currentPath = window.location.pathname;

if (VUE_ROUTES.some(route => currentPath.startsWith(route))) {
  // 隐藏 jQuery 容器,挂载 Vue
  document.getElementById('legacy-app').style.display = 'none';
  document.getElementById('vue-app').style.display = 'block';
  
  createApp(App).use(router).mount('#vue-app');
} else {
  // 继续走 jQuery 老逻辑
  legacyInit();
}

关键点:Vue 页面和 jQuery 页面完全隔离,互不干扰。用户切换页面时,URL 正常跳转,只是底层渲染引擎换了。

3.2 第二步:复用 jQuery 的数据层

公司有一套用了 8 年的 jQuery AJAX 封装,大概长这样:

JavaScript

javascript 复制代码
// legacy/api.js
window.LegacyAPI = {
  get(url, params) { return $.ajax({ url, type: 'GET', data: params }); },
  post(url, data) { return $.ajax({ url, type: 'POST', data: JSON.stringify(data) }); }
};

我在 Vue 项目里写了一个适配层,直接调用这个全局对象:

JavaScript

javascript 复制代码
// vue-app/utils/legacyApi.js
export const legacyApi = {
  get: (url, params) => window.LegacyAPI?.get(url, params) || Promise.reject('API not ready'),
  post: (url, data) => window.LegacyAPI?.post(url, data) || Promise.reject('API not ready')
};

Vue 组件里这样用:

vue

xml 复制代码
<script setup>
import { ref, onMounted } from 'vue';
import { legacyApi } from '@/utils/legacyApi';

const orders = ref([]);

onMounted(async () => {
  const res = await legacyApi.get('/api/orders', { page: 1, size: 20 });
  orders.value = res.data.list;
});
</script>

后端零改动,前端渐进替换。 这是整个方案能落地的核心。

3.3 第三步:用 Web Components 做组件桥接

有些页面是"半新半旧"------比如头部导航和侧边栏还是 jQuery 渲染的,但中间内容区想用 Vue 组件。

我封装了一个 Web Components 桥接器:

JavaScript

javascript 复制代码
// vue-app/utils/webComponentAdapter.js
import { defineCustomElement } from 'vue';
import OrderTable from '@/components/OrderTable.vue';

const OrderTableElement = defineCustomElement(OrderTable);
customElements.define('order-table', OrderTableElement);

然后在 jQuery 页面里直接当原生标签用:

HTML

xml 复制代码
<!-- 这是 jQuery 渲染的页面 -->
<div class="content">
  <h2>订单管理</h2>
  <!-- Vue 组件作为自定义元素嵌入 -->
  <order-table :initial-data='<%= JSON.stringify(orders) %>'></order-table>
</div>

jQuery 负责页面框架,Vue 负责复杂交互组件,两边各干各的,互不污染。


四、最关键的战役:订单列表页

第一个完全用 Vue3 重构的页面是订单列表,这也是之前让我出事故的页面。

4.1 原来的 jQuery 写法(节选)

JavaScript

css 复制代码
function renderOrderList(data) {
  var html = '<table class="table"><thead><tr>';
  html += '<th>订单号</th><th>客户</th><th>金额</th><th>状态</th>';
  html += '</tr></thead><tbody>';
  
  for (var i = 0; i < data.length; i++) {
    var item = data[i];
    var statusClass = item.status === 1 ? 'success' : 'danger';
    html += '<tr data-id="' + item.id + '">';
    html += '<td>' + item.orderNo + '</td>';
    html += '<td>' + item.customerName + '</td>';
    html += '<td>¥' + item.amount.toFixed(2) + '</td>';
    html += '<td><span class="label label-' + statusClass + '">' + getStatusText(item.status) + '</span></td>';
    html += '</tr>';
  }
  
  html += '</tbody></table>';
  $('#order-list-container').html(html);
  
  // 绑定事件
  $('tr[data-id]').click(function() {
    var id = $(this).data('id');
    openOrderDetail(id);
  });
}

问题:字符串拼接 HTML、全局事件委托、状态管理全靠 DOM 属性。加一个功能要改 5 个地方,删一行代码可能崩 3 个页面。

4.2 重构后的 Vue3 写法

vue

ini 复制代码
<template>
  <div class="order-page">
    <SearchFilter v-model="filters" @search="handleSearch" />
    
    <el-table :data="orders" @row-click="handleRowClick">
      <el-table-column prop="orderNo" label="订单号" />
      <el-table-column prop="customerName" label="客户" />
      <el-table-column label="金额">
        <template #default="{ row }">
          ¥{{ row.amount.toFixed(2) }}
        </template>
      </el-table-column>
      <el-table-column label="状态">
        <template #default="{ row }">
          <el-tag :type="row.status === 1 ? 'success' : 'danger'">
            {{ statusMap[row.status] }}
          </el-tag>
        </template>
      </el-table-column>
    </el-table>
    
    <Pagination v-model:page="pagination.page" v-model:size="pagination.size" 
                :total="pagination.total" @change="handleSearch" />
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { legacyApi } from '@/utils/legacyApi';
import SearchFilter from './components/SearchFilter.vue';
import Pagination from '@/components/Pagination.vue';

const filters = reactive({ keyword: '', status: '' });
const orders = ref([]);
const pagination = reactive({ page: 1, size: 20, total: 0 });
const statusMap = { 0: '待处理', 1: '已完成', 2: '已取消' };

const handleSearch = async () => {
  const res = await legacyApi.get('/api/orders', {
    ...filters,
    page: pagination.page,
    size: pagination.size
  });
  orders.value = res.data.list;
  pagination.total = res.data.total;
};

const handleRowClick = (row) => {
  window.openOrderDetail?.(row.id); // 兼容老系统的全局函数
};

onMounted(handleSearch);
</script>

变化

  • 模板和逻辑分离,不再拼接字符串;
  • 状态管理用 ref/reactive,不再依赖 DOM;
  • 事件绑定声明式处理,不再手动 $.click
  • 分页、筛选、表格全部组件化,可复用。

重构完这个页面,我测了 3 天,上线后零事故。更关键的是,业务方说"这个页面怎么变快了",但他们不知道底层已经换了引擎。


五、全组都来抄代码了

订单列表页上线两周后,组里陆续有人问我:

  • "你这个表格组件能借我用一下吗?"
  • "分页逻辑怎么写的?我那个页面也想要。"
  • "Vue 文件怎么在咱们项目里跑起来的?"

我表面上"勉为其难"地分享了代码,心里暗爽。

一个月后,组里 6 个人中有 4 个开始用 Vue3 写新页面。CTO 某天开周会时说:"最近前端代码质量好像有所提升,大家继续保持。"

我在下面憋笑。


六、技术方案总结

这套"渐进式入侵"方案的核心可以总结为一张图:

plain

bash 复制代码
┌─────────────────────────────────────────┐
│           用户看到的统一页面               │
├─────────────────────────────────────────┤
│  Vue Router 接管部分路由                  │
│  ┌─────────┐    ┌─────────┐            │
│  │ Vue3 页面 │    │ Vue3 页面 │            │
│  │ /orders  │    │ /users   │            │
│  └────┬────┘    └────┬────┘            │
│       │              │                   │
│  ┌────┴──────────────┴────┐            │
│  │    legacyApi 适配层     │            │
│  │  (复用 jQuery AJAX 封装) │            │
│  └──────────┬─────────────┘            │
│             │                          │
│  ┌──────────┴─────────────┐            │
│  │    jQuery 老系统        │            │
│  │  (其他页面继续运行)      │            │
│  └────────────────────────┘            │
└─────────────────────────────────────────┘

关键设计原则

  1. 零后端改动:所有接口复用,降低推进阻力;
  2. 页面级隔离:Vue 和 jQuery 互不污染,可灰度上线;
  3. 工具链独立:Vue 用 Vite,jQuery 用原生,构建产物通过 CDN 或同域部署引入;
  4. 渐进替换:风险可控,随时可回滚。

七、踩过的坑

  1. 全局样式污染 :jQuery 用的 Bootstrap 3 全局样式会作用到 Vue 组件。解决方案是在 Vue 根节点加 scoped 样式 + CSS Modules。
  1. 内存泄漏 :Vue 组件卸载时,jQuery 绑定的事件没清理。解决方案是在 onUnmounted 里手动 $.off()
  2. 路由刷新问题 :Vue Router 的 history 模式需要服务端配置,老项目不支持。改用 hash 模式,完美兼容。
  3. 构建产物体积 :Vue3 + Element Plus 打包后 400KB+,老用户首次加载慢。用 Vite 的 manualChunks 拆包 + CDN 引入,首屏降到 200KB 以内。

八、写在最后

重构这件事,技术上最难的不是写代码,而是在组织阻力下找到一条能推进的路

如果我当时直接跟 CTO 说"我要把 8 万行 jQuery 重写成 Vue3",大概率会被驳回。但"渐进式替换"让重构变成了一个页面一个页面的优化,每次改动都有明确的业务价值,风险可控,证据清晰。

三个月后,Vue3 页面占了项目 60% 的访问量。CTO 终于主动找我:"要不......我们把剩下的也迁了吧?"

我笑了笑:"好,我出个方案。"

这就是那个方案:👉👉👉典我

相关推荐
用户938515635071 小时前
深入理解 JavaScript 中的 this 与数据存储的奥秘
前端·javascript
JNX_SEMI2 小时前
AT2659 L1频段多模卫星导航低噪声放大器技术解析
前端·单片机·嵌入式硬件·物联网·硬件工程
Profile排查笔记4 小时前
指纹浏览器环境异常排查:Fingerprint、Profile、Proxy、Session 和 Task Log 怎么看
前端·人工智能·后端·自动化
京韵养生记4 小时前
【无标题】
java·服务器·前端
格子软件4 小时前
2026年分布式GEO代理流量调度:源码级状态机防重挂实战
java·vue.js·人工智能·spring boot·分布式·vue
大气的小蜜蜂4 小时前
领域层的服务
java·前端·数据库
星栈4 小时前
LiveView 的 LiveComponent:比 React 组件更轻,但我一开始真的把它用错了
前端·前端框架·elixir
林希_Rachel_傻希希5 小时前
web性能优化之延迟加载图片和<inframe>
前端·javascript·面试
maxmaxma5 小时前
Konva 从入门到实践 - day1
前端