> 先打個預防針:本文僅供技術交流,不建議在正式環境未經審批直接重構。但我確實這麼做了。
公司有個 2016 年上線的後台管理系統,前端技術棧是 jQuery 1.12 + Bootstrap 3 + 手寫模板引擎。程式碼量大概 8 萬行,沒有模組化,沒有建置工具,HTML 裡直接 <script src="..."> 引入 30 多個 JS 檔案,載入順序全靠人工記憶。
我入職第一天,CTO 跟我說:「這個專案很穩定,你熟悉一下業務邏輯就好,別亂動。」
我打開程式碼一看,一個 app.js 檔案 6000 多行,裡面混著 DOM 操作、AJAX 請求、業務邏輯、工具函式,變數命名從 a 到 z 輪著用。最絕的是,裡面有一行註解:
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,讓使用者無感切換。
核心策略就三條:
我在專案根目錄偷偷加了 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 正常跳轉,只是底層渲染引擎換了。
公司有一套用了 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>
後端零改動,前端漸進替換。 這是整個方案能落地的核心。
有些頁面是「半新半舊」——比如頁首導覽和側邊欄還是 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 重構的頁面是訂單列表,這也是之前讓我出事故的頁面。
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 個頁面。
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 天,上線後零事故。更關鍵的是,業務方說「這個頁面怎麼變快了」,但他們不知道底層已經換了引擎。
訂單列表頁上線兩週後,組裡陸續有人問我:
我表面上「勉為其難」地分享了程式碼,心裡暗爽。
一個月後,組裡 6 個人中有 4 個開始用 Vue3 寫新頁面。CTO 某天開週會時說:「最近前端程式碼品質好像有提升,大家繼續保持。」
我在下面憋笑。
這套「漸進式入侵」方案的核心可以總結成一張圖:
plain
bash 程式碼解讀複製程式碼┌─────────────────────────────────────────┐
│ 使用者看到的統一頁面 │
├─────────────────────────────────────────┤
│ Vue Router 接管部分路由 │
│ ┌─────────┐ ┌─────────┐ │
│ │ Vue3 頁面 │ │ Vue3 頁面 │ │
│ │ /orders │ │ /users │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────┴──────────────┴────┐ │
│ │ legacyApi 適配層 │ │
│ │ (復用 jQuery AJAX 封裝) │ │
│ └──────────┬─────────────┘ │
│ │ │
│ ┌──────────┴─────────────┐ │
│ │ jQuery 舊系統 │ │
│ │ (其他頁面繼續運行) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────┘
關鍵設計原則:
全域樣式污染:jQuery 用的 Bootstrap 3 全域樣式會作用到 Vue 元件。解決方案是在 Vue 根節點加 scoped 樣式 + CSS Modules。
記憶體洩漏:Vue 元件卸載時,jQuery 綁定的事件沒清理。解決方案是在 onUnmounted 裡手動 $.off()。
路由重新整理問題:Vue Router 的 history 模式需要伺服器設定,舊專案不支援。改用 hash 模式,完美相容。
建置產物體積:Vue3 + Element Plus 打包後 400KB+,老使用者首次載入慢。用 Vite 的 manualChunks 拆包 + CDN 引入,首屏降到 200KB 以內。
重構這件事,技術上最難的不是寫程式碼,而是在組織阻力下找到一條能推進的路。
如果我當時直接跟 CTO 說「我要把 8 萬行 jQuery 重寫成 Vue3」,大概率會被駁回。但「漸進式替換」讓重構變成了一個頁面一個頁面的優化,每次改動都有明確的業務價值,風險可控,證據清楚。
三個月後,Vue3 頁面占了專案 60% 的存取量。CTO 終於主動找我:「要不……我們把剩下的也遷了吧?」
我笑了笑:「好,我出個方案。」
這就是那個方案:👉👉👉點我