跪拜 Guibai
← Back to the summary

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:

  1. Page-level replacement: Refactor only one page at a time, leaving other modules untouched.
  2. Route hijacking: Use Vue Router to take over some routes, allowing jQuery pages and Vue pages to coexist.
  3. 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:

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:

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:

  1. Zero backend changes: All APIs are reused, reducing resistance to progress.
  2. Page-level isolation: Vue and jQuery do not pollute each other; can be rolled out gradually.
  3. Independent toolchains: Vue uses Vite, jQuery uses native; build artifacts are introduced via CDN or same-domain deployment.
  4. Gradual replacement: Risk is controllable, and rollback is possible at any time.

7. Pitfalls Encountered

  1. Global style pollution: Bootstrap 3 global styles used by jQuery affected Vue components. The solution was adding scoped styles and CSS Modules to the Vue root node.

  2. Memory leaks: Events bound by jQuery were not cleaned up when Vue components unmounted. The solution was manually calling $.off() inside onUnmounted.

  3. Route refresh issues: Vue Router's history mode requires server-side configuration, which the old project didn't support. Switching to hash mode provided perfect compatibility.

  4. Build artifact size: Vue3 + Element Plus bundled to over 400KB+, causing slow initial loads for old users. Using Vite's manualChunks for 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

Comments

Top 13 of 21 from juejin.cn, machine-translated. The original thread is authoritative.

_前端小菜鸟_ 2 likes

Are you telling a story or is this a real event? No one above you made the call, and you just went ahead and changed things below. If nothing goes wrong, fine, but if something does, the whole blame is yours. No one helps you test it; it's all on you. Also, what about the time? No other work? Just maintaining that one project?

Miles_ovo

No problem with that. The development pace is fast now, with lots of iteration tasks. I feel if the code runs, try not to touch it. Refactoring takes too much time and easily introduces bugs.

理子

Isn't it possible that rewriting the code in Vue yourself is faster than reading old code and adding features? The time saved is your own slacking-off time. Insisting on hacking away at a mountain of shit—is your skill lacking or do you just want to eat shit?

左右童鞋 1 likes

My personal suggestion is to just directly rebuild with Vue3, using AI, it's very fast. Your progressive replacement approach seems a bit like showing off. Zero backend changes? Why keep bringing this up? It's front-end and back-end separation in the first place, what's there to say?

理子

Suggest you find a job and work for a while before talking. All this talk about refactoring—if something goes wrong, are you the one working overtime?

左右童鞋  → 理子

Your mouth really stinks! I've already used AI to refactor a product at my company, which five or six front-end devs iterated on for five or six years, from Vue2+Element UI to Vue3+Ant Design Vue. Is that hard? You just keep running your mouth.

OnceSure 1 likes

'The reason was that in that 800-line function, there was a global variable i on line 203 being reused in a loop, and my new code declared another i on line 600, causing an earlier loop to exit prematurely and the DOM structure to misalign.' Someone who can't even write a closure—the refactored code is imaginable.

ashuicoder

You're a talent too. The guy said the old code was in one function. Do you dare to change that function? Changing it would break even more. Isn't it just continuing to write inside that function?

OnceSure  → ashuicoder

Seems you really are a talent. You actually don't know you can keep writing closure functions inside a function.

前进的搬砖er 5 likes

After optimizing and refactoring this, a week's workload can now be done in a day. That means one person can handle the work of five. Once the project runs stably for half a year, four people will be leaving by year-end. [dazed]

Tree1024 3 likes

Whoever changes it takes responsibility.

vishun 3 likes

Please, author, give me the AI prompt. I want to write similar articles too, otherwise no one reads them!

浪里个浪里小白龙 3 likes

Still too young, I can only say. Refactoring without getting approval from above is a major taboo. Never do this kind of thankless, unrewarding task... unless the boss or project lead gives the go-ahead.

泠沅 3 likes

I recently saw a blogger who is a company team lead write an article about why leaders are unwilling to upgrade the company's legacy projects, and now I see a newbie doing exactly that. If the project goes live without issues, fine, but if something goes wrong, your whole team is in for a scolding. If there are no problems, you probably won't get any bonus either, unless your boss is also a 'young person' who appreciates people.

薛定谔的玩具车 2 likes

People like this will get their comeuppance sooner or later. It's not that others can't make changes. It's that the boss pays low wages. You come in and upset the balance, and from now on all the work is yours.

用户438677710334 1 likes

Too idealistic.

斩了个月 1 likes

Too much free time with nothing to do.

大头 1 likes

A real talent.

真有你的呀 1 likes

Awesome [like]