How One Developer Swapped an 80k-Line jQuery App for Vue3 Without Permission
Disclaimer: This article is for technical exchange only. It is not recommended to refactor directly in a production environment without approval. But I did it anyway.
1. How It Started
The company had a backend management system that went live in 2016. The frontend tech stack was jQuery 1.12 + Bootstrap 3 + a hand-written template engine. The codebase was roughly 80,000 lines, with no modularization and no build tools. Over 30 JS files were imported directly in the HTML via <script src="...">, and the loading order relied entirely on human memory.
On my first day, the CTO told me: "This project is very stable. Just familiarize yourself with the business logic; don't touch anything else."
I opened the code and saw a single app.js file over 6,000 lines long, mixing DOM manipulation, AJAX requests, business logic, and utility functions. Variable names cycled from a to z. The best part was a comment inside:
// 2018.3.15 Added by Lao Wang. Don't delete, or the reports page will break.
I asked who Lao Wang was. A colleague said he left the company back in 2019.
At that moment I knew: This isn't maintenance; this is archaeology.
2. Why It Had to Be Refactored
What really made up my mind was a production incident.
The business team requested a feature: add a "Batch Export" button to the order list page. Sounds simple, right?
I spent 40 minutes searching through the jQuery code and finally located the list rendering logic—inside an 800-line function, lines 347 to 512 were responsible for concatenating HTML strings. I carefully added 3 lines of code, tested locally, and deployed.
The entire site's styles broke.
The reason: inside that 800-line function, a global variable i on line 203 was reused in a loop. My new code declared another i on line 600, causing an earlier loop to exit prematurely and the DOM structure to misalign.
Debugging took 4 hours. Rolling back took 20 minutes. The CTO @mentioned me in the group chat: "Next time, assess the risks before making changes."
I stared at the screen and thought: This isn't a risk problem; it's an architecture problem.
3. The Stealth Plan: Gradual Invasion
Rewriting 80,000 lines of code outright was unrealistic, and the CTO would never approve it. I decided on an "underground route": gradually embed Vue3 into the jQuery project, allowing users to switch without noticing.
The core strategy had three rules:
- Page-level replacement: Refactor only one page at a time, leaving other modules untouched.
- Route hijacking: Use Vue Router to take over some routes, allowing jQuery pages and Vue pages to coexist.
- Data layer reuse: Vue components directly call the existing jQuery AJAX wrapper, requiring zero changes to the backend APIs.
3.1 Step 1: "Plant" a Vue App Inside jQuery
I secretly added a vue-app/ folder in the project root and initialized a Vue3 project with Vite. Then I modified the entry HTML:
<!-- Original jQuery entry -->
<div id="legacy-app">
<!-- All jQuery pages render here -->
</div>
<!-- New Vue mount point, hidden by default -->
<div id="vue-app" style="display:none;"></div>
In main.js, I added route judgment logic:
// Route mapping table: which paths go to Vue, which go to jQuery
const VUE_ROUTES = ['/orders', '/users', '/dashboard-v2'];
const currentPath = window.location.pathname;
if (VUE_ROUTES.some(route => currentPath.startsWith(route))) {
// Hide jQuery container, mount Vue
document.getElementById('legacy-app').style.display = 'none';
document.getElementById('vue-app').style.display = 'block';
createApp(App).use(router).mount('#vue-app');
} else {
// Continue with old jQuery logic
legacyInit();
}
Key point: Vue pages and jQuery pages are completely isolated and do not interfere with each other. When users navigate, the URL changes normally; only the underlying rendering engine is swapped.
3.2 Step 2: Reuse jQuery's Data Layer
The company had an 8-year-old jQuery AJAX wrapper that looked roughly like this:
// 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) }); }
};
I wrote an adapter layer in the Vue project to call this global object directly:
// 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')
};
Usage inside a Vue component:
<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>
Zero backend changes, gradual frontend replacement. This is the core reason the whole plan was feasible.
3.3 Step 3: Use Web Components as a Component Bridge
Some pages were "half-new, half-old"—for example, the header navigation and sidebar were still rendered by jQuery, but the main content area needed Vue components.
I built a Web Components bridge:
// vue-app/utils/webComponentAdapter.js
import { defineCustomElement } from 'vue';
import OrderTable from '@/components/OrderTable.vue';
const OrderTableElement = defineCustomElement(OrderTable);
customElements.define('order-table', OrderTableElement);
Then it could be used as a native tag inside jQuery pages:
<!-- This is a jQuery-rendered page -->
<div class="content">
<h2>Order Management</h2>
<!-- Vue component embedded as a custom element -->
<order-table :initial-data='<%= JSON.stringify(orders) %>'></order-table>
</div>
jQuery handles the page frame, Vue handles complex interactive components. Each does its own job without polluting the other.
4. The Most Critical Battle: The Order List Page
The first page fully refactored with Vue3 was the order list—the same page that caused my earlier incident.
4.1 The Original jQuery Code (Excerpt)
function renderOrderList(data) {
var html = '<table class="table"><thead><tr>';
html += '<th>Order No.</th><th>Customer</th><th>Amount</th><th>Status</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);
// Bind events
$('tr[data-id]').click(function() {
var id = $(this).data('id');
openOrderDetail(id);
});
}
Problems: HTML string concatenation, global event delegation, state management relying entirely on DOM attributes. Adding one feature required changes in 5 places; deleting one line could break 3 pages.
4.2 The Refactored Vue3 Code
<template>
<div class="order-page">
<SearchFilter v-model="filters" @search="handleSearch" />
<el-table :data="orders" @row-click="handleRowClick">
<el-table-column prop="orderNo" label="Order No." />
<el-table-column prop="customerName" label="Customer" />
<el-table-column label="Amount">
<template #default="{ row }">
¥{{ row.amount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="Status">
<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: 'Pending', 1: 'Completed', 2: 'Cancelled' };
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); // Compatible with the old system's global function
};
onMounted(handleSearch);
</script>
Changes:
- Template and logic are separated; no more string concatenation.
- State management uses
ref/reactiveinstead of relying on the DOM. - Event binding is declarative; no more manual
$.click. - Pagination, filtering, and tables are all componentized and reusable.
After refactoring this page, I tested for 3 days. It went live with zero incidents. More importantly, the business team said, "This page seems faster," but they had no idea the underlying engine had been swapped.
5. The Whole Team Started Copying My Code
Two weeks after the order list page went live, colleagues started asking me:
- "Can I borrow your table component?"
- "How did you write the pagination logic? I want that for my page too."
- "How do you get Vue files running in our project?"
I reluctantly shared the code on the surface, but inside I was grinning.
A month later, 4 out of 6 people on the team had started writing new pages with Vue3. During a weekly meeting, the CTO said, "Lately, the frontend code quality seems to have improved. Keep it up, everyone."
I held back my laughter.
6. Technical Solution Summary
The core of this "gradual invasion" plan can be summarized in one diagram:
┌─────────────────────────────────────────┐
│ Unified Page for Users │
├─────────────────────────────────────────┤
│ Vue Router takes over some routes │
│ ┌─────────┐ ┌─────────┐ │
│ │ Vue3 Page │ │ Vue3 Page │ │
│ │ /orders │ │ /users │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────┴──────────────┴────┐ │
│ │ legacyApi Adapter │ │
│ │ (reuses jQuery AJAX) │ │
│ └──────────┬─────────────┘ │
│ │ │
│ ┌──────────┴─────────────┐ │
│ │ jQuery Legacy System │ │
│ │ (other pages continue) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────┘
Key Design Principles:
- Zero backend changes: All APIs are reused, reducing resistance to progress.
- Page-level isolation: Vue and jQuery do not pollute each other; can be rolled out gradually.
- Independent toolchains: Vue uses Vite, jQuery uses native; build artifacts are introduced via CDN or same-domain deployment.
- Gradual replacement: Risk is controllable, and rollback is possible at any time.
7. Pitfalls Encountered
Global style pollution: Bootstrap 3 global styles used by jQuery affected Vue components. The solution was adding
scopedstyles and CSS Modules to the Vue root node.Memory leaks: Events bound by jQuery were not cleaned up when Vue components unmounted. The solution was manually calling
$.off()insideonUnmounted.Route refresh issues: Vue Router's
historymode requires server-side configuration, which the old project didn't support. Switching tohashmode provided perfect compatibility.Build artifact size: Vue3 + Element Plus bundled to over 400KB+, causing slow initial loads for old users. Using Vite's
manualChunksfor code splitting plus a CDN reduced the first screen to under 200KB.
8. Final Thoughts
The hardest part of refactoring isn't writing the code; it's finding a path forward under organizational resistance.
If I had directly told the CTO, "I want to rewrite 80,000 lines of jQuery into Vue3," I would likely have been rejected. But "gradual replacement" turned the refactoring into optimizing one page at a time. Each change had clear business value, controllable risk, and clear evidence.
Three months later, Vue3 pages accounted for 60% of the project's traffic. The CTO finally came to me and said, "How about... we migrate the rest too?"
I smiled and said, "Sure, I'll draft a plan."
That's this plan: 👉👉👉 Click me
Top 5 of 6 from juejin.cn, machine-translated. The original thread is authoritative.
Awesome [thumbs up]
What a talent
"The reason was that in that 800-line function, line 203 had a global variable i being reused in a loop, and my new code declared another i at line 600, causing an earlier loop to exit prematurely and the DOM structure to misalign." Someone who can't even write a closure — you can imagine what their refactored code looks like.
You're a talent yourself. The guy said the old code was in one function. Do you dare to touch that function? Changing it would break even more. You just keep writing inside that function.
People like this will get their comeuppance sooner or later. It's not that others can't fix it. It's that the boss pays peanuts. You come in and upset the balance, and from then on all the work falls on you.
Nothing better to do.