引言:为什么需要翻拍经典老项目

在前端开发领域,jQuery 曾是统治 Web 开发十余年的王者。许多企业级应用、后台管理系统和门户网站至今仍运行着基于 jQuery 的遗留代码。这些项目通常具有以下特征:代码量庞大、依赖混乱、性能瓶颈明显、可维护性差。随着现代前端框架(如 React、Vue、Angular)的兴起,以及 ES6+ 标准的普及,翻拍这些老项目已成为技术升级的必然选择。

翻拍老项目并非简单的代码迁移,而是一次技术架构的重构。通过引入现代前端技术,我们可以显著提升应用的性能、可维护性和开发体验。更重要的是,这个过程可以帮助团队逐步建立现代前端工程化体系,为未来的技术演进打下坚实基础。

一、前期准备:评估与规划

1.1 代码审计与依赖分析

在开始翻拍之前,必须对现有项目进行全面审计。首先,使用工具分析代码结构和依赖关系。

# 安装依赖分析工具
npm install -g dependency-check madge

# 分析项目依赖
dependency-check package.json --detective precinct

# 生成模块依赖图
madge --extensions js --image graph.svg src/

通过这些工具,我们可以清晰地看到:

  • 项目依赖的 jQuery 版本及插件
  • 模块间的引用关系
  • 代码复杂度分布
  • 潜在的循环依赖

1.2 性能基准测试

在重构前建立性能基准至关重要。使用 Lighthouse 或 WebPageTest 进行全面测试:

// 使用 Puppeteer 进行自动化性能测试
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');

async function runAudit(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  // 收集性能指标
  const metrics = await page.metrics();
  const performance = await lighthouse(url, {
    port: browser.wsPort(),
    output: 'json'
  });
  
  await browser.close();
  return { metrics, performance };
}

runAudit('http://localhost:3000').then(console.log);

重点关注以下指标:

  • 首次内容绘制 (FCP)
  • 最大内容绘制 (LCP)
  • 累积布局偏移 (CLS)
  • 总阻塞时间 (TTB)
  • DOM 节点数量
  • JavaScript 执行时间

1.3 制定迁移策略

根据项目规模和团队能力,选择合适的迁移策略:

策略 A:完全重写

  • 适用于代码质量极差、业务逻辑混乱的项目
  • 风险高,但收益最大

策略 B:渐进式重构

  • 将 jQuery 代码逐步替换为原生 JavaScript 或现代框架
  • 风险低,适合大型项目

策略 C:混合架构

  • 保留部分 jQuery 代码,新功能使用现代框架
  • 适合需要持续迭代的项目

二、技术选型:现代前端技术栈

2.1 框架选择

根据项目特点选择合适的现代框架:

React 适合场景:

  • 复杂交互的单页应用
  • 需要组件化开发
  • 团队熟悉 JSX 语法

Vue 适合场景:

  • 渐进式迁移
  • 模板语法与 jQuery 的 HTML 操作习惯相近
  • 学习曲线平缓

原生 JavaScript + Web Components

  • 项目规模较小
  • 希望保持轻量级
  • 避免框架锁定

2.2 构建工具升级

现代构建工具能显著提升开发效率和性能:

// package.json 示例
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^5.0.0",
    "@vitejs/plugin-react": "^4.2.0",
    "typescript": "^5.2.0"
  }
}

Vite 配置示例:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'es2015',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          ui: ['@headlessui/react', '@heroicons/react']
        }
      }
    }
  }
});

2.3 TypeScript 迁移

为遗留代码添加类型系统是提升可维护性的关键步骤:

// jQuery 代码
function updateUserList(users) {
  $('#user-list').empty();
  users.forEach(function(user) {
    $('#user-list').append(
      `<div class="user-item" data-id="${user.id}">${user.name}</div>`
    );
  });
}

// TypeScript 重构后
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

class UserListManager {
  private container: HTMLElement;
  
  constructor(containerId: string) {
    const el = document.getElementById(containerId);
    if (!el) throw new Error(`Container ${containerId} not found`);
    this.container = el;
  }
  
  public render(users: User[]): void {
    this.container.innerHTML = '';
    users.forEach(user => {
      const userEl = document.createElement('div');
      userEl.className = 'user-item';
      userEl.dataset.id = user.id.toString();
      userEl.textContent = user.name;
      this.container.appendChild(userEl);
    });
  }
  
  public getUserById(id: number): User | undefined {
    const userEl = this.container.querySelector(`[data-id="${id}"]`);
    if (!userEl) return undefined;
    // 返回实际数据逻辑...
    return undefined;
  }
}

三、jQuery 到现代 JavaScript 的转换模式

3.1 选择器转换

jQuery 的 $() 选择器是最常用的 API,现代浏览器提供了强大的 querySelectorquerySelectorAll

jQuery 代码:

// 基础选择器
const $header = $('#header');
const $buttons = $('.btn-primary');
const $form = $('form#login');

// 层级选择器
const $links = $('nav a[target="_blank"]');

// 过滤选择器
const $evenRows = $('table tr:even');
const $firstChild = $('ul > li:first-child');

现代 JavaScript 转换:

// 基础选择器 - 单个元素
const header = document.getElementById('header');
const buttons = document.querySelectorAll('.btn-primary');
const form = document.querySelector('form#login');

// 层级选择器
const links = document.querySelectorAll('nav a[target="_blank"]');

// 过滤选择器 - 需要手动实现
const table = document.querySelector('table');
const evenRows = table ? Array.from(table.querySelectorAll('tr')).filter((_, index) => index % 2 === 0) : [];

const ul = document.querySelector('ul');
const firstChild = ul ? ul.querySelector(':scope > li:first-child') : null;

// 辅助函数封装
const $ = (selector, context = document) => context.querySelector(selector);
const $$ = (selector, context = document) => Array.from(context.querySelectorAll(selector));

// 使用示例
const $header = $('#header');
const $buttons = $$('.btn-primary');

3.2 DOM 操作转换

jQuery 的链式 DOM 操作非常方便,但现代 API 更加语义化且性能更好。

jQuery 代码:

// 创建和插入元素
const $newDiv = $('<div>', {
  class: 'alert alert-success',
  text: '操作成功!',
  css: { color: 'green', fontWeight: 'bold' }
});

$('#container').append($newDiv);

// 事件绑定
$('#btn-save').on('click', function(e) {
  e.preventDefault();
  const data = $('#form').serialize();
  $.ajax({
    url: '/api/save',
    method: 'POST',
    data: data,
    success: function(response) {
      $('#result').html(`<p>${response.message}</p>`);
    }
  });
});

// 类名操作
$('#element').addClass('active').removeClass('hidden').toggleClass('selected');

现代 JavaScript 转换:

// 创建和插入元素
const newDiv = document.createElement('div');
newDiv.className = 'alert alert-success';
newDiv.textContent = '操作成功!';
newDiv.style.color = 'green';
newDiv.style.fontWeight = 'bold';

const container = document.getElementById('container');
container?.appendChild(newDiv);

// 事件绑定 - 使用事件委托模式
document.addEventListener('click', function(e) {
  if (e.target.matches('#btn-save')) {
    e.preventDefault();
    
    const form = document.getElementById('form');
    const formData = new FormData(form);
    const data = Object.fromEntries(formData.entries());
    
    fetch('/api/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    .then(response => response.json())
    .then(result => {
      const resultDiv = document.getElementById('result');
      if (resultDiv) {
        resultDiv.innerHTML = `<p>${result.message}</p>`;
      }
    })
    .catch(error => console.error('Error:', error));
  }
});

// 类名操作 - 使用 classList API
const element = document.getElementById('element');
if (element) {
  element.classList.add('active');
  element.classList.remove('hidden');
  element.classList.toggle('selected');
}

// 封装工具函数
class DOMHelper {
  static addClass(selector, className) {
    const el = document.querySelector(selector);
    if (el) el.classList.add(className);
    return el;
  }
  
  static removeClass(selector, className) {
    const el = document.querySelector(selector);
    if (el) el.classList.remove(className);
    return el;
  }
  
  static toggleClass(selector, className) {
    const el = document.querySelector(selector);
    if (el) el.classList.toggle(className);
    return el;
  }
  
  static html(selector, content) {
    const el = document.querySelector(selector);
    if (el) el.innerHTML = content;
    return el;
  }
}

3.3 AJAX 请求转换

jQuery 的 $.ajax 是最常用的异步请求方法,现代浏览器提供了 fetch API。

jQuery 代码:

// GET 请求
$.get('/api/users', { page: 1, limit: 10 }, function(data) {
  console.log(data);
}).fail(function(error) {
  console.error('Error:', error);
});

// POST 请求
$.ajax({
  url: '/api/user',
  method: 'POST',
  data: JSON.stringify({ name: 'John', email: 'john@example.com' }),
  contentType: 'application/json',
  success: function(response) {
    console.log('User created:', response);
  },
  error: function(xhr, status, error) {
    console.error('Request failed:', status, error);
  },
  complete: function() {
    console.log('Request completed');
  }
});

// 全局配置
$.ajaxSetup({
  timeout: 5000,
  headers: { 'X-CSRF-Token': 'abc123' }
});

现代 JavaScript 转换:

// 封装 fetch 工具
class API {
  static baseURL = '/api';
  
  static async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const defaultOptions = {
      timeout: 5000,
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
      }
    };
    
    const config = { ...defaultOptions, ...options };
    
    // 超时处理
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), config.timeout);
    
    try {
      const response = await fetch(url, {
        ...config,
        signal: controller.signal,
        body: config.body ? JSON.stringify(config.body) : undefined
      });
      
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new Error('Request timeout');
      }
      throw error;
    }
  }
  
  static get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return this.request(url, { method: 'GET' });
  }
  
  static post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: data
    });
  }
}

// 使用示例
// GET 请求
API.get('/users', { page: 1, limit: 10 })
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// POST 请求
API.post('/user', { name: 'John', email: 'john@example.com' })
  .then(response => console.log('User created:', response))
  .catch(error => console.error('Request failed:', error))
  .finally(() => console.log('Request completed'));

3.4 事件处理转换

jQuery 的事件系统非常强大,但现代 API 提供了更好的性能和更清晰的 API。

jQuery 代码:

// 基础事件绑定
$('#btn').on('click', function() {
  console.log('Clicked');
});

// 事件委托
$('#list').on('click', '.item', function(e) {
  console.log('Item clicked:', $(this).data('id'));
});

// 一次性事件
$('#btn-once').one('click', function() {
  console.log('This will only fire once');
});

// 自定义事件
$('#element').on('customEvent', function(e, data) {
  console.log('Custom event triggered with:', data);
});

$('#element').trigger('customEvent', { message: 'Hello' });

// 事件对象扩展
$('#form').on('submit', function(e) {
  e.preventDefault();
  e.stopPropagation();
  // 自定义逻辑...
});

现代 JavaScript 转换:

// 基础事件绑定
const btn = document.getElementById('btn');
btn?.addEventListener('click', function() {
  console.log('Clicked');
});

// 事件委托 - 使用事件冒泡
const list = document.getElementById('list');
list?.addEventListener('click', function(e) {
  if (e.target.matches('.item')) {
    console.log('Item clicked:', e.target.dataset.id);
  }
});

// 一次性事件
const btnOnce = document.getElementById('btn-once');
btnOnce?.addEventListener('click', function handler() {
  console.log('This will only fire once');
  btnOnce.removeEventListener('click', handler);
});

// 自定义事件 - 使用 CustomEvent
const element = document.getElementById('element');
element?.addEventListener('customEvent', function(e) {
  console.log('Custom event triggered with:', e.detail);
});

// 触发自定义事件
const event = new CustomEvent('customEvent', {
  detail: { message: 'Hello' },
  bubbles: true,
  cancelable: true
});
element?.dispatchEvent(event);

// 事件对象处理
const form = document.getElementById('form');
form?.addEventListener('submit', function(e) {
  e.preventDefault();
  e.stopPropagation();
  // 自定义逻辑...
});

// 封装事件委托工具
class EventHelper {
  static delegate(context, selector, eventType, handler) {
    context.addEventListener(eventType, function(e) {
      if (e.target.matches(selector)) {
        handler.call(e.target, e);
      }
    });
  }
  
  static once(element, eventType, handler) {
    const wrappedHandler = function(e) {
      handler.call(this, e);
      element.removeEventListener(eventType, wrappedHandler);
    };
    element.addEventListener(eventType, wrappedHandler);
  }
  
  static trigger(element, eventName, detail = {}) {
    const event = new CustomEvent(eventName, {
      detail,
      bubbles: true,
      cancelable: true
    });
    element.dispatchEvent(event);
  }
}

3.5 动画与效果转换

jQuery 的 animate() 方法非常流行,现代 CSS 和 Web Animations API 提供了更好的性能。

jQuery 代码:

// 基础动画
$('#box').animate({
  left: '250px',
  opacity: 0.5,
  height: '150px'
}, 1000, function() {
  console.log('Animation complete');
});

// 淡入淡出
$('#element').fadeIn(300);
$('#element').fadeOut(300);
$('#element').fadeToggle(300);

// 滑动效果
$('#panel').slideDown(500);
$('#panel').slideUp(500);

// 队列动画
$('#box')
  .animate({ left: '100px' }, 200)
  .animate({ top: '100px' }, 200)
  .animate({ left: '0px' }, 200)
  .animate({ top: '0px' }, 200);

现代 JavaScript 转换:

// 使用 CSS Transitions(推荐)
// CSS
/*
.box {
  transition: all 1s ease;
  left: 0;
  opacity: 1;
  height: 100px;
}

.box.animated {
  left: 250px;
  opacity: 0.5;
  height: 150px;
}
*/

// JavaScript
const box = document.getElementById('box');
box?.classList.add('animated');

// 监听过渡结束
box?.addEventListener('transitionend', function() {
  console.log('Animation complete');
});

// 使用 Web Animations API
const keyframes = [
  { left: '0px', opacity: 1, height: '100px' },
  { left: '250px', opacity: 0.5, height: '150px' }
];

const options = {
  duration: 1000,
  easing: 'ease',
  fill: 'forwards'
};

box?.animate(keyframes, options).onfinish = () => {
  console.log('Animation complete');
};

// 淡入淡出 - 使用 CSS
/*
.fade-in {
  animation: fadeIn 0.3s ease forwards;
}

.fade-out {
  animation: fadeOut 0.3s ease forwards;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}
*/

const element = document.getElementById('element');
element?.classList.add('fade-in');

// 滑动效果 - 使用 CSS
/*
.slide-down {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease;
}

.slide-down.open {
  max-height: 500px; /* 根据实际内容调整 */
}
*/

const panel = document.getElementById('panel');
panel?.classList.add('slide-down');
setTimeout(() => panel.classList.add('open'), 10);

// 队列动画 - 使用 Web Animations API
const box2 = document.getElementById('box2');
if (box2) {
  const animations = [
    { left: '100px' },
    { top: '100px' },
    { left: '0px' },
    { top: '0px' }
  ];
  
  let promise = Promise.resolve();
  animations.forEach((keyframe, index) => {
    promise = promise.then(() => {
      return box2.animate([keyframe], { duration: 200, fill: 'forwards' }).finished;
    });
  });
}

四、现代框架迁移实战

4.1 Vue 3 迁移方案

Vue 3 的 Composition API 非常适合从 jQuery 迁移,因为它提供了更好的逻辑组织方式。

jQuery 项目结构:

project/
├── index.html
├── css/
│   └── style.css
├── js/
│   ├── app.js
│   ├── utils.js
│   └── plugins/
│       └── custom-plugin.js
└── lib/
    └── jquery-3.6.0.min.js

Vue 3 迁移步骤:

  1. 安装 Vue 3 和构建工具
npm init -y
npm install vue@next
npm install -D vite @vitejs/plugin-vue
  1. 创建 Vue 组件
<!-- src/components/UserList.vue -->
<template>
  <div class="user-list-container">
    <div class="header">
      <h2>用户管理</h2>
      <button @click="refreshUsers" class="btn btn-primary">刷新</button>
    </div>
    
    <div v-if="loading" class="loading">加载中...</div>
    
    <ul v-else class="user-list">
      <li 
        v-for="user in users" 
        :key="user.id"
        :class="{ active: selectedUser?.id === user.id }"
        @click="selectUser(user)"
      >
        <span class="user-name">{{ user.name }}</span>
        <span class="user-email">{{ user.email }}</span>
        <span class="user-role" :class="user.role">{{ user.role }}</span>
      </li>
    </ul>
    
    <div v-if="error" class="error">{{ error }}</div>
    
    <div class="pagination">
      <button 
        @click="prevPage" 
        :disabled="currentPage === 1"
      >
        上一页
      </button>
      <span>第 {{ currentPage }} 页</span>
      <button 
        @click="nextPage" 
        :disabled="currentPage >= totalPages"
      >
        下一页
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue';

// 响应式状态
const users = ref([]);
const loading = ref(false);
const error = ref('');
const selectedUser = ref(null);
const currentPage = ref(1);
const pageSize = ref(10);

// 计算属性
const totalPages = computed(() => {
  return Math.ceil(users.value.length / pageSize.value);
});

// 方法
const fetchUsers = async () => {
  loading.value = true;
  error.value = '';
  
  try {
    const response = await fetch(`/api/users?page=${currentPage.value}&limit=${pageSize.value}`);
    if (!response.ok) throw new Error('请求失败');
    
    const data = await response.json();
    users.value = data.users;
  } catch (err) {
    error.value = err.message;
    console.error('获取用户失败:', err);
  } finally {
    loading.value = false;
  }
};

const selectUser = (user) => {
  selectedUser.value = user;
  // 触发自定义事件
  emit('user-selected', user);
};

const refreshUsers = () => {
  fetchUsers();
};

const prevPage = () => {
  if (currentPage.value > 1) {
    currentPage.value--;
  }
};

const nextPage = () => {
  if (currentPage.value < totalPages.value) {
    currentPage.value++;
  }
};

// 生命周期钩子
onMounted(() => {
  fetchUsers();
});

// 监听器
watch(currentPage, () => {
  fetchUsers();
});

// 定义 emits
const emit = defineEmits(['user-selected']);
</script>

<style scoped>
.user-list-container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.user-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.user-list li {
  padding: 12px;
  border: 1px solid #ddd;
  margin-bottom: 8px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.user-list li:hover {
  background-color: #f5f5f5;
}

.user-list li.active {
  background-color: #e3f2fd;
  border-color: #2196f3;
}

.user-name {
  font-weight: bold;
  margin-right: 10px;
}

.user-email {
  color: #666;
  margin-right: 10px;
}

.user-role {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.user-role.admin {
  background-color: #ffeb3b;
  color: #333;
}

.user-role.user {
  background-color: #e0e0e0;
  color: #333;
}

.loading, .error {
  padding: 20px;
  text-align: center;
  color: #666;
}

.error {
  color: #f44336;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-top: 20px;
}

.pagination button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 4px;
}

.pagination button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  background-color: #2196f3;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary:hover {
  background-color: #1976d2;
}
</style>
  1. 主应用文件
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);
app.mount('#app');
<!-- src/App.vue -->
<template>
  <div id="app">
    <UserList @user-selected="handleUserSelected" />
    
    <div v-if="selectedUser" class="user-detail">
      <h3>选中用户详情</h3>
      <p><strong>ID:</strong> {{ selectedUser.id }}</p>
      <p><strong>姓名:</strong> {{ selectedUser.name }}</p>
      <p><strong>邮箱:</strong> {{ selectedUser.email }}</p>
      <p><strong>角色:</strong> {{ selectedUser.role }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import UserList from './components/UserList.vue';

const selectedUser = ref(null);

const handleUserSelected = (user) => {
  selectedUser.value = user;
};
</script>

<style>
#app {
  font-family: Arial, sans-serif;
  min-height: 100vh;
}

.user-detail {
  margin-top: 30px;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 8px;
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}
</style>
  1. Vite 配置
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'assets'
  }
});

4.2 React 迁移方案

React 的组件化思想适合复杂的交互逻辑,特别是需要大量状态管理的场景。

React 迁移示例:

// src/components/UserManager.jsx
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import './UserManager.css';

// 自定义 Hook - 封装数据获取逻辑
const useUsers = (page, pageSize) => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(`/api/users?page=${page}&limit=${pageSize}`);
        if (!response.ok) throw new Error('请求失败');
        
        const data = await response.json();
        setUsers(data.users);
        setTotal(data.total);
      } catch (err) {
        setError(err.message);
        console.error('获取用户失败:', err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, [page, pageSize]);

  return { users, loading, error, total };
};

// 用户列表组件
const UserList = ({ users, selectedUser, onUserSelect, loading, error }) => {
  if (loading) return <div className="loading">加载中...</div>;
  if (error) return <div className="error">{error}</div>;
  if (users.length === 0) return <div className="empty">暂无数据</div>;

  return (
    <ul className="user-list">
      {users.map(user => (
        <li
          key={user.id}
          className={`user-item ${selectedUser?.id === user.id ? 'active' : ''}`}
          onClick={() => onUserSelect(user)}
        >
          <span className="user-name">{user.name}</span>
          <span className="user-email">{user.email}</span>
          <span className={`user-role ${user.role}`}>{user.role}</span>
        </li>
      ))}
    </ul>
  );
};

// 分页组件
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
  const handlePrev = useCallback(() => {
    if (currentPage > 1) onPageChange(currentPage - 1);
  }, [currentPage, onPageChange]);

  const handleNext = useCallback(() => {
    if (currentPage < totalPages) onPageChange(currentPage + 1);
  }, [currentPage, totalPages, onPageChange]);

  return (
    <div className="pagination">
      <button onClick={handlePrev} disabled={currentPage === 1}>
        上一页
      </button>
      <span>第 {currentPage} 页</span>
      <button onClick={handleNext} disabled={currentPage >= totalPages}>
        下一页
      </button>
    </div>
  );
};

// 主组件
const UserManager = () => {
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize] = useState(10);
  const [selectedUser, setSelectedUser] = useState(null);

  // 使用自定义 Hook
  const { users, loading, error, total } = useUsers(currentPage, pageSize);

  // 计算总页数
  const totalPages = useMemo(() => {
    return Math.ceil(total / pageSize);
  }, [total, pageSize]);

  // 处理用户选择
  const handleUserSelect = useCallback((user) => {
    setSelectedUser(user);
  }, []);

  // 刷新用户列表
  const handleRefresh = useCallback(() => {
    setCurrentPage(prev => prev); // 触发 useEffect 重新执行
  }, []);

  return (
    <div className="user-manager">
      <div className="header">
        <h2>用户管理</h2>
        <button onClick={handleRefresh} className="btn btn-primary">
          刷新
        </button>
      </div>

      <UserList
        users={users}
        selectedUser={selectedUser}
        onUserSelect={handleUserSelect}
        loading={loading}
        error={error}
      />

      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
      />

      {selectedUser && (
        <div className="user-detail">
          <h3>选中用户详情</h3>
          <p><strong>ID:</strong> {selectedUser.id}</p>
          <p><strong>姓名:</strong> {selectedUser.name}</p>
          <p><strong>邮箱:</strong> {selectedUser.email}</p>
          <p><strong>角色:</strong> {selectedUser.role}</p>
        </div>
      )}
    </div>
  );
};

export default UserManager;
/* src/components/UserManager.css */
.user-manager {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
  font-family: Arial, sans-serif;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.header h2 {
  margin: 0;
  color: #333;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s;
}

.btn-primary {
  background-color: #2196f3;
  color: white;
}

.btn-primary:hover {
  background-color: #1976d2;
}

.user-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.user-item {
  padding: 12px;
  border: 1px solid #ddd;
  margin-bottom: 8px;
  cursor: pointer;
  transition: background-color 0.2s;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.user-item:hover {
  background-color: #f5f5f5;
}

.user-item.active {
  background-color: #e3f2fd;
  border-color: #2196f3;
}

.user-name {
  font-weight: bold;
  flex: 0 0 auto;
  margin-right: 10px;
}

.user-email {
  color: #666;
  flex: 1 1 auto;
  margin-right: 10px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.user-role {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
  flex: 0 0 auto;
}

.user-role.admin {
  background-color: #ffeb3b;
  color: #333;
}

.user-role.user {
  background-color: #e0e0e0;
  color: #333;
}

.loading, .error, .empty {
  padding: 20px;
  text-align: center;
  color: #666;
}

.error {
  color: #f44336;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-top: 20px;
}

.pagination button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 4px;
}

.pagination button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.user-detail {
  margin-top: 30px;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 8px;
  border: 1px solid #ddd;
}

.user-detail h3 {
  margin-top: 0;
  color: #333;
}

.user-detail p {
  margin: 8px 0;
  color: #555;
}
// src/App.jsx
import React from 'react';
import UserManager from './components/UserManager';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="app-header">
        <h1>现代前端应用 - 用户管理系统</h1>
      </header>
      <main>
        <UserManager />
      </main>
    </div>
  );
}

export default App;
/* src/App.css */
.App {
  min-height: 100vh;
  background-color: #f5f5f5;
}

.app-header {
  background-color: #1976d2;
  color: white;
  padding: 20px;
  text-align: center;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.app-header h1 {
  margin: 0;
  font-size: 24px;
}

main {
  padding: 20px;
}
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

4.3 原生 JavaScript + Web Components 方案

对于希望保持轻量级且避免框架锁定的项目,可以使用原生 JavaScript 和 Web Components。

Web Components 实现:

// src/components/UserList.js
class UserList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.users = [];
    this.selectedUser = null;
    this.loading = false;
    this.error = null;
  }

  static get observedAttributes() {
    return ['api-url', 'page-size'];
  }

  connectedCallback() {
    this.render();
    this.loadUsers();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (name === 'api-url' || name === 'page-size') {
        this.loadUsers();
      }
    }
  }

  async loadUsers() {
    this.loading = true;
    this.error = null;
    this.render();

    const apiUrl = this.getAttribute('api-url') || '/api/users';
    const pageSize = this.getAttribute('page-size') || 10;

    try {
      const response = await fetch(`${apiUrl}?limit=${pageSize}`);
      if (!response.ok) throw new Error('请求失败');
      
      const data = await response.json();
      this.users = data.users || [];
      this.loading = false;
      this.render();
    } catch (err) {
      this.error = err.message;
      this.loading = false;
      this.render();
      console.error('获取用户失败:', err);
    }
  }

  selectUser(user) {
    this.selectedUser = user;
    this.render();
    
    // 触发自定义事件
    this.dispatchEvent(new CustomEvent('user-selected', {
      detail: user,
      bubbles: true,
      composed: true
    }));
  }

  refresh() {
    this.loadUsers();
  }

  render() {
    const styles = `
      :host {
        display: block;
        padding: 20px;
        max-width: 800px;
        margin: 0 auto;
        font-family: Arial, sans-serif;
      }
      
      .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20px;
      }
      
      .header h2 {
        margin: 0;
        color: #333;
      }
      
      .btn {
        padding: 8px 16px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        transition: background-color 0.2s;
      }
      
      .btn-primary {
        background-color: #2196f3;
        color: white;
      }
      
      .btn-primary:hover {
        background-color: #1976d2;
      }
      
      .user-list {
        list-style: none;
        padding: 0;
        margin: 0;
      }
      
      .user-item {
        padding: 12px;
        border: 1px solid #ddd;
        margin-bottom: 8px;
        cursor: pointer;
        transition: background-color 0.2s;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      
      .user-item:hover {
        background-color: #f5f5f5;
      }
      
      .user-item.active {
        background-color: #e3f2fd;
        border-color: #2196f3;
      }
      
      .user-name {
        font-weight: bold;
        flex: 0 0 auto;
        margin-right: 10px;
      }
      
      .user-email {
        color: #666;
        flex: 1 1 auto;
        margin-right: 10px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      
      .user-role {
        padding: 2px 8px;
        border-radius: 4px;
        font-size: 12px;
        flex: 0 0 auto;
      }
      
      .user-role.admin {
        background-color: #ffeb3b;
        color: #333;
      }
      
      .user-role.user {
        background-color: #e0e0e0;
        color: #333;
      }
      
      .loading, .error, .empty {
        padding: 20px;
        text-align: center;
        color: #666;
      }
      
      .error {
        color: #f44336;
      }
    `;

    let content = '';

    if (this.loading) {
      content = '<div class="loading">加载中...</div>';
    } else if (this.error) {
      content = `<div class="error">${this.error}</div>`;
    } else if (this.users.length === 0) {
      content = '<div class="empty">暂无数据</div>';
    } else {
      content = `
        <ul class="user-list">
          ${this.users.map(user => `
            <li 
              class="user-item ${this.selectedUser?.id === user.id ? 'active' : ''}"
              data-id="${user.id}"
            >
              <span class="user-name">${user.name}</span>
              <span class="user-email">${user.email}</span>
              <span class="user-role ${user.role}">${user.role}</span>
            </li>
          `).join('')}
        </ul>
      `;
    }

    this.shadowRoot.innerHTML = `
      <style>${styles}</style>
      <div class="header">
        <h2>用户管理</h2>
        <button class="btn btn-primary" id="refresh-btn">刷新</button>
      </div>
      ${content}
    `;

    // 绑定事件
    const refreshBtn = this.shadowRoot.getElementById('refresh-btn');
    if (refreshBtn) {
      refreshBtn.addEventListener('click', () => this.refresh());
    }

    // 绑定列表项点击事件
    const listItems = this.shadowRoot.querySelectorAll('.user-item');
    listItems.forEach(item => {
      item.addEventListener('click', (e) => {
        const userId = parseInt(e.currentTarget.dataset.id);
        const user = this.users.find(u => u.id === userId);
        if (user) this.selectUser(user);
      });
    });
  }
}

// 注册自定义元素
customElements.define('user-list', UserList);

// src/components/UserDetail.js
class UserDetail extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.user = null;
  }

  static get observedAttributes() {
    return ['user'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'user' && oldValue !== newValue) {
      try {
        this.user = newValue ? JSON.parse(newValue) : null;
        this.render();
      } catch (e) {
        console.error('解析用户数据失败:', e);
      }
    }
  }

  render() {
    const styles = `
      :host {
        display: block;
        margin-top: 30px;
        padding: 20px;
        background: #f9f9f9;
        border-radius: 8px;
        border: 1px solid #ddd;
        max-width: 800px;
        margin-left: auto;
        margin-right: auto;
      }
      
      :host([hidden]) {
        display: none;
      }
      
      h3 {
        margin-top: 0;
        color: #333;
      }
      
      p {
        margin: 8px 0;
        color: #555;
      }
      
      strong {
        color: #333;
      }
    `;

    if (!this.user) {
      this.shadowRoot.innerHTML = '';
      return;
    }

    this.shadowRoot.innerHTML = `
      <style>${styles}</style>
      <h3>选中用户详情</h3>
      <p><strong>ID:</strong> ${this.user.id}</p>
      <p><strong>姓名:</strong> ${this.user.name}</p>
      <p><strong>邮箱:</strong> ${this.user.email}</p>
      <p><strong>角色:</strong> ${this.user.role}</p>
    `;
  }
}

customElements.define('user-detail', UserDetail);
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Components 用户管理系统</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      background-color: #f5f5f5;
      min-height: 100vh;
      font-family: Arial, sans-serif;
    }
    
    header {
      background-color: #1976d2;
      color: white;
      padding: 20px;
      text-align: center;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    
    header h1 {
      margin: 0;
      font-size: 24px;
    }
    
    main {
      padding: 20px;
    }
  </style>
</head>
<body>
  <header>
    <h1>Web Components 用户管理系统</h1>
  </header>
  
  <main>
    <user-list 
      api-url="/api/users" 
      page-size="10"
      id="user-list"
    ></user-list>
    
    <user-detail id="user-detail" hidden></user-detail>
  </main>

  <script type="module" src="./src/components/UserList.js"></script>
  <script type="module" src="./src/components/UserDetail.js"></script>
  
  <script>
    // 主应用逻辑
    document.addEventListener('DOMContentLoaded', () => {
      const userList = document.getElementById('user-list');
      const userDetail = document.getElementById('user-detail');

      // 监听用户选择事件
      userList.addEventListener('user-selected', (e) => {
        const user = e.detail;
        userDetail.setAttribute('user', JSON.stringify(user));
        userDetail.removeAttribute('hidden');
      });
    });
  </script>
</body>
</html>

五、性能优化策略

5.1 代码分割与懒加载

jQuery 项目的问题:

// 所有代码打包在一个文件中
// app.js - 500KB+
$(document).ready(function() {
  // 初始化所有模块
  initDashboard();
  initUserManagement();
  initReports();
  initSettings();
  // ... 更多初始化
});

现代优化方案:

// 动态导入 - 基于路由或功能
const routes = {
  '/dashboard': () => import('./modules/dashboard.js'),
  '/users': () => import('./modules/users.js'),
  '/reports': () => import('./modules/reports.js'),
  '/settings': () => import('./modules/settings.js')
};

// 路由切换时懒加载
async function navigate(path) {
  const loader = routes[path];
  if (!loader) {
    console.error('Route not found');
    return;
  }
  
  // 显示加载状态
  showLoading();
  
  try {
    const module = await loader();
    module.default(); // 执行模块初始化
    hideLoading();
  } catch (error) {
    console.error('Failed to load module:', error);
    hideLoading();
  }
}

// Vue 3 中的懒加载
const UserList = () => import('./components/UserList.vue');
const UserDetail = () => import('./components/UserDetail.vue');

const routes = [
  { path: '/users', component: UserList },
  { path: '/users/:id', component: UserDetail }
];

5.2 虚拟列表优化

对于大量数据渲染,虚拟列表是必备优化:

// 虚拟列表实现
class VirtualList {
  constructor(container, itemHeight, totalItems, renderItem) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.totalItems = totalItems;
    this.renderItem = renderItem;
    this.visibleCount = 0;
    this.scrollTop = 0;
    
    this.init();
  }
  
  init() {
    this.container.style.overflow = 'auto';
    this.container.style.position = 'relative';
    
    // 创建占位容器
    this.placeholder = document.createElement('div');
    this.placeholder.style.height = `${this.totalItems * this.itemHeight}px`;
    this.container.appendChild(this.placeholder);
    
    // 创建可见区域容器
    this.viewport = document.createElement('div');
    this.viewport.style.position = 'absolute';
    this.viewport.style.top = '0';
    this.viewport.style.left = '0';
    this.viewport.style.right = '0';
    this.container.appendChild(this.viewport);
    
    // 监听滚动
    this.container.addEventListener('scroll', this.handleScroll.bind(this));
    
    // 初始渲染
    this.render();
  }
  
  handleScroll() {
    this.scrollTop = this.container.scrollTop;
    this.render();
  }
  
  render() {
    const containerHeight = this.container.clientHeight;
    this.visibleCount = Math.ceil(containerHeight / this.itemHeight) + 2; // +2 for buffer
    
    const startIndex = Math.floor(this.scrollTop / this.itemHeight);
    const endIndex = Math.min(startIndex + this.visibleCount, this.totalItems);
    
    // 清空并重新渲染
    this.viewport.innerHTML = '';
    this.viewport.style.top = `${startIndex * this.itemHeight}px`;
    
    for (let i = startIndex; i < endIndex; i++) {
      const item = this.renderItem(i);
      item.style.height = `${this.itemHeight}px`;
      item.style.boxSizing = 'border-box';
      this.viewport.appendChild(item);
    }
  }
  
  update(totalItems) {
    this.totalItems = totalItems;
    this.placeholder.style.height = `${totalItems * this.itemHeight}px`;
    this.render();
  }
}

// 使用示例
const container = document.getElementById('list-container');
const virtualList = new VirtualList(
  container,
  50, // 每项高度
  10000, // 总数据量
  (index) => {
    const div = document.createElement('div');
    div.textContent = `Item ${index}`;
    div.style.borderBottom = '1px solid #eee';
    div.style.padding = '10px';
    return div;
  }
);

Vue 3 虚拟列表组件:

<template>
  <div 
    ref="container" 
    class="virtual-list"
    @scroll="handleScroll"
  >
    <div :style="{ height: totalHeight + 'px' }" class="placeholder"></div>
    <div class="viewport" :style="{ top: viewportTop + 'px' }">
      <div 
        v-for="item in visibleItems" 
        :key="item.index"
        class="list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item.data" :index="item.index"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue';

const props = defineProps({
  items: { type: Array, required: true },
  itemHeight: { type: Number, default: 50 },
  buffer: { type: Number, default: 2 }
});

const container = ref(null);
const scrollTop = ref(0);

const totalHeight = computed(() => props.items.length * props.itemHeight);

const visibleCount = computed(() => {
  if (!container.value) return 0;
  return Math.ceil(container.value.clientHeight / props.itemHeight) + props.buffer * 2;
});

const startIndex = computed(() => {
  return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer);
});

const endIndex = computed(() => {
  return Math.min(props.items.length, startIndex.value + visibleCount.value);
});

const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, endIndex.value).map((item, i) => ({
    data: item,
    index: startIndex.value + i
  }));
});

const viewportTop = computed(() => startIndex.value * props.itemHeight);

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop;
};

onMounted(() => {
  if (container.value) {
    scrollTop.value = container.value.scrollTop;
  }
});

watch(() => props.items, () => {
  scrollTop.value = 0;
  if (container.value) {
    container.value.scrollTop = 0;
  }
});
</script>

<style scoped>
.virtual-list {
  position: relative;
  overflow: auto;
  height: 400px; /* 固定高度 */
  border: 1px solid #ddd;
}

.placeholder {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
}

.viewport {
  position: absolute;
  left: 0;
  right: 0;
}

.list-item {
  box-sizing: border-box;
  border-bottom: 1px solid #eee;
}
</style>

5.3 防抖与节流

jQuery 时代:

// 手动实现或使用插件
$(window).on('resize', debounce(function() {
  // 处理窗口调整
}, 250));

function debounce(func, wait) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), wait);
  };
}

现代实现:

// 工具库
class EventThrottler {
  // 防抖 - 事件停止触发后执行
  static debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
  }
  
  // 节流 - 事件触发时限制执行频率
  static throttle(fn, limit) {
    let inThrottle;
    let lastExecTime = 0;
    
    return function(...args) {
      const now = Date.now();
      
      if (!inThrottle) {
        fn.apply(this, args);
        lastExecTime = now;
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      } else if (now - lastExecTime >= limit) {
        fn.apply(this, args);
        lastExecTime = now;
      }
    };
  }
  
  // 立即执行版防抖
  static debounceLeading(fn, delay) {
    let timeoutId;
    let lastExecTime = 0;
    
    return function(...args) {
      const now = Date.now();
      const elapsed = now - lastExecTime;
      
      clearTimeout(timeoutId);
      
      if (elapsed > delay) {
        fn.apply(this, args);
        lastExecTime = now;
      } else {
        timeoutId = setTimeout(() => {
          fn.apply(this, args);
          lastExecTime = Date.now();
        }, delay);
      }
    };
  }
}

// 使用示例
// 搜索框实时搜索
const searchInput = document.getElementById('search');
searchInput?.addEventListener('input', EventThrottler.debounce(async (e) => {
  const query = e.target.value;
  if (query.length < 2) return;
  
  const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
  renderSearchResults(results);
}, 300));

// 窗口调整优化
window.addEventListener('resize', EventThrottler.throttle(() => {
  console.log('窗口大小已改变');
  // 重新计算布局等
}, 200));

// 滚动加载更多
const scrollContainer = document.getElementById('scroll-container');
scrollContainer?.addEventListener('scroll', EventThrottler.debounce(() => {
  if (scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 100) {
    loadMoreData();
  }
}, 150));

5.4 Web Workers 处理复杂计算

jQuery 时代:

// 长时间阻塞主线程
function processData(data) {
  const result = [];
  for (let i = 0; i < data.length; i++) {
    // 复杂计算
    const processed = heavyCalculation(data[i]);
    result.push(processed);
  }
  return result;
}

现代 Web Workers:

// worker.js - 在独立线程运行
self.addEventListener('message', function(e) {
  const { data, type } = e.data;
  
  if (type === 'process') {
    const result = data.map(item => heavyCalculation(item));
    self.postMessage({ type: 'result', result });
  }
});

function heavyCalculation(item) {
  // 模拟复杂计算
  let sum = 0;
  for (let i = 0; i < 10000; i++) {
    sum += Math.sqrt(item.value * i);
  }
  return { ...item, processed: sum };
}
// 主线程使用
class WorkerManager {
  constructor() {
    this.worker = new Worker('worker.js');
    this.pendingRequests = new Map();
    
    this.worker.onmessage = (e) => {
      const { type, result, requestId } = e.data;
      if (type === 'result' && this.pendingRequests.has(requestId)) {
        const { resolve, reject } = this.pendingRequests.get(requestId);
        resolve(result);
        this.pendingRequests.delete(requestId);
      }
    };
    
    this.worker.onerror = (error) => {
      console.error('Worker error:', error);
    };
  }
  
  process(data) {
    return new Promise((resolve, reject) => {
      const requestId = Math.random().toString(36).substr(2, 9);
      this.pendingRequests.set(requestId, { resolve, reject });
      
      this.worker.postMessage({
        type: 'process',
        data,
        requestId
      });
      
      // 超时处理
      setTimeout(() => {
        if (this.pendingRequests.has(requestId)) {
          this.pendingRequests.delete(requestId);
          reject(new Error('Worker timeout'));
        }
      }, 5000);
    });
  }
  
  terminate() {
    this.worker.terminate();
  }
}

// 使用示例
const workerManager = new WorkerManager();

// 处理大量数据而不阻塞UI
async function handleLargeDataset() {
  const largeData = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    value: Math.random() * 100
  }));
  
  try {
    const result = await workerManager.process(largeData);
    console.log('Processing complete:', result.length);
    // 更新UI
    updateUI(result);
  } catch (error) {
    console.error('Processing failed:', error);
  }
}

5.5 缓存策略

jQuery 时代:

// 简单的内存缓存
const cache = {};
function getCachedData(url) {
  if (cache[url]) {
    return Promise.resolve(cache[url]);
  }
  return $.get(url).then(data => {
    cache[url] = data;
    return data;
  });
}

现代缓存方案:

// 多层缓存策略
class CacheManager {
  constructor() {
    this.memoryCache = new Map();
    this.sessionCache = window.sessionStorage;
    this.localCache = window.localStorage;
  }
  
  // 内存缓存(最快)
  setMemory(key, value, ttl = 60000) {
    const item = {
      value,
      expiry: Date.now() + ttl
    };
    this.memoryCache.set(key, item);
  }
  
  getMemory(key) {
    const item = this.memoryCache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiry) {
      this.memoryCache.delete(key);
      return null;
    }
    
    return item.value;
  }
  
  // Session Storage(页面会话级)
  setSession(key, value, ttl = 3600000) {
    const item = {
      value,
      expiry: Date.now() + ttl
    };
    this.sessionCache.setItem(key, JSON.stringify(item));
  }
  
  getSession(key) {
    const itemStr = this.sessionCache.getItem(key);
    if (!itemStr) return null;
    
    const item = JSON.parse(itemStr);
    if (Date.now() > item.expiry) {
      this.sessionCache.removeItem(key);
      return null;
    }
    
    return item.value;
  }
  
  // Local Storage(持久化)
  setLocal(key, value, ttl = 86400000) {
    const item = {
      value,
      expiry: Date.now() + ttl
    };
    this.localCache.setItem(key, JSON.stringify(item));
  }
  
  getLocal(key) {
    const itemStr = this.localCache.getItem(key);
    if (!itemStr) return null;
    
    const item = JSON.parse(itemStr);
    if (Date.now() > item.expiry) {
      this.localCache.removeItem(key);
      return null;
    }
    
    return item.value;
  }
  
  // 统一获取接口(按优先级)
  get(key, options = {}) {
    const { source = ['memory', 'session', 'local'] } = options;
    
    for (const src of source) {
      let value = null;
      switch (src) {
        case 'memory':
          value = this.getMemory(key);
          break;
        case 'session':
          value = this.getSession(key);
          break;
        case 'local':
          value = this.getLocal(key);
          break;
      }
      if (value !== null) return value;
    }
    
    return null;
  }
  
  // 统一设置接口
  set(key, value, options = {}) {
    const { 
      ttl = 60000, 
      storage = ['memory', 'session'] 
    } = options;
    
    storage.forEach(src => {
      switch (src) {
        case 'memory':
          this.setMemory(key, value, ttl);
          break;
        case 'session':
          this.setSession(key, value, ttl);
          break;
        case 'local':
          this.setLocal(key, value, ttl);
          break;
      }
    });
  }
  
  // 清理过期数据
  cleanup() {
    const now = Date.now();
    
    // 清理内存缓存
    for (const [key, item] of this.memoryCache) {
      if (now > item.expiry) {
        this.memoryCache.delete(key);
      }
    }
    
    // 清理Session Storage
    for (let i = 0; i < this.sessionCache.length; i++) {
      const key = this.sessionCache.key(i);
      const itemStr = this.sessionCache.getItem(key);
      if (itemStr) {
        const item = JSON.parse(itemStr);
        if (now > item.expiry) {
          this.sessionCache.removeItem(key);
        }
      }
    }
    
    // 清理Local Storage
    for (let i = 0; i < this.localCache.length; i++) {
      const key = this.localCache.key(i);
      const itemStr = this.localCache.getItem(key);
      if (itemStr) {
        const item = JSON.parse(itemStr);
        if (now > item.expiry) {
          this.localCache.removeItem(key);
        }
      }
    }
  }
}

// API 缓存封装
class CachedAPI {
  constructor(cacheManager) {
    this.cache = cacheManager;
    this.baseURL = '/api';
  }
  
  async get(endpoint, params = {}, options = {}) {
    const { ttl = 300000, useCache = true } = options;
    
    // 生成缓存键
    const cacheKey = `GET:${endpoint}:${JSON.stringify(params)}`;
    
    // 尝试从缓存读取
    if (useCache) {
      const cached = this.cache.get(cacheKey);
      if (cached !== null) {
        console.log('Cache hit:', cacheKey);
        return cached;
      }
    }
    
    // 发起请求
    const queryString = new URLSearchParams(params).toString();
    const url = `${this.baseURL}${endpoint}${queryString ? '?' + queryString : ''}`;
    
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      const data = await response.json();
      
      // 存入缓存
      this.cache.set(cacheKey, data, { ttl, storage: ['memory', 'session'] });
      
      return data;
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
  
  async post(endpoint, data, options = {}) {
    const { useCache = false } = options;
    
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    
    const result = await response.json();
    
    // POST 成功后清理相关缓存
    if (useCache) {
      this.cache.cleanup();
    }
    
    return result;
  }
}

// 使用示例
const cacheManager = new CacheManager();
const api = new CachedAPI(cacheManager);

// 定期清理过期缓存
setInterval(() => {
  cacheManager.cleanup();
}, 60000); // 每分钟清理一次

// 使用缓存的API调用
async function loadUserData() {
  try {
    const users = await api.get('/users', { page: 1, limit: 10 }, { ttl: 300000 });
    console.log('Users loaded:', users);
    return users;
  } catch (error) {
    console.error('Failed to load users:', error);
  }
}

// 第一次调用会请求服务器
await loadUserData();

// 第二次调用(5分钟内)会从缓存读取
await loadUserData();

六、工程化建设

6.1 现代构建流程

传统 jQuery 项目构建:

手动复制文件 → 手动压缩 → 手动上传

现代构建流程:

// package.json 脚本
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
    "test": "vitest",
    "type-check": "tsc --noEmit",
    "analyze": "npx vite-bundle-visualizer"
  }
}

Vite 配置(生产环境优化):

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => {
  const isProduction = mode === 'production';
  
  return {
    plugins: [
      // 根据项目选择插件
      vue(),
      // react(),
      
      // 打包分析
      visualizer({
        filename: './dist/stats.html',
        template: 'sunburst'
      })
    ],
    
    // 路径别名
    resolve: {
      alias: {
        '@': '/src',
        '@components': '/src/components',
        '@utils': '/src/utils'
      }
    },
    
    // 开发服务器
    server: {
      port: 3000,
      open: true,
      proxy: {
        '/api': {
          target: 'http://localhost:8080',
          changeOrigin: true,
          secure: false
        }
      }
    },
    
    // 构建配置
    build: {
      target: 'es2015',
      outDir: 'dist',
      assetsDir: 'assets',
      assetsInlineLimit: 4096, // 小于4KB的资源内联
      cssCodeSplit: true,
      rollupOptions: {
        output: {
          manualChunks: {
            // 拆分第三方库
            vendor: ['vue', 'react', 'react-dom'],
            ui: ['@headlessui/vue', '@heroicons/vue'],
            // 按业务拆分
            dashboard: ['./src/modules/dashboard'],
            users: ['./src/modules/users']
          }
        }
      },
      // 生产环境优化
      minify: 'terser',
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true
        }
      }
    },
    
    // CSS 预处理
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/variables.scss";`
        }
      }
    }
  };
});

6.2 代码规范与质量保证

ESLint 配置:

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parserOptions: {
    ecmaVersion: 2021,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  plugins: ['vue', 'react', '@typescript-eslint'],
  rules: {
    // 通用规则
    'no-console': isProduction ? 'warn' : 'off',
    'no-debugger': isProduction ? 'error' : 'off',
    'prefer-const': 'error',
    'no-var': 'error',
    
    // Vue 特定
    'vue/multi-word-component-names': 'off',
    'vue/no-v-html': 'off',
    
    // React 特定
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    
    // TypeScript 特定
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/explicit-module-boundary-types': 'off'
  },
  overrides: [
    {
      files: ['*.vue'],
      parser: 'vue-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser'
      }
    }
  ]
};

Prettier 配置:

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "always",
  "endOfLine": "lf"
}

Husky + lint-staged 提交前检查:

# 安装
npm install --save-dev husky lint-staged

# 初始化 husky
npx husky install

# 添加 pre-commit 钩子
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,less}": [
      "prettier --write"
    ],
    "*.md": [
      "prettier --write"
    ]
  }
}

6.3 自动化测试

单元测试(Vitest):

// src/utils/__tests__/cache.test.js
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { CacheManager } from '../cache';

describe('CacheManager', () => {
  let cache;
  
  beforeEach(() => {
    cache = new CacheManager();
    // 清除所有缓存
    localStorage.clear();
    sessionStorage.clear();
  });
  
  afterEach(() => {
    vi.useRealTimers();
  });
  
  it('应该正确存储和读取内存缓存', () => {
    const data = { user: 'John' };
    cache.setMemory('test-key', data, 1000);
    
    expect(cache.getMemory('test-key')).toEqual(data);
  });
  
  it('内存缓存应该过期', () => {
    vi.useFakeTimers();
    
    const data = { user: 'John' };
    cache.setMemory('test-key', data, 1000);
    
    // 快进时间
    vi.advanceTimersByTime(1001);
    
    expect(cache.getMemory('test-key')).toBeNull();
  });
  
  it('应该按优先级获取缓存', () => {
    const data = { user: 'John' };
    
    // 设置不同层级的缓存
    cache.setLocal('test-key', data, 5000);
    cache.setSession('test-key', data, 5000);
    cache.setMemory('test-key', data, 5000);
    
    // 应该优先返回内存缓存
    const result = cache.get('test-key', { source: ['memory', 'session', 'local'] });
    expect(result).toEqual(data);
  });
  
  it('应该清理过期的缓存', () => {
    vi.useFakeTimers();
    
    cache.setLocal('expired', { data: 'old' }, 1000);
    cache.setLocal('valid', { data: 'new' }, 5000);
    
    vi.advanceTimersByTime(1001);
    
    // 手动触发清理
    cache.cleanup();
    
    expect(cache.getLocal('expired')).toBeNull();
    expect(cache.getLocal('valid')).toEqual({ data: 'new' });
  });
});

端到端测试(Playwright):

// tests/user-management.spec.js
const { test, expect } = require('@playwright/test');

test.describe('用户管理功能', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('http://localhost:3000');
  });
  
  test('应该正确加载用户列表', async ({ page }) => {
    // 等待用户列表加载
    await expect(page.locator('.user-list')).toBeVisible();
    
    // 验证用户项存在
    const userItems = page.locator('.user-item');
    await expect(userItems).toHaveCount(10);
  });
  
  test('应该能够选择用户', async ({ page }) => {
    // 点击第一个用户
    await page.locator('.user-item').first().click();
    
    // 验证详情显示
    await expect(page.locator('.user-detail')).toBeVisible();
    await expect(page.locator('.user-detail')).toContainText('选中用户详情');
  });
  
  test('应该能够分页', async ({ page }) => {
    // 点击下一页
    await page.locator('button:has-text("下一页")').click();
    
    // 验证页码更新
    await expect(page.locator('text=第 2 页')).toBeVisible();
  });
  
  test('应该能够刷新数据', async ({ page }) => {
    // 点击刷新按钮
    await page.locator('button:has-text("刷新")').click();
    
    // 验证加载状态
    await expect(page.locator('.loading')).toBeVisible();
    await expect(page.locator('.loading')).toBeHidden();
  });
});

七、迁移后的性能对比

7.1 性能指标对比

迁移前(jQuery 项目):

  • 首次内容绘制 (FCP): 2.3s
  • 最大内容绘制 (LCP): 4.1s
  • 累积布局偏移 (CLS): 0.25
  • 总阻塞时间 (TTB): 850ms
  • JavaScript 文件大小: 1.2MB (未压缩)
  • DOM 节点数: 2500+
  • 内存占用: 150MB

迁移后(Vue 3 + 优化):

  • 首次内容绘制 (FCP): 0.8s (提升65%)
  • 最大内容绘制 (LCP): 1.5s (提升63%)
  • 累积布局偏移 (CLS): 0.02 (提升92%)
  • 总阻塞时间 (TTB): 120ms (提升86%)
  • JavaScript 文件大小: 250KB (gzip后,提升79%)
  • DOM 节点数: 800 (优化68%)
  • 内存占用: 45MB (提升70%)

7.2 代码质量提升

代码行数对比:

  • jQuery: 15,000 行
  • Vue 3: 8,500 行 (减少43%)
  • React: 9,200 行 (减少39%)

维护性指标:

  • 圈复杂度: 从平均 15 降至 5
  • 代码重复率: 从 25% 降至 3%
  • 测试覆盖率: 从 0% 提升至 85%

八、最佳实践与注意事项

8.1 迁移过程中的陷阱

1. 事件冒泡处理不当

// ❌ 错误:事件委托选择器不准确
document.body.addEventListener('click', function(e) {
  if (e.target.className === 'btn') { // 可能匹配不到
    // ...
  }
});

// ✅ 正确:使用 matches 方法
document.body.addEventListener('click', function(e) {
  if (e.target.matches('.btn')) {
    // ...
  }
});

2. 异步处理不当

// ❌ 错误:忽略 Promise 拒绝
fetch('/api/data').then(data => {
  // 处理数据
});

// ✅ 正确:添加错误处理
fetch('/api/data')
  .then(response => {
    if (!response.ok) throw new Error('请求失败');
    return response.json();
  })
  .then(data => {
    // 处理数据
  })
  .catch(error => {
    console.error('Error:', error);
    // 显示错误提示
  });

3. 内存泄漏

// ❌ 错误:未移除事件监听器
class Component {
  constructor() {
    window.addEventListener('resize', this.handleResize);
  }
  
  handleResize() {
    // ...
  }
  
  // 没有销毁方法
}

// ✅ 正确:提供销毁方法
class Component {
  constructor() {
    this.handleResize = this.handleResize.bind(this);
    window.addEventListener('resize', this.handleResize);
  }
  
  handleResize() {
    // ...
  }
  
  destroy() {
    window.removeEventListener('resize', this.handleResize);
  }
}

8.2 渐进式迁移策略

阶段 1:基础设施升级

  • 升级构建工具
  • 添加 TypeScript 支持
  • 建立代码规范

阶段 2:核心功能迁移

  • 迁移数据层(API 调用)
  • 迁移状态管理
  • 迁移核心业务逻辑

阶段 3:UI 组件迁移

  • 按页面逐步迁移
  • 保持新旧代码共存
  • 使用微前端方案过渡

阶段 4:优化与清理

  • 移除 jQuery 依赖
  • 性能优化
  • 添加测试覆盖

8.3 团队协作建议

  1. 文档化: 详细记录迁移过程中的决策和遇到的问题
  2. 代码审查: 每个 PR 必须经过严格审查
  3. 知识共享: 定期组织技术分享会
  4. 风险控制: 使用特性开关(Feature Flags)控制新功能上线

九、总结

翻拍 jQuery 老项目是一次技术升级的绝佳机会。通过引入现代前端技术,我们不仅提升了应用的性能和可维护性,还为团队建立了现代化的开发流程。

关键收获:

  1. 性能提升: 通过代码分割、懒加载、虚拟列表等技术,显著改善用户体验
  2. 可维护性: TypeScript 和组件化开发让代码更易理解和维护
  3. 开发效率: 现代构建工具和热重载大幅提升开发体验
  4. 团队成长: 迁移过程是团队学习现代前端技术的最好实践

建议:

  • 不要追求一次性完美,采用渐进式迁移策略
  • 优先迁移性能瓶颈最大的部分
  • 建立完善的测试体系,确保迁移质量
  • 保持与业务方的沟通,确保功能一致性

通过本文提供的详细指南和代码示例,相信你已经掌握了翻拍 jQuery 老项目的核心技术和方法。现在就开始行动,让你的遗留代码重获新生!