【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?

刚进入前端领域的前端开发用jQuery还是Vue3?

前言:从"微观管理"到"宏观架构"的思维跃迁

文章摘要

2026 年,构建仿桌面应用 等复杂交互项目时,是否还在 jQuery 与 Vue3 之间犹豫?本文从架构师视角给出明确答案:Vue3 是现代复杂应用的唯一正解 ,而 jQuery 更适合轻量脚本或老项目维护。

文章深度对比命令式与声明式编程范式,揭示"不自由"感背后的思维鸿沟,并提供一套从 jQuery 到 Vue3 的平滑迁移策略 ,涵盖渐进式引入、插件替换及代码重构实例。

阅读本文,你将获得清晰的技术选型依据、可落地的代码对比示例,以及一套经实战验证的学习路径,在 2026 年前端浪潮中做出明智选择。

作为一名在前端领域摸爬滚打多年、带过不少团队的架构师,我深刻理解许多开发者从 jQuery 转向 Vue3 时所经历的阵痛。那种"失去自由、感觉处处受限"的困惑,几乎是每一位初学者必经的关卡。本文旨在为你拨开迷雾,不仅回答"选哪个"的问题,更深入剖析其背后的思维差异,并通过实战案例,为你指明一条清晰的学习与破局之路。

核心结论先行:在 2026 年的今天,如果你要构建一个"仿 Windows/macOS/Android 桌面"这类复杂交互应用,请毫不犹豫地选择 Vue3。 jQuery 的时代使命已悄然转变,它更适合维护老项目或编写轻量脚本,而 Vue3 等现代框架,才是驾驭复杂工程、实现"架构级自由"的利器。

jQuery vs. Vue3 在仿桌面应用开发中的核心差异对比

维度 jQuery Vue3
核心范式 命令式 (Imperative) 开发者直接操作 DOM,通过 $('#id').show().addClass() 等指令一步步描述"如何做"。 声明式 (Declarative) 开发者声明"数据与视图的绑定关系",Vue 自动根据数据变化更新视图。开发者关注"是什么",而非"如何做"。
状态管理 手动同步 状态分散在 DOM 属性、全局变量或自定义对象中,需要开发者手动保证状态与 UI 同步,极易出错。 响应式数据驱动 通过 refreactivecomputed 等构建响应式状态,状态变更自动触发视图更新,同步由框架保证。
代码组织 过程式脚本 代码通常按"事件监听 → DOM 操作 → 状态更新"的线性流程组织,随着功能增加,代码迅速变得冗长、耦合。 组件化架构 应用被拆分为高内聚、低耦合的组件(.vue 文件),每个组件包含模板、逻辑、样式,便于复用和维护。
学习曲线 入门极快,精通难 语法简单,可快速实现简单交互。但面对复杂应用时,缺乏架构指导,容易写出"面条代码",维护成本陡增。 初期有门槛,长期收益高 需要理解响应式、组件生命周期、组合式 API 等概念,初期学习曲线较陡。但一旦掌握,开发复杂应用的效率和可维护性远超 jQuery。
适用场景 轻量脚本、老项目维护、快速原型 适合添加简单交互、操作现有 DOM、或在不便引入现代框架的环境中使用。 复杂交互应用、单页应用 (SPA)、仿桌面系统 尤其适合状态复杂、交互密集、需要高度组件化与团队协作的中大型项目。

文章概括

本文将从一个架构师的视角,为你系统性地拆解 jQuery 与 Vue3 的本质区别,并论证为何 Vue3 是仿桌面项目的唯一正解:

  1. 深度思维剖析 :揭示你觉得 Vue "难且不自由"的根本原因------命令式编程与声明式编程的思维鸿沟,以及"复杂度守恒定律"在不同阶段的体现。
  2. 实战案例对比:通过"实现一个可拖拽的桌面窗口"这一具体功能,直观对比 jQuery(命令式,直接操作 DOM)与 Vue3(声明式,数据驱动)在代码结构、可维护性和扩展性上的天壤之别。
  3. 架构师选型建议 :从复杂状态管理、组件化复用、成熟生态三个维度,论证为何仿桌面这类"复杂状态机"项目必须选择 Vue3。
  4. 初学者破局指南:提供四条具体、可操作的学习建议,帮助你戒除"直接操作 DOM"的旧习,拥抱 Vue3 的现代开发范式,平稳度过新手期。

无论你是正在技术选型中犹豫,还是已在 Vue 学习中感到挫败,相信本文都能为你带来新的视角与切实的解决方案。

技术选型决策流程图

对于还在犹豫的开发者,下面这张决策流程图可以帮助你根据项目需求快速做出判断:
#mermaid-svg-3PZnu85UHINjLfAP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3PZnu85UHINjLfAP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3PZnu85UHINjLfAP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3PZnu85UHINjLfAP .error-icon{fill:#552222;}#mermaid-svg-3PZnu85UHINjLfAP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3PZnu85UHINjLfAP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3PZnu85UHINjLfAP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3PZnu85UHINjLfAP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3PZnu85UHINjLfAP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3PZnu85UHINjLfAP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3PZnu85UHINjLfAP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3PZnu85UHINjLfAP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3PZnu85UHINjLfAP .marker.cross{stroke:#333333;}#mermaid-svg-3PZnu85UHINjLfAP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3PZnu85UHINjLfAP p{margin:0;}#mermaid-svg-3PZnu85UHINjLfAP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3PZnu85UHINjLfAP .cluster-label text{fill:#333;}#mermaid-svg-3PZnu85UHINjLfAP .cluster-label span{color:#333;}#mermaid-svg-3PZnu85UHINjLfAP .cluster-label span p{background-color:transparent;}#mermaid-svg-3PZnu85UHINjLfAP .label text,#mermaid-svg-3PZnu85UHINjLfAP span{fill:#333;color:#333;}#mermaid-svg-3PZnu85UHINjLfAP .node rect,#mermaid-svg-3PZnu85UHINjLfAP .node circle,#mermaid-svg-3PZnu85UHINjLfAP .node ellipse,#mermaid-svg-3PZnu85UHINjLfAP .node polygon,#mermaid-svg-3PZnu85UHINjLfAP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3PZnu85UHINjLfAP .rough-node .label text,#mermaid-svg-3PZnu85UHINjLfAP .node .label text,#mermaid-svg-3PZnu85UHINjLfAP .image-shape .label,#mermaid-svg-3PZnu85UHINjLfAP .icon-shape .label{text-anchor:middle;}#mermaid-svg-3PZnu85UHINjLfAP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3PZnu85UHINjLfAP .rough-node .label,#mermaid-svg-3PZnu85UHINjLfAP .node .label,#mermaid-svg-3PZnu85UHINjLfAP .image-shape .label,#mermaid-svg-3PZnu85UHINjLfAP .icon-shape .label{text-align:center;}#mermaid-svg-3PZnu85UHINjLfAP .node.clickable{cursor:pointer;}#mermaid-svg-3PZnu85UHINjLfAP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3PZnu85UHINjLfAP .arrowheadPath{fill:#333333;}#mermaid-svg-3PZnu85UHINjLfAP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3PZnu85UHINjLfAP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3PZnu85UHINjLfAP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PZnu85UHINjLfAP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3PZnu85UHINjLfAP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PZnu85UHINjLfAP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3PZnu85UHINjLfAP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3PZnu85UHINjLfAP .cluster text{fill:#333;}#mermaid-svg-3PZnu85UHINjLfAP .cluster span{color:#333;}#mermaid-svg-3PZnu85UHINjLfAP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3PZnu85UHINjLfAP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3PZnu85UHINjLfAP rect.text{fill:none;stroke-width:0;}#mermaid-svg-3PZnu85UHINjLfAP .icon-shape,#mermaid-svg-3PZnu85UHINjLfAP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PZnu85UHINjLfAP .icon-shape p,#mermaid-svg-3PZnu85UHINjLfAP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3PZnu85UHINjLfAP .icon-shape .label rect,#mermaid-svg-3PZnu85UHINjLfAP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PZnu85UHINjLfAP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3PZnu85UHINjLfAP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3PZnu85UHINjLfAP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-3PZnu85UHINjLfAP .jQueryNode>*{fill:#fff3cd!important;color:#856404!important;stroke:#d89e2c!important;stroke-width:2px!important;}#mermaid-svg-3PZnu85UHINjLfAP .jQueryNode span{fill:#fff3cd!important;color:#856404!important;stroke:#d89e2c!important;stroke-width:2px!important;}#mermaid-svg-3PZnu85UHINjLfAP .jQueryNode tspan{fill:#856404!important;}#mermaid-svg-3PZnu85UHINjLfAP .vueNode>*{fill:#4a6fa5!important;color:#fff!important;stroke:#3a5f95!important;}#mermaid-svg-3PZnu85UHINjLfAP .vueNode span{fill:#4a6fa5!important;color:#fff!important;stroke:#3a5f95!important;}#mermaid-svg-3PZnu85UHINjLfAP .vueNode tspan{fill:#fff!important;}#mermaid-svg-3PZnu85UHINjLfAP .transitionNode>*{fill:#5cb85c!important;color:#fff!important;stroke:#4a9e4a!important;}#mermaid-svg-3PZnu85UHINjLfAP .transitionNode span{fill:#5cb85c!important;color:#fff!important;stroke:#4a9e4a!important;}#mermaid-svg-3PZnu85UHINjLfAP .transitionNode tspan{fill:#fff!important;} 项目复杂度较高

(多窗口、拖拽、实时数据)
项目复杂度较低

(简单页面、轻量脚本)
是,必须兼容 IE11
否,无需兼容 IE11
是,团队已有 Vue3 经验
否,且开发周期紧张
否,但有学习意愿和时间
团队积累 Vue3 经验后
项目复杂度随时间上升
开始:新项目技术选型
项目是否需要复杂交互?

(如仿桌面、SaaS后台、SPA)
推荐:Vue3 等现代框架
是否需要兼容老旧浏览器

(如 IE11)?
备选方案:jQuery 或原生 JS
团队是否熟悉 Vue3 生态?
备选方案:jQuery / 原生 JS 快速交付
渐进式迁移:先用 Vue3 做外围组件

简而言之:对于 2026 年构建仿桌面应用这类复杂项目,流程图会直接把你引向 Vue3;而 jQuery 仅在维护老项目或极简脚本时作为备选。

一、深度分析:为什么你觉得 Vue "好难,一点不自由"?

你感觉 Vue 不自由,本质上是因为你的编程思维还停留在 jQuery 时代,没有完成向"现代前端框架"的思维跃迁。

1. 命令式 (jQuery) vs 声明式 (Vue3)
  • jQuery 是"命令式"的 :你像是一个微观管理者。你要改变页面,必须亲自去"找到那个元素($('#box')),然后命令它改变(.css('left', '100px'))"。这非常符合人类直觉,初期极其"自由",指哪打哪。
  • Vue3 是"声明式"的 :你像是一个宏观架构师。你不需要亲自去操作 DOM,你只需要维护数据(状态),然后告诉 Vue:"当数据是 A 时,页面长这样;当数据是 B 时,页面长那样"。Vue 的响应式系统会自动帮你更新 DOM。

你觉得不自由,是因为你试图在 Vue 里继续做"微观管理者" 。当你试图在 Vue 里用 document.querySelector 或者 ref.value 去直接修改 DOM 的样式时,你会发现处处碰壁,因为 Vue 的虚拟 DOM 和响应式机制会覆盖你的手动修改。

在 Vue 里,最大的忌讳就是"直接操作 DOM"。

2. 复杂度守恒定律

jQuery 把复杂度留给了后期维护 ;Vue 把复杂度前置到了前期设计

仿桌面系统是一个极度复杂的项目,包含多窗口管理、拖拽、层级(z-index)控制、右键菜单、状态同步等。如果用 jQuery,前期写起来很爽,但代码量超过 2000 行后,就会变成一团无法维护的"意大利面条";Vue 前期需要设计组件、定义状态,感觉束手束脚,但后期扩展时你会感谢自己当初立的"规矩"。

二、案例对比:以"实现一个可拖拽的桌面窗口"为例

我们来看看两种思维在实现同一个功能时的差异。

❌ jQuery 思维(命令式:直接操作 DOM)
javascript 复制代码
// 找到窗口元素
let $window = $('#my-window');
let isDragging = false;
let offsetX, offsetY;

// 监听鼠标按下
$window.on('mousedown', function(e) {
    isDragging = true;
    offsetX = e.clientX - $window.offset().left;
    offsetY = e.clientY - $window.offset().top;
});

// 监听鼠标移动
$(document).on('mousemove', function(e) {
    if (isDragging) {
        // 直接修改 DOM 的 CSS
        $window.css({
            left: e.clientX - offsetX + 'px',
            top: e.clientY - offsetY + 'px'
        });
    }
});

// 监听鼠标松开
$(document).on('mouseup', function() {
    isDragging = false;
});

分析:代码直接耦合了 DOM 操作。如果此时窗口还需要记录位置到本地存储、或者限制拖拽边界,代码会迅速膨胀且难以复用。

✅ Vue3 思维(声明式:数据驱动 + 组合式 API)

在 Vue3 中,我们使用 <script setup> 和状态来驱动视图。

vue 复制代码
<template>
  <!-- 视图只负责绑定数据,不关心怎么计算 -->
  <div 
    class="window" 
    :style="{ left: x + 'px', top: y + 'px' }"
    @mousedown="startDrag"
  >
    我是窗口内容
  </div>
</template>

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

// 1. 定义状态(数据)
const x = ref(100);
const y = ref(100);
const isDragging = ref(false);
let offsetX = 0, offsetY = 0;

// 2. 定义行为(修改数据,而不是修改 DOM)
const startDrag = (e) => {
  isDragging.value = true;
  offsetX = e.clientX - x.value;
  offsetY = e.clientY - y.value;
};

const onMove = (e) => {
  if (isDragging.value) {
    x.value = e.clientX - offsetX; // 修改数据,Vue 会自动更新 DOM
    y.value = e.clientY - offsetY;
  }
};

const stopDrag = () => {
  isDragging.value = false;
};

// 3. 生命周期管理
onMounted(() => {
  window.addEventListener('mousemove', onMove);
  window.addEventListener('mouseup', stopDrag);
});

onUnmounted(() => {
  window.removeEventListener('mousemove', onMove);
  window.removeEventListener('mouseup', stopDrag);
});
</script>

分析:虽然代码看起来多了一点,但逻辑和视图彻底分离 了。xy 是纯粹的数据,你随时可以把它们保存到 Pinia(状态管理)中,或者同步到 LocalStorage,而不需要去操作 DOM。

(注:在实际的 Vue3 桌面项目中,我们甚至不需要手写这些事件,直接使用生态库 VueUseuseDraggable 一行代码就能搞定,这就是框架生态带来的自由。)

三、架构师建议:为什么"仿桌面项目"必须选 Vue3?

作为架构师,如果我接手你这个项目,我会从以下几个维度评估技术选型:

  1. 桌面系统的本质是"复杂的状态机"

    桌面系统不是简单的网页展示,它是一个操作系统。你需要管理:当前激活的窗口、所有打开的窗口列表、任务栏的状态、图标的排列、壁纸、主题等。

    • jQuery 处理这种全局状态会极其痛苦,你需要到处传递变量。
    • Vue3 + Pinia 天生就是为了解决复杂状态而生的。你可以轻松实现"在任务栏点击图标,对应窗口状态改变,视图自动更新"。
  2. 组件化与复用

    桌面系统有大量的重复 UI:各种 App 的窗口、图标、右键菜单、通知中心。

    Vue3 的组件化设计可以让你写一个 <WindowFrame> 组件,然后所有的应用(计算器、记事本、浏览器)都作为插槽(Slot)传进去,极大减少代码冗余。

  3. 2026 年的 Vue3 生态极其成熟

    现在的 Vue3 已经非常完善。特别是 VueUse 这个库,它提供了上百个开箱即用的组合式函数(Hooks)。比如做桌面项目必备的:

    • useDraggable (拖拽)
    • useMouse (鼠标位置)
    • useEventListener (事件监听)
    • useLocalStorage (本地持久化)
      有了这些,你会发现 Vue3 不仅不繁琐,反而比 jQuery 更快捷。

四、给初学者的破局与学习建议

五、从 jQuery 到 Vue3 的平滑迁移策略

对于已经熟悉 jQuery 的开发者来说,完全重写现有项目可能不现实。幸运的是,Vue3 提供了渐进式迁移的路径,让你可以逐步将 jQuery 项目现代化,而不是一次性推倒重来。以下是一套具体的迁移策略:

1. 在现有 jQuery 项目中逐步引入 Vue3 组件

策略:从外围到核心,逐步替换

不要试图一次性重写整个应用。相反,从最独立、最不复杂的部分开始:

  1. 安装 Vue3:在现有项目中通过 CDN 或 npm 安装 Vue3。

    html 复制代码
    <!-- 通过 CDN 引入 -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  2. 创建第一个 Vue 组件:选择一个独立的 UI 部件(如导航栏、侧边栏、弹窗)作为试点。

    javascript 复制代码
    // 在现有 HTML 文件中创建 Vue 应用
    const { createApp } = Vue;
    
    const NavBar = {
      template: `
        <nav class="navbar">
          <a v-for="item in menuItems" :href="item.link">{{ item.text }}</a>
        </nav>
      `,
      data() {
        return {
          menuItems: [
            { text: '首页', link: '/' },
            { text: '关于', link: '/about' },
            { text: '联系', link: '/contact' }
          ]
        };
      }
    };
    
    // 挂载到现有页面的某个容器
    createApp(NavBar).mount('#vue-navbar');
  3. 逐步扩展:成功迁移一个组件后,继续迁移其他独立组件,逐步扩大 Vue 的"领地"。

2. 将 jQuery 插件替换为 Vue3 生态组件

jQuery 生态中有大量插件,但 Vue3 生态通常有更好的替代方案:

示例:日期选择器迁移

jQuery 方式(使用 jQuery UI Datepicker):

html 复制代码
<!-- HTML -->
<input type="text" id="datepicker">

<!-- JavaScript -->
<script>
$(function() {
  $("#datepicker").datepicker({
    dateFormat: "yy-mm-dd",
    onSelect: function(dateText) {
      console.log("选择的日期: " + dateText);
    }
  });
});
</script>

Vue3 方式(使用 Vue Datepicker 组件):

  1. 安装 Vue3 日期选择器库

    bash 复制代码
    npm install @vuepic/vue-datepicker
  2. 创建 Vue 组件

    vue 复制代码
    <!-- DatePicker.vue -->
    <template>
      <VueDatePicker
        v-model="selectedDate"
        :format="'yyyy-MM-dd'"
        @update:model-value="handleDateSelect"
      />
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import VueDatePicker from '@vuepic/vue-datepicker';
    import '@vuepic/vue-datepicker/dist/main.css';
    
    const selectedDate = ref(new Date());
    
    const handleDateSelect = (date) => {
      console.log('选择的日期:', date);
      // 可以在这里触发其他业务逻辑
    };
    </script>

其他常见插件替代方案:

  • jQuery Validation → VeeValidate / Vue Formulate
  • jQuery UI Dialog → Element Plus Dialog / PrimeVue Dialog
  • jQuery DataTables → PrimeVue DataTable / Vuetify Data Tables
  • Select2 → Vue Select / Element Plus Select
3. 重构典型的 jQuery 事件绑定与 DOM 操作代码

让我们通过一个具体的例子来对比:一个带搜索和列表的页面。

jQuery 实现(命令式):

html 复制代码
<!-- HTML -->
<div id="app">
  <input type="text" id="search" placeholder="搜索用户...">
  <button id="add-btn">添加用户</button>
  <ul id="user-list"></ul>
</div>

<script>
$(document).ready(function() {
  let users = ['张三', '李四', '王五'];
  
  // 初始化列表
  function renderList() {
    $('#user-list').empty();
    users.forEach(function(user) {
      $('#user-list').append('<li>' + user + '</li>');
    });
  }
  
  // 搜索功能
  $('#search').on('input', function() {
    const keyword = $(this).val().toLowerCase();
    const filtered = users.filter(function(user) {
      return user.toLowerCase().includes(keyword);
    });
    
    $('#user-list').empty();
    filtered.forEach(function(user) {
      $('#user-list').append('<li>' + user + '</li>');
    });
  });
  
  // 添加用户
  $('#add-btn').on('click', function() {
    const newUser = prompt('请输入新用户姓名:');
    if (newUser) {
      users.push(newUser);
      renderList();
    }
  });
  
  // 初始渲染
  renderList();
});
</script>

Vue3 实现(声明式响应式):

vue 复制代码
<!-- UserList.vue -->
<template>
  <div class="app">
    <input 
      type="text" 
      v-model="searchKeyword" 
      placeholder="搜索用户..."
      @input="filterUsers"
    >
    <button @click="addUser">添加用户</button>
    
    <ul>
      <li v-for="user in filteredUsers" :key="user">
        {{ user }}
      </li>
    </ul>
  </div>
</template>

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

// 1. 定义响应式数据(状态)
const users = ref(['张三', '李四', '王五']);
const searchKeyword = ref('');

// 2. 使用计算属性自动过滤用户列表
const filteredUsers = computed(() => {
  if (!searchKeyword.value) {
    return users.value;
  }
  return users.value.filter(user => 
    user.toLowerCase().includes(searchKeyword.value.toLowerCase())
  );
});

// 3. 定义方法(修改数据,而不是 DOM)
const addUser = () => {
  const newUser = prompt('请输入新用户姓名:');
  if (newUser) {
    users.value.push(newUser);
  }
};

// 4. 不再需要手动渲染列表!
// Vue 会自动根据 filteredUsers 的变化更新 DOM
</script>

<style scoped>
.app {
  padding: 20px;
}

input {
  padding: 8px;
  margin-right: 10px;
  width: 200px;
}

button {
  padding: 8px 16px;
  background: #4a6fa5;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background: #3a5f95;
}

ul {
  margin-top: 20px;
  list-style: none;
  padding: 0;
}

li {
  padding: 8px;
  border-bottom: 1px solid #eee;
}
</style>
迁移步骤总结
  1. 识别可独立组件化的部分:找出项目中功能独立、耦合度低的 UI 组件。
  2. 创建 Vue 单文件组件 :将 jQuery 的 HTML 结构、CSS 样式和 JavaScript 逻辑整合到 .vue 文件中。
  3. 数据驱动思维转换
    • 将 jQuery 中的变量声明改为 ref()reactive()
    • 将 DOM 操作改为数据绑定(v-modelv-bindv-for
    • 将事件监听改为 @click@input 等指令
  4. 逐步替换:每次迁移一个组件,确保功能正常后再继续下一个。
  5. 状态管理升级:当多个组件需要共享状态时,引入 Pinia 替代全局变量。
  6. 最终移除 jQuery:当所有核心功能都迁移到 Vue3 后,可以安全地移除 jQuery 依赖。
迁移中的常见问题与解决方案

问题1:jQuery 插件依赖特定 DOM 结构

  • 解决方案 :使用 Vue 的 ref 获取 DOM 元素,在 onMounted 生命周期中初始化插件,在 onUnmounted 中销毁。

问题2:现有代码大量使用 $.ajax

  • 解决方案 :逐步替换为 axiosfetch,Vue3 可以很好地与这些现代 HTTP 客户端配合。

问题3:第三方库只支持 jQuery

  • 解决方案:创建包装组件,在组件内部使用 jQuery 初始化库,对外暴露 Vue 友好的接口。
迁移收益
  1. 代码可维护性大幅提升:组件化让代码结构清晰,易于理解和修改。
  2. 开发效率提高:响应式系统减少了大量手动 DOM 操作代码。
  3. 性能优化:Vue 的虚拟 DOM 和智能更新策略比 jQuery 的直接 DOM 操作更高效。
  4. 团队协作更顺畅:Vue 的标准化开发模式降低了新人上手成本。

记住:迁移不是一蹴而就的。可以从一个小功能开始,逐步积累经验,最终完成整个项目的现代化改造。这种渐进式迁移既能保证项目稳定运行,又能让你逐步掌握 Vue3 的开发模式。

既然决定了用 Vue3,如何度过你觉得"难且不自由"的新手期?我建议你按照以下步骤调整:

1. 戒掉"直接操作 DOM"的瘾

给自己定一个规矩:在 Vue 中,绝对不允许使用 document.getElementByIddocument.querySelector 去修改样式或内容。 所有的改变,都必须通过修改 refreactive 中的数据来实现。熬过这个思维转换期,你会豁然开朗。

2. 拥抱 <script setup> 和 Composition API

不要去看老旧的 Options API(data, methods, computed 分开写的那种)教程。直接学习 Vue3 最新的 <script setup> 语法糖。它更符合现代编程习惯,代码更紧凑,逻辑复用(自定义 Hooks)极其方便。

3. 学会"拆解"项目(架构思维)

不要一上来就写一个巨大的 index.html。把桌面系统拆解为组件树:

  • Desktop.vue (最外层容器,处理壁纸和图标网格)
    • DesktopIcon.vue (单个图标,处理双击打开、右键菜单)
    • WindowManager.vue (窗口管理器,处理 z-index 层级和窗口状态)
      • Window.vue (窗口外壳,处理拖拽、缩放、最大化/最小化)
        • AppCalculator.vue / AppNotes.vue (具体的应用内容)
4. 引入状态管理 (Pinia)

当你的项目超过 3 个窗口时,就必须引入 Pinia。把"窗口列表"、"当前激活窗口 ID"放在全局 Store 中。你会体会到数据驱动带来的巨大爽感。

五、总结

作为初学者,你觉得 jQuery 自由,是因为你承担了所有的复杂性 ;你觉得 Vue 不自由,是因为框架用"规矩"帮你接管了复杂性

在 2026 年的前端工程化体系下,jQuery 已经退居幕后,主要用于维护老项目或编写极其简单的独立小脚本。而 Vue3、React 等现代框架,才是构建复杂交互应用(如仿桌面系统、SaaS 平台、大型后台)的绝对主力。

六、实战进阶:用 Vue3 + VueUse 快速构建窗口管理器

前面我们讨论了 Vue3 的理论优势,现在让我们通过一个实际的代码示例,看看如何用 Vue3 + VueUse + Pinia 快速构建一个功能完整的桌面窗口管理器。这个示例将展示:

  1. 使用 Vue3 的 <script setup> 语法
  2. 使用 Pinia 进行全局状态管理
  3. 使用 VueUse 的 useDraggable 实现拖拽功能
  4. 使用 VueUse 的 useStorage 实现状态持久化
  5. 完整的窗口管理功能(打开、关闭、激活、拖拽)
1. Pinia Store 定义:管理窗口状态

首先,我们创建一个 Pinia Store 来管理所有窗口的全局状态:

javascript 复制代码
// stores/windowStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useWindowStore = defineStore('window', () => {
  // 所有窗口的列表
  const windows = ref([]);
  
  // 当前激活的窗口 ID
  const activeWindowId = ref(null);
  
  // 下一个可用的窗口 ID
  let nextWindowId = 1;
  
  // 打开新窗口
  const openWindow = (title = '新窗口', content = '窗口内容') => {
    const newWindow = {
      id: nextWindowId++,
      title,
      content,
      x: Math.random() * 200, // 随机初始位置
      y: Math.random() * 200,
      width: 400,
      height: 300,
      zIndex: windows.value.length + 1, // 层级管理
      isMinimized: false
    };
    
    windows.value.push(newWindow);
    activeWindowId.value = newWindow.id;
    return newWindow.id;
  };
  
  // 关闭窗口
  const closeWindow = (id) => {
    const index = windows.value.findIndex(w => w.id === id);
    if (index !== -1) {
      windows.value.splice(index, 1);
      
      // 如果关闭的是当前激活窗口,激活最后一个窗口
      if (activeWindowId.value === id) {
        activeWindowId.value = windows.value.length > 0 
          ? windows.value[windows.value.length - 1].id 
          : null;
      }
    }
  };
  
  // 激活窗口(点击窗口时调用)
  const activateWindow = (id) => {
    const window = windows.value.find(w => w.id === id);
    if (window) {
      // 将激活窗口的 zIndex 设为最高
      const maxZIndex = Math.max(...windows.value.map(w => w.zIndex));
      window.zIndex = maxZIndex + 1;
      activeWindowId.value = id;
    }
  };
  
  // 更新窗口位置(拖拽时调用)
  const updateWindowPosition = (id, x, y) => {
    const window = windows.value.find(w => w.id === id);
    if (window) {
      window.x = x;
      window.y = y;
    }
  };
  
  // 获取当前激活的窗口
  const activeWindow = computed(() => 
    windows.value.find(w => w.id === activeWindowId.value)
  );
  
  return {
    windows,
    activeWindowId,
    openWindow,
    closeWindow,
    activateWindow,
    updateWindowPosition,
    activeWindow
  };
});
2. Window.vue 组件:可拖拽的窗口外壳

接下来,我们创建窗口组件,使用 VueUse 的 useDraggable 实现拖拽功能:

vue 复制代码
<!-- components/Window.vue -->
<template>
  <div 
    class="window"
    :class="{ 'active': isActive }"
    :style="{
      left: `${x}px`,
      top: `${y}px`,
      width: `${width}px`,
      height: `${height}px`,
      zIndex: zIndex
    }"
    @mousedown="handleActivate"
    ref="windowEl"
  >
    <!-- 窗口标题栏 -->
    <div class="window-header" ref="headerEl">
      <span class="window-title">{{ title }}</span>
      <div class="window-controls">
        <button class="window-btn minimize" @click="toggleMinimize">−</button>
        <button class="window-btn close" @click="handleClose">×</button>
      </div>
    </div>
    
    <!-- 窗口内容 -->
    <div class="window-content" v-show="!isMinimized">
      <slot>{{ content }}</slot>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useDraggable } from '@vueuse/core';
import { useWindowStore } from '@/stores/windowStore';

const props = defineProps({
  id: Number,
  title: String,
  content: String,
  x: Number,
  y: Number,
  width: Number,
  height: Number,
  zIndex: Number,
  isMinimized: Boolean
});

const emit = defineEmits(['update:position', 'close', 'activate']);

const windowStore = useWindowStore();

// 使用 VueUse 的 useDraggable 实现拖拽
const windowEl = ref(null);
const headerEl = ref(null);

const { x, y, style } = useDraggable(windowEl, {
  initialValue: { x: props.x, y: props.y },
  handle: headerEl, // 只能通过标题栏拖拽
  onMove: (position) => {
    // 拖拽时更新 store 中的位置
    windowStore.updateWindowPosition(props.id, position.x, position.y);
  }
});

// 计算当前窗口是否激活
const isActive = computed(() => windowStore.activeWindowId.value === props.id);

// 激活窗口
const handleActivate = () => {
  windowStore.activateWindow(props.id);
};

// 关闭窗口
const handleClose = () => {
  windowStore.closeWindow(props.id);
};

// 最小化/恢复窗口
const isMinimized = ref(props.isMinimized);
const toggleMinimize = () => {
  isMinimized.value = !isMinimized.value;
};
</script>

<style scoped>
.window {
  position: absolute;
  background: white;
  border: 1px solid #ccc;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  transition: box-shadow 0.2s;
}

.window.active {
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}

.window-header {
  background: linear-gradient(to right, #4a6fa5, #6a93d3);
  color: white;
  padding: 12px 16px;
  cursor: move;
  display: flex;
  justify-content: space-between;
  align-items: center;
  user-select: none;
}

.window-title {
  font-weight: 500;
}

.window-controls {
  display: flex;
  gap: 8px;
}

.window-btn {
  width: 24px;
  height: 24px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  transition: background 0.2s;
}

.window-btn:hover {
  background: rgba(255, 255, 255, 0.3);
}

.window-btn.close:hover {
  background: #ff5f57;
}

.window-content {
  padding: 20px;
  height: calc(100% - 48px);
  overflow: auto;
}
</style>
3. WindowManager.vue 组件:窗口管理器

最后,创建窗口管理器组件,负责渲染所有窗口并提供控制按钮:

vue 复制代码
<!-- components/WindowManager.vue -->
<template>
  <div class="window-manager">
    <!-- 桌面区域 -->
    <div class="desktop">
      <!-- 渲染所有窗口 -->
      <Window
        v-for="window in windows"
        :key="window.id"
        :id="window.id"
        :title="window.title"
        :content="window.content"
        :x="window.x"
        :y="window.y"
        :width="window.width"
        :height="window.height"
        :zIndex="window.zIndex"
        :isMinimized="window.isMinimized"
      />
    </div>
    
    <!-- 控制面板 -->
    <div class="control-panel">
      <button @click="openNewWindow" class="btn-primary">
        + 打开新窗口
      </button>
      <button @click="closeActiveWindow" class="btn-secondary" :disabled="!activeWindow">
        × 关闭当前窗口
      </button>
      <div class="window-count">
        已打开窗口: {{ windows.length }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';
import { useWindowStore } from '@/stores/windowStore';
import Window from './Window.vue';

const windowStore = useWindowStore();

// 使用 VueUse 的 useStorage 实现状态持久化
import { useStorage } from '@vueuse/core';

// 自动保存窗口状态到 localStorage
const savedWindows = useStorage('desktop-windows', []);
const savedActiveWindowId = useStorage('desktop-active-window-id', null);

// 监听窗口变化并自动保存
watch(() => windowStore.windows, (newWindows) => {
  savedWindows.value = newWindows.map(w => ({
    ...w,
    // 移除响应式引用
    x: w.x,
    y: w.y,
    zIndex: w.zIndex
  }));
}, { deep: true });

watch(() => windowStore.activeWindowId, (newId) => {
  savedActiveWindowId.value = newId;
});

// 初始化时恢复保存的状态
onMounted(() => {
  if (savedWindows.value.length > 0) {
    windowStore.windows = savedWindows.value;
    windowStore.activeWindowId = savedActiveWindowId.value;
  } else {
    // 如果没有保存的状态,创建两个示例窗口
    windowStore.openWindow('计算器', '这里是计算器应用');
    windowStore.openWindow('记事本', '这里是记事本应用');
  }
});

const windows = computed(() => windowStore.windows);
const activeWindow = computed(() => windowStore.activeWindow);

const openNewWindow = () => {
  const titles = ['浏览器', '文件管理器', '终端', '设置', '音乐播放器'];
  const contents = [
    '浏览器内容区域',
    '文件列表显示在这里',
    '$ 请输入命令...',
    '系统设置选项',
    '正在播放: Vue3 开发指南'
  ];
  
  const randomIndex = Math.floor(Math.random() * titles.length);
  windowStore.openWindow(titles[randomIndex], contents[randomIndex]);
};

const closeActiveWindow = () => {
  if (activeWindow.value) {
    windowStore.closeWindow(activeWindow.value.id);
  }
};
</script>

<style scoped>
.window-manager {
  position: relative;
  width: 100vw;
  height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  overflow: hidden;
}

.desktop {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 60px;
}

.control-panel {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 60px;
  background: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  padding: 0 20px;
  gap: 15px;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
}

.btn-primary, .btn-secondary {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-primary {
  background: #4a6fa5;
  color: white;
}

.btn-primary:hover {
  background: #3a5f95;
}

.btn-secondary {
  background: #f0f0f0;
  color: #333;
}

.btn-secondary:hover:not(:disabled) {
  background: #e0e0e0;
}

.btn-secondary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.window-count {
  margin-left: auto;
  color: #666;
  font-size: 14px;
}
</style>
4. 主应用入口
vue 复制代码
<!-- App.vue -->
<template>
  <WindowManager />
</template>

<script setup>
import WindowManager from './components/WindowManager.vue';
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  overflow: hidden;
}
</style>
关键特性说明
  1. 状态管理:使用 Pinia 集中管理所有窗口的状态,包括位置、大小、层级、激活状态等。

  2. 拖拽实现 :使用 VueUse 的 useDraggable,一行代码实现拖拽功能,无需手动处理鼠标事件。

  3. 状态持久化 :使用 VueUse 的 useStorage 自动将窗口状态保存到 localStorage,刷新页面后状态不丢失。

  4. 组件化架构Window 组件负责单个窗口的渲染和交互,WindowManager 组件负责管理所有窗口和全局控制。

  5. 响应式设计:所有状态变更自动触发视图更新,无需手动操作 DOM。

  6. 层级管理:点击窗口时自动提升 z-index,确保激活窗口在最上层。

安装依赖
bash 复制代码
npm install vue@3 pinia @vueuse/core

这个示例展示了 Vue3 生态的强大之处:用极少的代码实现了复杂的功能。相比 jQuery 需要手动处理 DOM 操作、事件绑定、状态同步,Vue3 的组合式 API 和生态库让开发变得异常简洁。

通过这个实战示例,你可以看到:

  • 数据驱动:所有 UI 变化都通过修改数据自动完成
  • 组件复用Window 组件可以轻松复用于不同应用
  • 状态管理:Pinia 让全局状态管理变得简单可靠
  • 生态优势:VueUse 提供了大量开箱即用的工具函数

这正是现代前端框架带来的"架构级自由"------你不再需要关心 DOM 操作的细节,而是专注于业务逻辑和用户体验的设计。

参考资料与延伸阅读

为了帮助你更深入地学习和实践,以下是一些高质量的外部资源:

  1. Vue 3 官方文档

    • 推荐理由:一切学习的起点和终点。从核心概念到高级 API,内容全面、权威且持续更新。务必从《介绍》和《快速上手》开始。
  2. VueUsePinia 官方文档

    • 推荐理由:Vue 3 生态的"瑞士军刀"与状态管理标准。VueUse 提供了大量开箱即用的组合式函数,Pinia 是管理复杂应用状态的必备工具。掌握它们是提升开发效率的关键。
  3. "You Might Not Need jQuery" 及现代框架对比文章

    • 推荐理由:一个经典的网站,展示了如何使用原生 JavaScript 实现常见的 jQuery 功能。结合阅读一些关于"jQuery vs. React/Vue"的深度对比文章,能帮助你从原理上理解现代框架的价值。
  4. Windows 11 in ReactmacOS Web

    • 推荐理由:在 GitHub 上搜索 "windows react"、"macos vue" 等关键词,可以找到许多高质量的仿桌面 UI 开源项目。阅读这些项目的源码,是学习如何用现代前端框架(React/Vue)组织复杂界面和交互的绝佳方式。

相关推荐
IMPYLH2 小时前
HTML 的 <data> 元素
前端·html
YHHLAI3 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
weixin_BYSJ19873 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php
沪飘大军4 小时前
ll-llm:一个零依赖、可插拔的 OpenAI 兼容 LLM 代理
javascript
kyriewen5 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
前端H5 小时前
微前端 v3 架构升级:从加载隔离到状态协同
前端·架构·状态模式
山河木马6 小时前
GPU自动处理专题3-NDC怎么映射成屏幕像素坐标(视口变换)
javascript·webgl·图形学
陆枫Larry6 小时前
小程序包体积优化:用 SVG 图片替换 iconfont,并保留 CSS 控色能力
前端
Appthing7 小时前
在 TanStack Start 中使用 VitePWA 的正确配置
javascript