计算机基础知识大全(四)

同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!上一节我们全面掌握了Web前端的基石——HTML、CSS和原生JavaScript。现在,我们将进入现代前端开发的新篇章:前端框架

在实际的大型Web应用开发中,直接使用原生HTML、CSS、JS来操作DOM和管理复杂状态会变得非常困难和低效。前端框架应运而生,它们提供了一套高效的开发范式、组件化思想和数据管理机制,极大地提升了开发效率和项目可维护性。

我们将以Vue.js为例进行深入学习。Vue.js以其渐进式、易学易用、性能高效的特点,在前端开发者中拥有极高的人气。


课程3.4:前端框架 - Vue.js基础(超详细版)

一、Vue.js简介与核心理念:渐进式框架的魅力

1.1 什么是Vue.js?
  • Vue.js 是由尤雨溪(Evan You)创建的一套用于构建用户界面的渐进式JavaScript框架

  • 渐进式:意味着你可以逐步地、按需地引入Vue。你可以只用它来增强页面的一小部分交互,也可以用它来构建一个功能完整的、大型的单页应用(Single Page Application, SPA)。这使得Vue非常灵活,既适合小型项目快速上手,也适合大型企业级应用。

  • 核心特点

    • 数据驱动视图(MVVM模式):开发者只需关注数据状态,UI会自动响应数据变化而更新,无需手动操作DOM。

    • 组件化开发:将复杂的页面拆分为独立的、可复用的组件,每个组件有自己的逻辑、模板和样式。

    • 响应式数据系统:Vue通过劫持数据访问(Vue 2使用Object.defineProperty,Vue 3使用Proxy)来追踪数据的变化,并自动通知视图更新。

    • 易学、高效、灵活:API设计直观,中文文档完善,性能优秀,生态工具丰富。

1.2 与其他前端框架的对比:Vue的优势何在?

目前前端三大主流框架是Vue、React和Angular。它们各有优劣,适应不同场景。

| 特性/框架 | Vue.js | React | Angular |

|-------------|----------------------------------------|------------------------------------------|------------------------------------------|

| 语法风格| 基于HTML模板*.vue单文件组件),渐进增强,学习门槛低。 | JSX(JavaScript XML),将HTML写在JS中,更灵活但上手门槛略高。 | TypeScript + HTML模板,强类型,基于组件化。 |

| 学习曲线| 最平缓,文档友好,API直观。 | 较陡峭(JSX、函数式组件、Hooks概念)。 | 最陡峭(概念多:模块、服务、RxJS、DI)。 |

| 核心理念| 数据驱动视图(MVVM),易于理解。 | UI = f(state),组件化,函数式编程。 | MVC/MVVM框架,完整的解决方案,数据双向绑定。 |

| 适用项目| 小到大、灵活,从小型项目到复杂SPA均可。 | 大型工程、复杂交互,生态最广。 | 企业级应用,一体化解决方案,约束性强。 |

| 官方生态| 完善(Vue Router, Pinia/Vuex, Vue CLI, Vite)。| 丰富(React Router, Redux, Next.js)。 | 一体化很强(CLI工具、RxJS等)。 |

| 性能 | 优秀(Vue 3的Proxy优化)。 | 优秀(虚拟DOM,Fiber架构)。 | 良好(AOT编译)。 |

老师提示:Vue因其易用性,常被视为前端入门框架首选,同时其性能和扩展性也足以支撑大型项目。

1.3 核心设计思想:Vue的“内功心法”
  • 数据驱动视图(Data-Driven View)

    • 这是MVVM(Model-View-ViewModel)模式在Vue中的体现。

    • Model:即JavaScript数据(如组件的data属性)。

    • View:即DOM元素(HTML模板)。

    • ViewModel:Vue实例本身,它作为Model和View之间的桥梁。

    • 核心:你只需要改变JavaScript中的数据,Vue会自动检测到数据的变化,并负责更新DOM,将最新的数据同步到视图上。开发者无需手动调用document.getElementById()来修改DOM。

    • 比喻:你修改了幕后剧本(数据),舞台(视图)上的演员(DOM元素)就会自动按照新剧本表演,你不用去手动调整演员的走位。

  • 组件化开发(Component-Based Development)

    • 含义:将整个UI界面拆分成独立的、可复用的小单元——组件。每个组件封装了自己的逻辑(JavaScript)、模板(HTML)和样式(CSS)。

    • 优点

      1. 代码复用:一次编写,多处使用。

      2. 降低复杂度:将大问题分解为小问题,便于开发和维护。

      3. 提高可维护性:组件之间相互独立,修改一个组件通常不会影响其他组件。

      4. 团队协作:不同成员可以独立开发不同组件。

    • 比喻:你不再建造一座整体大楼,而是搭建一个个标准化的“乐高积木”,然后用这些积木组装出你想要的复杂结构。

  • 响应式数据系统(Reactivity System)

    • 含义:Vue能够追踪JavaScript数据对象的变化。当数据发生修改时,所有依赖于这些数据的地方(如模板中的绑定、计算属性、侦听器)都会自动收到通知,并触发相应的更新。

    • Vue 2实现:基于Object.defineProperty来劫持(Hook)对象的属性访问和修改。

    • Vue 3实现:基于ES6的Proxy对象,提供了更强大、更高效的响应式能力,可以监听对象的所有操作(包括属性的增删),而无需预先遍历所有属性。

二、Vue项目结构与开发环境:Vue项目的“骨骼”与“工具”

2.1 单文件组件(.vue文件):Vue的“积木单元”
  • 特点:Vue独有的文件格式,将一个组件的所有相关部分(HTML、JavaScript、CSS)集中在一个文件中。

  • 结构:一个.vue文件通常由三部分组成:

    1. <template>:包含组件的HTML模板结构。

    2. <script>:包含组件的JavaScript逻辑(数据、方法、生命周期钩子等)。

    3. <style>:包含组件的CSS样式。

  • 优点:高度内聚,模块化清晰,便于管理和理解。

  • 示例

    
    <template>
      <div class="hello-world">
        <h1>{{ message }}</h1>
        <button @click="changeMessage">Change Message</button>
      </div>
    </template>
    
    <script>
    export default {
      data() { // 组件的数据
        return {
          message: "Hello, Vue World!"
        };
      },
      methods: { // 组件的方法
        changeMessage() {
          this.message = "Message Changed!";
        }
      }
    };
    </script>
    
    <style scoped> /* scoped 属性使样式只作用于当前组件 */
    .hello-world {
      color: #42b983; /* Vue 的绿色 */
      font-family: 'Arial', sans-serif;
      text-align: center;
    }
    h1 {
      margin-bottom: 20px;
    }
    button {
      padding: 10px 20px;
      font-size: 16px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    button:hover {
      background-color: #0056b3;
    }
    </style>
    
2.2 开发环境与构建工具:快速启动你的Vue项目
  • Node.js和npm/yarn:这是所有现代前端项目的基础。Vue项目的依赖管理和构建都需要它们。

  • Vue CLI(Command Line Interface)

    • 用途:Vue官方提供的命令行工具,用于快速搭建Vue项目、进行项目开发和构建。它预配置了Webpack等复杂的打包工具,让开发者可以专注于代码编写。

    • 安装npm install -g @vue/cli

    • 创建项目vue create my-vue-app (会提供各种预设配置选项)

  • Vite

    • 用途:一个更现代、更快速的构建工具,由Vue的作者尤雨溪开发。它利用浏览器原生的ES Modules支持,在开发服务器启动和热更新方面速度极快。

    • 创建项目npm create vite@latest my-vite-app -- --template vue

    • 老师推荐:对于新项目,Vite通常是更好的选择,开发体验非常棒。

  • 运行项目

    • npm run dev (或 npm run serve):启动开发服务器,通常在localhost:3000localhost:8080运行,支持热更新。

    • npm run build:将项目打包为生产环境可部署的静态文件。

2.3 目录结构说明:Vue项目的“组织图”

一个典型的Vue项目(使用Vue CLI或Vite创建)的目录结构如下:

  
my-vue-app/
  
├── public/          # 公共静态资源,不会被Webpack/Vite处理,直接复制到dist
  
│   ├── index.html   # HTML入口文件
  
│   └── favicon.ico
  
├── src/             # 项目核心源代码
  
│   ├── assets/      # 静态资源,会被Webpack/Vite处理(如图片、CSS预处理器文件)
  
│   │   ├── logo.png
  
│   │   └── styles/
  
│   │       └── global.scss
  
│   ├── components/  # 存放可复用的小组件(如Button, Card, Header)
  
│   │   ├── MyButton.vue
  
│   │   └── UserCard.vue
  
│   ├── views/       # 存放页面级组件(通常与路由对应,如Home.vue, About.vue)
  
│   │   ├── Home.vue
  
│   │   └── About.vue
  
│   ├── router/      # 路由配置目录 (Vue Router)
  
│   │   └── index.js
  
│   ├── store/       # 状态管理目录 (Pinia/Vuex)
  
│   │   └── index.js
  
│   ├── App.vue      # 根组件,所有其他组件的入口
  
│   └── main.js      # JavaScript入口文件,负责创建Vue应用实例、挂载根组件、引入路由和状态管理等
  
├── .gitignore       # Git忽略文件
  
├── package.json     # 项目依赖和脚本配置
  
├── README.md        # 项目说明文件
  
└── vite.config.js / vue.config.js # 构建工具配置文件
  

三、Vue核心语法与响应式原理:Vue的“魔法”所在

3.1 模板语法:在HTML中“使用”数据

Vue使用基于HTML的模板语法,让你可以在HTML中声明式地将DOM绑定到数据。

  • 插值表达式

    • 语法{{ variable }}

    • 作用:将组件数据直接插入到HTML文本中。

    • 示例

      
      <template>
        <p>当前消息:{{ message }}</p>
      </template>
      <script>
      export default { data() { return { message: '你好 Vue' } } }
      </script>
      
  • 指令系统(Directives)

    • 语法v-prefix

    • 作用:Vue提供的一系列特殊属性,以v-开头,用于在DOM元素上应用特殊的响应式行为。

    • 常用指令

      • v-bind单向绑定HTML属性到数据。可以简写为冒号:

        • 示例

          
          <img :src="imgUrl" :alt="imgAlt">
          <button :disabled="isDisabled">点击</button>
          
      • v-model:用于表单输入元素和组件上的双向绑定。它会根据输入类型自动选取正确的方式更新数据。

        • 示例

          
          <input v-model="inputValue" type="text">
          <p>你输入了:{{ inputValue }}</p>
          
          • 当用户在输入框输入时,inputValue数据会自动更新。

          • inputValue数据在JS中被修改时,输入框内容也会自动更新。

      • v-on:监听DOM事件,并执行对应的方法。可以简写为@

        • 示例

          
          <button @click="handleClick">点击我</button>
          <input @input="handleInput" @keydown.enter="submitForm">
          
      • v-if / v-else-if / v-else:条件渲染,根据条件销毁或重建DOM元素(开销较大,但真实)。

        • 示例

          
          <p v-if="isShow">这段文本会根据isShow的值显示或隐藏。</p>
          <p v-else>当isShow为false时显示。</p>
          
      • v-show:条件渲染,通过CSS的display属性来切换元素的显示/隐藏(开销小,但元素一直在DOM中)。

        • 示例<p v-show="isVisible">这段文本只是隐藏而非销毁。</p>

        • 老师提示v-if适用于不频繁切换,v-show适用于频繁切换。

      • v-for:列表渲染,循环遍历数组或对象,生成一组元素。必须提供key属性。

        • key属性:非常重要!用于Vue跟踪列表中的每个节点的身份,以便在列表数据变化时,高效地复用或重新排序DOM元素。key的值应该是列表中每个项的唯一标识。

        • 示例

          
          <ul>
            <li v-for="item in items" :key="item.id">
              {{ item.name }}
            </li>
          </ul>
          <!-- 遍历对象 -->
          <div v-for="(value, key) in user" :key="key">
            {{ key }}: {{ value }}
          </div>
          <!-- 遍历数字范围 -->
          <span v-for="n in 10" :key="n">{{ n }}</span>
          
      • v-text / v-html

        • v-text:等同于textContent,设置纯文本内容。

        • v-html:等同于innerHTML,设置HTML内容(注意XSS安全风险)。

3.2 响应式数据系统:Vue的“魔力之源”
  • 核心概念:Vue的响应式系统是其最核心的特性之一。当你修改Vue组件中的数据时,视图会自动更新,这就是响应式的体现。

  • Vue 2的实现:主要通过Object.defineProperty来劫持(getter/setter)数据对象的属性访问和修改。当数据被访问时,Vue会收集依赖;当数据被修改时,Vue会通知所有依赖的地方进行更新。

  • Vue 3的实现:引入了ES6的**Proxy对象**来重写响应式系统。

    • 优点

      1. 更全面的劫持Proxy可以直接监听整个对象,包括属性的添加、删除、数组的索引修改和length属性的变化,解决了Vue 2中无法直接监听数组索引修改和对象属性增删的问题。

      2. 更高的性能Proxy性能更好,减少了Vue 2中递归遍历所有属性进行监听的开销。

  • Vue 3中的响应式API(Composition API部分,在setup()中使用):

    • ref():用于处理基本数据类型(如number, string, boolean)和包装对象(让基本类型也具备响应式能力)。

      • 访问/修改值时需要.value
      
      import { ref } from 'vue';
      const count = ref(0); // 创建一个响应式的number
      console.log(count.value); // 访问值
      count.value++;           // 修改值
      
    • reactive():用于处理对象和数组,使其成为响应式对象。

      • 直接访问/修改属性即可。
      
      import { reactive } from 'vue';
      const state = reactive({
        name: 'Alice',
        age: 30,
        hobbies: ['coding', 'reading']
      });
      console.log(state.name); // 访问属性
      state.age++;             // 修改属性
      state.hobbies.push('hiking'); // 修改数组元素
      
    • 老师提示refreactive是Vue 3 Composition API中创建响应式数据的两种主要方式。ref更通用,因为它可以包装任何类型的值。

3.3 计算属性(Computed Properties)与侦听器(Watchers):数据的“派生”与“监控”
  • 计算属性(computed

    • 作用:基于已有的响应式数据,派生出新的数据。它的值是惰性求值的,只有当它所依赖的响应式数据发生变化时,才会重新计算。计算结果会被缓存。

    • 用途:处理复杂的数据逻辑、格式化数据、筛选数据等。

    • 比喻:你的工资(原始数据)会变化,但你的年薪(计算属性)会根据工资的变化自动计算,并且如果工资不变,年薪就从上次计算的结果中直接拿,不用重新算。

    • 示例

      
      <template>
        <p>原始消息:{{ rawMessage }}</p>
        <p>反转消息:{{ reversedMessage }}</p>
      </template>
      <script>
      import { ref, computed } from 'vue'; // Vue 3 Composition API
      
      export default {
        setup() {
          const rawMessage = ref('Hello Vue');
      
          // 定义一个计算属性
          const reversedMessage = computed(() => {
            return rawMessage.value.split('').reverse().join('');
          });
      
          return {
            rawMessage,
            reversedMessage
          };
        }
      };
      </script>
      
  • 侦听器(watch

    • 作用:**侦听(Watch)一个或多个响应式数据源的变化,并在数据变化时执行副作用(Side Effect)**函数。它更适用于执行异步操作、响应数据变化进行DOM操作或调用外部API等场景。

    • 比喻:你是一个监控员,当某个指标(数据)发生变化时,你就触发一个报警(副作用操作)。

    • 示例

      
      <template>
        <input v-model="question" placeholder="问我一个问题">
        <p>{{ answer }}</p>
      </template>
      <script>
      import { ref, watch } from 'vue'; // Vue 3 Composition API
      
      export default {
        setup() {
          const question = ref('');
          const answer = ref('我无法回答,直到你问一个问题!');
      
          // 侦听 question 的变化
          watch(question, async (newQuestion, oldQuestion) => {
            if (newQuestion.includes('?')) {
              answer.value = '思考中...';
              try {
                // 模拟一个异步请求
                const res = await new Promise(resolve => setTimeout(() => resolve('当然可以!'), 500));
                answer.value = res;
              } catch (error) {
                answer.value = '发生错误。';
              }
            } else {
              answer.value = '我无法回答,直到你问一个问题!';
            }
          });
      
          return {
            question,
            answer
          };
        }
      };
      </script>
      
    • 老师提示computed用于“根据A计算B”,watch用于“当A变化时执行C”。两者用途不同。

到这里,我们已经深入了解了Vue的核心思想、项目结构以及最基础也是最核心的响应式原理、模板语法、计算属性和侦听器。掌握这些,你已经具备了构建Vue组件的基本能力。


好的,同学们,我们继续前端框架Vue.js基础的学习!上一节我们详细探讨了Vue的核心语法、响应式原理、模板指令、计算属性和侦听器。现在,我们将进入Vue最强大的特性之一——组件系统(Component System),学习如何构建可复用的UI模块,以及在组件之间进行通信。同时,我们还将深入了解Vue 3中革新的Composition API

组件化是现代前端框架的基石。它使得复杂的用户界面可以像搭乐高积木一样被拆解、构建和重组,大大提升了开发效率和可维护性。


四、组件系统与组件通信:UI的“乐高积木”与“对话机制”

4.1 组件的创建与注册:让你的“积木”可用
  • 什么是组件:组件是Vue应用程序的基本构建块,它是一个独立的、可复用的UI单元,拥有自己的模板、逻辑(JavaScript)和样式。

  • 创建组件:在*.vue单文件组件中编写即可。

  • 注册组件:组件必须先被注册,才能在模板中使用。

    1. 局部注册(Local Registration)

      • 特点:在父组件的components选项中显式引入和注册,只在当前父组件及其子组件中可用。

      • 优点:按需引入,减少不必要的打包体积,模块化更清晰。

      • 示例

        
        <!-- ParentComponent.vue -->
        <template>
          <div>
            <MyButton /> <!-- 使用MyButton组件 -->
          </div>
        </template>
        
        <script>
        import MyButton from './MyButton.vue'; // 引入子组件
        
        export default {
          components: { // 局部注册
            MyButton // 注册后才可以在模板中使用 <MyButton />
          }
        };
        </script>
        
    2. 全局注册(Global Registration)

      • 特点:在Vue应用实例的入口文件(通常是main.js)中注册,注册后可以在任何组件的模板中直接使用,无需再次导入。

      • 优点:方便常用、通用的组件。

      • 缺点:所有组件都会被打包,即使有些页面没有用到,可能增加打包体积。

      • 示例 (main.js):

        
        import { createApp } from 'vue';
        import App from './App.vue';
        import MyGlobalButton from './components/MyGlobalButton.vue';
        
        const app = createApp(App);
        app.component('MyGlobalButton', MyGlobalButton); // 全局注册组件
        app.mount('#app');
        
  • 组件命名建议

    • 在定义组件时,文件名通常使用大驼峰命名法(PascalCase),如MyButton.vue

    • 在模板中使用组件时,推荐使用短横线命名法(kebab-case),如<my-button>,虽然也可以使用<MyButton>,但kebab-case更符合Web组件规范。

4.2 组件通信:让“积木”之间相互“对话”

组件化开发中,组件之间经常需要共享数据或触发事件。这就是组件通信解决的问题。

4.2.1 父子通信:最常见的数据流

  • props(属性):父传子(单向数据流)

    • 原理:父组件通过HTML属性的形式向子组件传递数据。子组件通过props选项声明接收这些数据。

    • 特点props单向数据流,子组件不能直接修改接收到的prop。如果子组件需要修改数据,应该通过事件通知父组件修改。

    • 优点:数据流清晰,易于调试。

    • 示例

      
      <!-- ParentComponent.vue -->
      <template>
        <ChildComponent :title="pageTitle" :user-count="count" />
      </template>
      <script>
      import ChildComponent from './ChildComponent.vue';
      export default {
        components: { ChildComponent },
        data() { return { pageTitle: '我的页面', count: 100 } }
      }
      </script>
      
      <!-- ChildComponent.vue -->
      <template>
        <div>
          <h1>{{ title }}</h1>
          <p>用户数量: {{ userCount }}</p>
        </div>
      </template>
      <script>
      export default {
        props: {
          title: String, // 声明接收名为title的prop,类型为String
          userCount: {    // 更详细的prop定义
            type: Number,
            required: true, // 必须传递
            default: 0      // 默认值
          }
        }
      };
      </script>
      
  • emit(自定义事件):子传父

    • 原理:子组件通过触发一个自定义事件,并可以附带数据。父组件在子组件标签上监听这个事件,并在事件发生时执行对应的方法。

    • 语法:子组件调用this.$emit('eventName', data)来触发事件。父组件在模板中用@eventName="method"来监听。

    • 示例

      
      <!-- ParentComponent.vue -->
      <template>
        <UserForm @submit-user="handleUserSubmit" />
      </template>
      <script>
      import UserForm from './UserForm.vue';
      export default {
        components: { UserForm },
        methods: {
          handleUserSubmit(userData) { // 接收子组件传递过来的数据
            console.log('父组件接收到用户数据:', userData);
            // 这里可以进行数据处理,例如发送到后端
          }
        }
      }
      </script>
      
      <!-- UserForm.vue -->
      <template>
        <form @submit.prevent="submitForm">
          <input type="text" v-model="userName" placeholder="用户名">
          <button type="submit">提交</button>
        </form>
      </template>
      <script>
      export default {
        data() { return { userName: '' } },
        methods: {
          submitForm() {
            // 子组件触发自定义事件 'submit-user',并传递 userName
            this.$emit('submit-user', { name: this.userName });
            this.userName = ''; // 清空输入
          }
        }
      }
      </script>
      

4.2.2 兄弟通信/跨层通信:更复杂的场景

  • provide/inject(祖先-后代通信)

    • 原理:允许一个祖先组件(父组件或更高级别的组件)向其所有后代组件(包括子组件、孙子组件等)提供数据,而无需层层传递props。

    • 优点:解决了多层嵌套组件间数据传递的“props drilling”(属性逐层透传)问题。

    • 示例

      
      <!-- AncestorComponent.vue -->
      <script>
      import { provide, ref } from 'vue'; // Vue 3 Composition API
      export default {
        setup() {
          const theme = ref('dark');
          provide('appTheme', theme); // 提供 'appTheme' 属性
          return { theme };
        }
      };
      </script>
      
      <!-- DescendantComponent.vue (任意层级深的后代组件) -->
      <script>
      import { inject } from 'vue';
      export default {
        setup() {
          const theme = inject('appTheme'); // 注入 'appTheme' 属性
          return { theme };
        }
      };
      </script>
      
  • 全局状态管理(State Management)

    • 原理:对于大型复杂应用,当多个组件(特别是兄弟组件、无直接父子关系的组件)需要共享和修改同一份数据时,传统通信方式会变得非常复杂。全局状态管理模式(如PiniaVuex)提供一个集中式存储来管理所有组件的状态。

    • 优点:状态集中,数据流清晰,易于调试和维护。

    • 比喻:就像公司里的“中央数据库”,所有部门都可以从这里获取和更新数据,而不需要部门之间直接打电话。

    • 将在路由与状态管理章节详细讲解

4.2.3 插槽(Slots):内容的“占位符”

  • 原理:允许父组件向子组件的指定位置插入内容(HTML模板片段)。这使得组件更加灵活和可组合。

  • 类型

    • 默认插槽(Default Slot):没有名字的插槽,父组件插入的内容会渲染到子组件slot标签所在的位置。

      
      <!-- MyCard.vue (子组件) -->
      <template>
        <div class="card">
          <header><slot name="header"></slot></header> <!-- 具名插槽 -->
          <main><slot></slot></main> <!-- 默认插槽 -->
          <footer><slot name="footer"></slot></footer>
        </div>
      </template>
      
      <!-- ParentUsingCard.vue (父组件) -->
      <template>
        <MyCard>
          <!-- 默认插槽内容 -->
          <p>这是卡片的主体内容。</p>
          <!-- 具名插槽内容 -->
          <template v-slot:header>
            <h2>卡片标题</h2>
          </template>
          <template #footer> <!-- #是v-slot:的简写 -->
            <button>详情</button>
          </template>
        </MyCard>
      </template>
      
    • 具名插槽(Named Slots):有名字的插槽,父组件可以精确地将内容插入到子组件的特定插槽中。

    • 作用域插槽(Scoped Slots):允许子组件向父组件的插槽内容传递数据。

五、Vue 3 Composition API:更灵活的代码组织方式

5.1 为什么需要Composition API
  • 背景:Vue 2的Options API(选项式API,即data, methods, computed等选项)在小型组件中组织清晰。但当组件逻辑变得复杂,特别是在大型组件中需要处理多个不相关的逻辑关注点时,相关逻辑的代码会分散在不同的选项中,导致代码难以阅读和维护(“高内聚低耦合”的反例)。

    • 比喻:Options API像一个抽屉柜,所有“方法”放在一个抽屉,“数据”放在另一个抽屉。当一个功能(如用户管理)需要用到很多方法和数据时,这些代码就散落在不同抽屉里了。
  • Composition API(组合式API,Vue 3新增)

    • 目标:解决上述问题,提供一种更灵活、更强大的方式来组织和复用组件逻辑。它允许你将同一功能相关的逻辑代码(包括数据、方法、计算属性、侦听器等)组织在一起,无论它们来自哪个“选项”。

    • 优点

      1. 更好的逻辑复用:可以将可复用的逻辑封装成独立的“组合式函数”(Composable functions),在不同组件中复用。

      2. 更好的代码组织:同一功能的代码聚合在一起,提高了可读性和可维护性。

      3. 更好的类型推断:对TypeScript支持更友好。

      4. 更灵活的生命周期钩子:直接在setup函数中导入和使用。

    • 比喻:Composition API像一个文件夹,你可以把所有关于“用户管理”的代码(数据、方法、计算属性等)都放在这个文件夹里,即使它们分属于抽屉柜的不同抽屉。

5.2 基本用法
  • setup()函数

    • 作用:作为Vue 3 Composition API的主要入口点。在组件实例创建之前执行,是组合式API的核心。

    • 特点

      • 接收propscontext作为参数。

      • setup中定义的响应式数据、方法、计算属性等,必须显式地return出去,才能在模板中使用。

      • 没有this上下文:在setup函数内部,this不再指向组件实例,因为setup在组件创建之前执行。

  • 响应式API

    • ref():用于声明基本数据类型(或简单对象)的响应式变量。访问/修改值需要.value

    • reactive():用于声明复杂对象或数组的响应式变量。访问/修改属性无需.value

    • computed():定义计算属性。

    • watch() / watchEffect():定义侦听器。

    • 生命周期钩子:如onMountedonUnmounted等,直接导入并调用。

  • 示例:使用Composition API重写计数器组件

    
    <template>
      <div>
        <p>计数器: {{ count }}</p>
        <p>双倍计数: {{ doubleCount }}</p>
        <button @click="increment">增加</button>
        <p v-if="count > 5">计数已超过5!</p>
      </div>
    </template>
    
    <script>
    import { ref, computed, watch, onMounted } from 'vue'; // 从vue中导入需要的API
    
    export default {
      // setup函数是Composition API的入口
      setup() {
        // 1. 定义响应式数据
        const count = ref(0); // 使用ref定义一个响应式数字
    
        // 2. 定义计算属性
        const doubleCount = computed(() => count.value * 2); // 访问ref需要.value
    
        // 3. 定义方法
        const increment = () => {
          count.value++; // 修改ref的值需要.value
        };
    
        // 4. 定义侦听器
        watch(count, (newCount, oldCount) => {
          console.log(`计数器从 ${oldCount} 变为 ${newCount}`);
          if (newCount > 10) {
            console.log('计数器达到10,停止增长!');
            // 实际应用中可能触发其他逻辑或API调用
          }
        });
    
        // 5. 使用生命周期钩子
        onMounted(() => {
          console.log('组件已挂载!');
        });
    
        // 6. 必须返回所有需要在模板中使用的数据、方法、计算属性等
        return {
          count,
          doubleCount,
          increment
        };
      }
    };
    </script>
    
  • 可复用逻辑的封装(Composable Functions)

    • Composition API最强大的特性之一。可以将一段逻辑(如处理鼠标位置、管理购物车、进行API请求等)封装到一个独立的.js文件中,作为一个普通函数导出。这个函数内部可以使用refreactivecomputed等Vue响应式API。

    • 在其他组件中,只需导入并调用这个函数,即可复用这段逻辑。

    • 示例 (useMousePosition.js):

      
      // useMousePosition.js
      import { ref, onMounted, onUnmounted } from 'vue';
      
      export function useMousePosition() {
        const x = ref(0);
        const y = ref(0);
      
        function update(e) {
          x.value = e.pageX;
          y.value = e.pageY;
        }
      
        onMounted(() => {
          window.addEventListener('mousemove', update);
        });
      
        onUnmounted(() => {
          window.removeEventListener('mousemove', update);
        });
      
        return { x, y };
      }
      
    • 在组件中使用:

      
      <!-- MyComponent.vue -->
      <template>
        <div>
          鼠标位置: {{ x }}, {{ y }}
        </div>
      </template>
      <script>
      import { useMousePosition } from './useMousePosition'; // 导入可复用逻辑
      
      export default {
        setup() {
          const { x, y } = useMousePosition(); // 调用可复用逻辑
          return { x, y };
        }
      };
      </script>
      

通过本节的学习,大家应该对Vue的组件化思想、父子组件通信方式、以及Vue 3革新的Composition API有了深入理解。掌握这些,你就能构建出模块化、可维护、可复用的大型前端应用。


好的,同学们,我们继续前端框架Vue.js基础的学习!前一节我们全面探讨了Vue的组件系统、组件通信方式以及Vue 3的Composition API,理解了如何构建模块化、可复用的UI模块。现在,我们将进入Vue在构建大型单页应用(SPA)时的两个核心组成部分——路由管理(Vue Router)状态管理(Pinia/Vuex),以及如何进行HTTP请求与API集成

构建一个复杂的单页应用,意味着我们不再是简单的页面跳转,而是在同一个HTML页面内根据URL变化切换组件。同时,应用的状态(如用户登录信息、购物车商品、待办事项列表等)需要在不同组件之间共享和修改,这时就需要一套统一的状态管理机制。而与后端进行数据交互,则是所有Web应用的核心功能。


六、路由与状态管理:SPA的“导航员”与“中央大脑”

6.1 路由管理(Vue Router):SPA的“导航系统”
  • 什么是Vue Router

    • Vue Router是Vue.js官方的路由管理器,用于构建单页应用(SPA, Single Page Application)

    • SPA特点:整个应用只有一个HTML页面。当用户在应用内导航时,URL会改变,但页面不会刷新。Vue Router会根据URL的变化,动态地加载和渲染对应的组件,从而模拟传统多页应用的行为。

    • 比喻:Vue Router就像你的汽车导航系统,你在地图上切换目的地,但你始终在同一辆车(SPA)里,只是导航系统为你规划了不同的路线(组件)。

  • 核心概念

    • 路由表配置:定义URL路径与组件的映射关系。

    • 动态路由:支持路径中包含可变参数(如/users/:id)。

    • 嵌套路由:路由可以包含子路由,形成层级结构。

    • 路由视图(Router View)<router-view>组件,用于渲染当前路由匹配到的组件。

    • 路由链接(Router Link)<router-link>组件,用于生成导航链接,避免浏览器刷新。

  • 安装与使用

    1. 安装npm install vue-router@4 (Vue 3版本)

    2. 配置路由表:通常在router/index.js中定义。

    3. main.js中引入并挂载到Vue应用实例。

    4. App.vue或其他组件中使用<router-view><router-link>

  • 示例 (router/index.js):

    
    import { createRouter, createWebHistory } from 'vue-router'; // 导入路由相关函数
    
    // 导入页面级组件
    import HomeView from '../views/HomeView.vue';
    import AboutView from '../views/AboutView.vue';
    import UserProfile from '../views/UserProfile.vue'; // 假设有用户详情页
    
    // 定义路由规则数组
    const routes = [
      {
        path: '/',            // 路径
        name: 'home',         // 路由名称
        component: HomeView   // 对应的组件
      },
      {
        path: '/about',
        name: 'about',
        component: AboutView
      },
      {
        path: '/users/:id',   // 动态路由参数,:id会作为参数传递给组件
        name: 'userProfile',
        component: UserProfile,
        props: true,          // 将路由参数作为props传递给组件
        // 路由守卫 (可选)
        beforeEnter: (to, from, next) => {
          console.log(`即将进入用户ID为 ${to.params.id} 的页面`);
          next(); // 允许跳转
        }
      }
    ];
    
    // 创建路由实例
    const router = createRouter({
      history: createWebHistory(), // 使用HTML5 History模式,URL不带#
      routes // 路由规则
    });
    
    // 全局前置守卫 (可选)
    router.beforeEach((to, from, next) => {
      // 检查用户是否登录,如果没有登录且目标路由需要认证,则重定向到登录页
      // const isAuthenticated = checkIfUserIsLoggedIn(); // 假设有这样一个函数
      // if (to.meta.requiresAuth && !isAuthenticated) {
      //   next('/login'); // 重定向到登录页
      // } else {
        next(); // 允许跳转
      // }
    });
    
    export default router;
    
  • main.js中挂载路由

    
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router'; // 导入路由配置
    
    const app = createApp(App);
    app.use(router); // 挂载路由实例到Vue应用
    app.mount('#app');
    
  • App.vue或其他组件中使用

    
    <template>
      <div id="app">
        <nav>
          <router-link to="/">首页</router-link> |
          <router-link to="/about">关于</router-link> |
          <router-link :to="{ name: 'userProfile', params: { id: 123 }}">用户123</router-link>
        </nav>
        <router-view /> <!-- 路由匹配到的组件会在这里渲染 -->
      </div>
    </template>
    
  • 路由守卫(Navigation Guards)

    • 作用:在路由跳转过程中执行逻辑,如权限控制、数据预加载、页面切换动效等。

    • 类型:全局守卫(router.beforeEach)、路由独享守卫(beforeEnter)、组件内守卫(beforeRouteEnter等)。

6.2 状态管理(Pinia / Vuex):SPA的“中央大脑”
  • 什么是状态管理

    • 在大型应用中,很多数据需要在多个组件之间共享或在不同路由页面间持久化。传统props/emit通信方式难以管理这种复杂的状态流。

    • 状态管理模式提供一个集中式的状态存储(Store),来管理所有组件的共享状态。任何组件都可以从Store中获取状态,也可以通过定义好的操作(Mutation/Action)来修改状态。

  • Pinia(推荐用于Vue 3)

    • 含义:Vue.js官方推荐的轻量级状态管理库,专为Vue 3设计。它比Vuex更简单、API更直观、对TypeScript支持更好。

    • 核心概念

      • Store:定义一个独立的Pinia Store,包含状态(state)、获取器(getters,类似计算属性)、动作(actions,类似方法)。

      • State:存储共享数据。

      • Getters:从Store中派生状态,类似Store的计算属性。

      • Actions:定义修改状态的异步或同步逻辑。

  • Vuex(主要用于Vue 2,也可用于Vue 3)

    • 含义:Vue.js官方的状态管理库,核心概念有State, Getters, Mutations, Actions, Modules。

    • Mutation:同步修改State的唯一方式。

    • Action:提交Mutation,可包含异步操作。

  • Pinia示例 (store/counter.js):

    
    import { defineStore } from 'pinia';
    
    export const useCounterStore = defineStore('counter', {
      state: () => ({
        count: 0,
        name: 'Pinia Counter'
      }),
      getters: {
        doubleCount: (state) => state.count * 2,
        // 也可以访问其他getter
        // doubleAndAddOne(): number { return this.doubleCount + 1 }
      },
      actions: {
        increment(value = 1) {
          this.count += value; // 直接修改状态
        },
        async decrementAsync() {
          // 异步操作
          await new Promise(resolve => setTimeout(resolve, 1000));
          this.count--;
        }
      }
    });
    
  • main.js中引入Pinia

    
    import { createApp } from 'vue';
    import { createPinia } from 'pinia'; // 导入createPinia
    import App from './App.vue';
    import router from './router';
    
    const app = createApp(App);
    const pinia = createPinia(); // 创建pinia实例
    
    app.use(router);
    app.use(pinia); // 挂载pinia实例
    app.mount('#app');
    
  • 在组件中使用Pinia Store

    
    <template>
      <div>
        <p>当前计数: {{ counterStore.count }}</p>
        <p>双倍计数: {{ counterStore.doubleCount }}</p>
        <button @click="counterStore.increment()">增加</button>
        <button @click="counterStore.increment(5)">增加5</button>
        <button @click="counterStore.decrementAsync()">异步减少</button>
      </div>
    </template>
    <script setup>
    import { useCounterStore } from '../store/counter'; // 导入Store
    
    const counterStore = useCounterStore(); // 获取Store实例
    // 也可以解构属性和方法,但需要用 storeToRefs 来保持响应式
    // import { storeToRefs } from 'pinia';
    // const { count, doubleCount } = storeToRefs(counterStore);
    // const { increment } = counterStore;
    </script>
    

七、HTTP请求与API集成:前端与后端的“握手”

前端应用的核心是与后端API进行数据交互。虽然原生JavaScript提供了XMLHttpRequestFetch API,但在Vue项目中,我们通常会使用更专业的库。

7.1 axios的使用:Promise-based的HTTP客户端
  • axios

    • 含义:一个流行的、基于Promise的HTTP客户端,可用于浏览器和Node.js。

    • 特点

      1. 基于Promise:天然支持Promise,便于使用then/catchasync/await处理异步请求。

      2. 拦截器(Interceptors):可以全局拦截请求和响应,方便进行统一处理(如添加认证头、处理错误、显示加载动画)。

      3. 请求取消、请求超时

      4. 自动转换JSON数据

  • 安装npm install axios

  • 集成方式

    1. 全局引入:直接import axios from 'axios',然后axios.get(...)

    2. 封装实例(推荐):创建axios实例并配置基础URL、拦截器,方便管理和复用。

  • 示例:封装axios实例 (utils/request.js)

    
    // utils/request.js
    import axios from 'axios';
    
    // 1. 创建 axios 实例
    const service = axios.create({
      baseURL: '/api', // 所有请求的基础URL,方便统一管理,开发环境下可配置代理
      timeout: 10000,   // 请求超时时间
      headers: {
        'Content-Type': 'application/json;charset=UTF-8'
      }
    });
    
    // 2. 请求拦截器 (Request Interceptors)
    service.interceptors.request.use(
      config => {
        // 在发送请求之前做些什么,例如:
        // 1. 添加认证Token
        const token = localStorage.getItem('jwt_token');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        // 2. 显示加载动画
        // NProgress.start(); 
        return config;
      },
      error => {
        // 对请求错误做些什么
        console.error('请求拦截器错误:', error);
        return Promise.reject(error);
      }
    );
    
    // 3. 响应拦截器 (Response Interceptors)
    service.interceptors.response.use(
      response => {
        // 对响应数据做些什么
        // 1. 隐藏加载动画
        // NProgress.done();
        // 2. 统一处理后端返回的业务状态码
        const res = response.data;
        if (res.code !== 0) { // 假设后端定义0为成功
          // alert(`业务错误: ${res.message}`);
          // 可以在这里统一处理401(未授权),跳转到登录页
          return Promise.reject(new Error(res.message || 'Error'));
        }
        return res; // 返回业务数据部分
      },
      error => {
        // 对响应错误做些什么 (例如HTTP状态码非2xx)
        console.error('响应拦截器错误:', error.response || error.message);
        if (error.response && error.response.status === 401) {
          // 例如,Token过期或未授权,跳转到登录页
          console.log('未授权,重定向到登录页...');
          // router.push('/login'); // 需要引入router
        }
        // NProgress.done();
        return Promise.reject(error);
      }
    );
    
    export default service;
    
  • 在组件中使用封装后的axios

    
    <template>
      <div>
        <p>用户列表</p>
        <ul>
          <li v-for="user in users" :key="user.id">{{ user.name }}</li>
        </ul>
        <button @click="fetchUsers">加载用户</button>
      </div>
    </template>
    <script setup>
    import { ref, onMounted } from 'vue';
    import request from '../utils/request'; // 导入封装好的axios实例
    
    const users = ref([]);
    
    async function fetchUsers() {
      try {
        // 使用封装好的 request 实例发送请求
        const response = await request.get('/users'); // 实际请求的是 /api/users
        users.value = response.data; // 假设后端返回 { code: 0, data: [...] }
        console.log('用户数据:', users.value);
      } catch (error) {
        console.error('获取用户数据失败:', error.message);
      }
    }
    
    onMounted(() => {
      fetchUsers(); // 组件挂载后自动加载数据
    });
    </script>
    
7.2 最佳实践:HTTP请求的规范与统一
  • 配置基础URL、超时、拦截器:如上述request.js示例,统一管理请求配置。

  • 错误统一处理:通过响应拦截器统一处理HTTP状态码、业务错误码,提供友好的用户提示。

  • Token鉴权、自动刷新:在请求拦截器中添加认证Token(如JWT),或处理Token过期时的自动刷新机制。

  • 请求取消:对于快速连续点击或组件销毁时,可以取消不必要的请求。

  • Loading状态管理:在请求开始时显示加载动画,请求结束时隐藏。

八、构建与部署:从开发到线上的旅程

8.1 构建工具:将开发代码转化为生产代码
  • Vite/Vue CLI:作为项目开发服务器和生产环境打包工具。

  • npm run build:在项目根目录运行此命令,打包工具会执行以下操作:

    1. 代码转换:将ES6+语法转换为兼容旧浏览器的ES5。

    2. 模块打包:将所有JavaScript、CSS、图片等资源打包成优化后的静态文件。

    3. 代码优化

      • 压缩(Minification):移除空格、注释、缩短变量名,减小文件体积。

      • 混淆(Obfuscation):使代码难以阅读,保护源代码。

      • Tree Shaking:移除JavaScript中未被使用的代码,进一步减小体积。

      • 代码分割(Code Splitting):将代码分割成多个小块,按需加载,提升首屏加载速度。

    4. 生成结果:通常会生成一个dist/(或build/)目录,里面包含优化后的HTML、CSS、JavaScript、图片等静态资源。

8.2 项目部署流程:让你的网站上线

打包后的dist/目录包含了所有可以部署的静态文件。部署Vue前端项目通常有两种方式:

  1. 部署到静态服务器(如Nginx、Apache)

    • 原理:将dist/目录中的所有文件直接复制到Web服务器(如Nginx)的Web根目录(html文件夹)。

    • 配置Nginx(示例)

      
      server {
          listen 80; # 监听80端口
          server_name yourdomain.com; # 你的域名或服务器IP
      
          root /path/to/your/vue_project/dist; # 指向Vue打包后的dist目录
          index index.html; # 默认索引文件
      
          location / {
              try_files $uri $uri/ /index.html; # 解决SPA路由刷新404问题
          }
          # 如果有后端API,需要配置代理转发
          location /api/ {
              proxy_pass http://localhost:3000/; # 转发到后端服务
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              # ... 其他代理头
          }
      }
      
    • 其他静态托管服务:Vercel、Netlify、GitHub Pages等,它们提供便捷的CI/CD集成,可自动部署你的静态站点。

  2. 集成到后端应用

    • 原理:将打包后的dist/目录放置在后端项目的静态资源目录中,由后端服务器直接提供静态文件服务。

    • 优点:部署在一个应用中,简化CI/CD。

    • 缺点:前后端耦合度增加。

同学们,路由、状态管理、HTTP请求以及项目构建和部署,是构建现代Web应用必不可少的环节。掌握这些,你就具备了独立开发和上线一个完整Web应用的能力。


好的,同学们,我们继续前端框架Vue.js基础的学习!至此,我们已经系统学习了Vue的核心概念、组件系统、路由、状态管理、HTTP请求,以及项目的构建与部署。恭喜大家,你们已经掌握了构建现代交互式前端应用的核心技能!

现在,我们来聊聊Vue.js在整个全栈开发学习路径中的**“连接器”作用**,以及如何通过实践项目来巩固和提升你的前端开发能力。

九、与全栈开发和后续课程的逻辑衔接:Vue的“连接器”作用

Vue.js作为一款优秀的前端框架,是现代全栈开发中不可或缺的一环。它将你前面所学的编程基础与即将学习的后端开发、数据库、云服务等内容紧密连接起来。

  • 前端框架让页面开发高效、可维护

    • 告别原生DOM操作的繁琐和低效,通过数据驱动组件化,大大提高了开发效率和代码的可维护性。

    • 你将能轻松应对复杂UI的构建,使得页面结构清晰、逻辑分明。

    • 比喻:Vue把原生JS操作DOM的“手工活”,变成了“流水线自动化生产”,效率当然更高。

  • 通过API与Node.js/Express后端对接,实现数据驱动的动态页面

    • 前端框架的核心就是通过HTTP请求(我们学过的Fetch APIaxios)与后端API进行数据交互。

    • 你将使用Vue来构建用户界面,然后从后端获取数据并动态展示,或者将用户输入的数据提交给后端保存。

    • 举例

      • 用户登录:Vue组件收集用户名密码,通过axios发送POST请求到后端API,后端验证后返回JWT Token

      • 文章列表:Vue组件挂载后,通过axios发送GET请求获取文章列表数据,然后v-for渲染到页面上。

      • 提交评论:用户在Vue组件输入评论,通过axios发送POST请求到后端API,后端将评论保存到数据库。

  • 组件化、模块化思想贯穿前后端,助力大型系统开发

    • Vue的组件化思想(将UI拆分为独立模块)与后端微服务架构(将业务逻辑拆分为独立服务)异曲同工。

    • 无论是前端的组件、后端的模块、还是数据库中的表,都体现了“高内聚,低耦合”的设计原则。理解了Vue的组件化,你将更容易理解后端服务的模块化和微服务拆分。

    • 比喻:前端用乐高积木搭房子,后端用集装箱搭建仓库,虽然形式不同,但都追求标准化、模块化、可插拔。

  • 状态管理、路由、HTTP请求为后续深入Vue生态、性能优化、SSR、PWA等高级话题打下基础

    • 掌握Pinia/Vuex的状态管理,是深入理解复杂应用数据流的关键。

    • 理解Vue Router,是掌握单页应用(SPA)导航和多页面应用(MPA)混合模式的基础。

    • 熟悉HTTP请求,为你学习更高级的网络优化(如HTTP/2、CDN)、API网关、甚至服务端渲染(SSR)等打下基础。

十、实践项目:任务管理应用

是时候将你的Vue技能应用到实际项目中了!

10.1 项目目标
  • 目标:实现一个具有**增删查改(CRUD, Create, Read, Update, Delete)**功能的任务管理前端应用。

  • 功能需求

    • 任务列表展示:显示所有任务,包括任务标题、状态(未完成/已完成)。

    • 添加新任务:通过输入框添加任务,并显示在列表中。

    • 删除任务:点击按钮可删除指定任务。

    • 切换任务状态:点击任务可切换其完成状态。

    • 任务筛选:可筛选显示“所有任务”、“未完成任务”、“已完成任务”。

    • 任务计数:显示当前未完成任务的数量。

  • 技术要求

    • Vue 3 (Composition API)

    • Pinia (状态管理)

    • Vue Router (路由)

    • axios (模拟API请求,后续可对接真实后端)

    • 单文件组件(.vue

10.2 项目结构设计
  • 组件划分

    • App.vue:根组件,包含导航和主要路由视图。

    • HomeView.vue (views/HomeView.vue):主页,包含任务列表和添加任务表单。

    • TaskItem.vue (components/TaskItem.vue):独立的任务项组件,用于展示单个任务。

    • TaskForm.vue (components/TaskForm.vue):添加任务的表单组件。

    • FilterButtons.vue (components/FilterButtons.vue):筛选任务的按钮组。

  • 状态管理(Pinia)

    • 创建一个taskStore.js (store/task.js),用于管理全局的任务列表状态。

    • 包含tasks(数组),以及addTask, removeTask, toggleTaskStatusactions

    • 包含filteredTasks, uncompletedTasksCountgetters

  • 路由

    • 简单路由:/ (所有任务), /active (未完成任务), /completed (已完成任务)。
  • API请求

    • 初期可以模拟数据(假数据,不与后端交互),直接在Pinia的actions中操作数据。

    • 进阶:使用axiosactions中模拟异步请求或对接真实的后端API。

10.3 关键代码片段(简化与示意)

main.js (入口文件)

  
import { createApp } from 'vue';
  
import { createPinia } from 'pinia';
  
import App from './App.vue';
  
import router from './router'; // 假设你已配置好router
  

  
const app = createApp(App);
  
const pinia = createPinia();
  

  
app.use(router);
  
app.use(pinia);
  
app.mount('#app');
  

store/task.js (Pinia Store)

  
import { defineStore } from 'pinia';
  
import { ref, computed } from 'vue';
  

  
export const useTaskStore = defineStore('task', () => {
  
  // 状态 (state)
  
  const tasks = ref([
  
    { id: 1, title: '学习Vue基础', completed: false },
  
    { id: 2, title: '完成前端项目', completed: false },
  
    { id: 3, title: '准备后端课程', completed: true },
  
  ]);
  
  const currentFilter = ref('all'); // 'all', 'active', 'completed'
  

  
  // Getter (类似计算属性)
  
  const filteredTasks = computed(() => {
  
    if (currentFilter.value === 'active') {
  
      return tasks.value.filter(task => !task.completed);
  
    } else if (currentFilter.value === 'completed') {
  
      return tasks.value.filter(task => task.completed);
  
    }
  
    return tasks.value;
  
  });
  

  
  const uncompletedTasksCount = computed(() => {
  
    return tasks.value.filter(task => !task.completed).length;
  
  });
  

  
  // Actions (修改状态的逻辑)
  
  function addTask(title) {
  
    if (title.trim() === '') return;
  
    tasks.value.push({
  
      id: Date.now(), // 简单生成唯一ID
  
      title: title.trim(),
  
      completed: false,
  
    });
  
  }
  

  
  function removeTask(id) {
  
    tasks.value = tasks.value.filter(task => task.id !== id);
  
  }
  

  
  function toggleTaskStatus(id) {
  
    const task = tasks.value.find(task => task.id === id);
  
    if (task) {
  
      task.completed = !task.completed; // 直接修改响应式对象内部属性
  
    }
  
  }
  

  
  function setFilter(filterType) {
  
    currentFilter.value = filterType;
  
  }
  

  
  // 暴露给外部
  
  return {
  
    tasks,
  
    currentFilter,
  
    filteredTasks,
  
    uncompletedTasksCount,
  
    addTask,
  
    removeTask,
  
    toggleTaskStatus,
  
    setFilter,
  
  };
  
});
  

views/HomeView.vue (页面组件)

  
<template>
  
  <div class="home-view">
  
    <h2>我的待办事项</h2>
  
    <TaskForm @add-task="addTask" />
  
    <FilterButtons :current-filter="currentFilter" @set-filter="setFilter" />
  
    <p>未完成任务: {{ uncompletedTasksCount }}</p>
  
    
  
    <ul class="task-list">
  
      <TaskItem
  
        v-for="task in filteredTasks"
  
        :key="task.id"
  
        :task="task"
  
        @toggle-status="toggleTaskStatus"
  
        @remove-task="removeTask"
  
      />
  
      <li v-if="filteredTasks.length === 0">
  
        <p>暂无任务。</p>
  
      </li>
  
    </ul>
  
  </div>
  
</template>
  

  
<script setup>
  
import TaskForm from '../components/TaskForm.vue';
  
import TaskItem from '../components/TaskItem.vue';
  
import FilterButtons from '../components/FilterButtons.vue';
  
import { useTaskStore } from '../store/task'; // 导入Store
  

  
// 从Store中获取状态和actions
  
const taskStore = useTaskStore();
  
const {
  
  currentFilter,
  
  filteredTasks,
  
  uncompletedTasksCount,
  
  addTask,
  
  removeTask,
  
  toggleTaskStatus,
  
  setFilter,
  
} = taskStore; // 直接使用解构,因为Pinia的actions可以直接解构使用
  

  
// 如果要解构 state 或 getters 的响应式属性,需要用 storeToRefs
  
// import { storeToRefs } from 'pinia';
  
// const { currentFilter, filteredTasks, uncompletedTasksCount } = storeToRefs(taskStore);
  
</script>
  

  
<style scoped>
  
/* 你的 HomeView 样式 */
  
.home-view {
  
  max-width: 600px;
  
  margin: 20px auto;
  
  padding: 20px;
  
  background: white;
  
  border-radius: 8px;
  
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  
}
  
.task-list {
  
  list-style: none;
  
  padding: 0;
  
  margin-top: 20px;
  
}
  
</style>
  

components/TaskForm.vue (子组件)

  
<template>
  
  <form @submit.prevent="submitTask" class="task-form">
  
    <input type="text" v-model="newTaskTitle" placeholder="添加新任务..." />
  
    <button type="submit">添加</button>
  
  </form>
  
</template>
  

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

  
const newTaskTitle = ref('');
  
// 定义组件事件
  
const emit = defineEmits(['add-task']);
  

  
function submitTask() {
  
  if (newTaskTitle.value.trim()) {
  
    emit('add-task', newTaskTitle.value); // 触发父组件的add-task事件
  
    newTaskTitle.value = ''; // 清空输入框
  
  }
  
}
  
</script>
  

  
<style scoped>
  
.task-form {
  
  display: flex;
  
  margin-bottom: 20px;
  
}
  
.task-form input {
  
  flex-grow: 1;
  
  padding: 10px;
  
  border: 1px solid #ddd;
  
  border-radius: 4px 0 0 4px;
  
}
  
.task-form button {
  
  padding: 10px 15px;
  
  background-color: #007bff;
  
  color: white;
  
  border: none;
  
  border-radius: 0 4px 4px 0;
  
  cursor: pointer;
  
}
  
</style>
  

components/TaskItem.vue (子组件)

  
<template>
  
  <li :class="{ completed: task.completed }" class="task-item">
  
    <span @click="toggleStatus" class="task-title">
  
      {{ task.title }}
  
    </span>
  
    <button @click="removeTaskItem" class="remove-btn">X</button>
  
  </li>
  
</template>
  

  
<script setup>
  
import { defineProps, defineEmits } from 'vue';
  

  
const props = defineProps({
  
  task: {
  
    type: Object,
  
    required: true,
  
  },
  
});
  

  
const emit = defineEmits(['toggle-status', 'remove-task']);
  

  
function toggleStatus() {
  
  emit('toggle-status', props.task.id);
  
}
  

  
function removeTaskItem() {
  
  emit('remove-task', props.task.id);
  
}
  
</script>
  

  
<style scoped>
  
.task-item {
  
  display: flex;
  
  justify-content: space-between;
  
  align-items: center;
  
  padding: 10px 15px;
  
  border-bottom: 1px solid #eee;
  
  background-color: #f9f9f9;
  
  margin-bottom: 5px;
  
  border-radius: 4px;
  
}
  
.task-item .task-title {
  
  cursor: pointer;
  
  flex-grow: 1;
  
}
  
.task-item.completed .task-title {
  
  text-decoration: line-through;
  
  color: #888;
  
}
  
.remove-btn {
  
  background-color: #dc3545;
  
  color: white;
  
  border: none;
  
  border-radius: 4px;
  
  padding: 5px 10px;
  
  cursor: pointer;
  
  margin-left: 10px;
  
}
  
</style>
  

components/FilterButtons.vue (子组件)

  
<template>
  
  <div class="filter-buttons">
  
    <button :class="{ active: currentFilter === 'all' }" @click="setFilter('all')">所有任务</button>
  
    <button :class="{ active: currentFilter === 'active' }" @click="setFilter('active')">未完成</button>
  
    <button :class="{ active: currentFilter === 'completed' }" @click="setFilter('completed')">已完成</button>
  
  </div>
  
</template>
  

  
<script setup>
  
import { defineProps, defineEmits } from 'vue';
  

  
const props = defineProps({
  
  currentFilter: {
  
    type: String,
  
    required: true,
  
  },
  
});
  

  
const emit = defineEmits(['set-filter']);
  

  
function setFilter(filterType) {
  
  emit('set-filter', filterType);
  
}
  
</script>
  

  
<style scoped>
  
.filter-buttons button {
  
  background-color: #e2e6ea;
  
  border: 1px solid #dae0e5;
  
  padding: 8px 15px;
  
  margin: 0 5px;
  
  border-radius: 4px;
  
  cursor: pointer;
  
  transition: background-color 0.3s;
  
}
  
.filter-buttons button:hover {
  
  background-color: #d1d7db;
  
}
  
.filter-buttons button.active {
  
  background-color: #007bff;
  
  color: white;
  
  border-color: #007bff;
  
}
  
</style>
  
10.4 进阶挑战
  • 完善功能:增加“编辑任务”、“清空已完成任务”等功能。

  • 本地存储:将任务数据保存到浏览器localStorage,实现数据持久化(刷新页面不丢失)。

  • 真实API集成:如果你已经学习了Node.js/Express后端,尝试将前端任务数据通过axios与后端API进行交互,实现任务的真正持久化存储到数据库。

十一、学习建议与扩展资源:持续精进你的Vue技能

  • 官方文档是最好的老师

  • 动手实践,多做小项目

    • TodoMVC:这是一个经典的框架实现任务管理应用的示例,可以作为参考。

    • 尝试制作一个个人博客的前端界面、一个简单的管理后台界面等。

  • 关注社区与最新动态

    • 掘金、知乎、SegmentFault等技术社区,有很多Vue相关的文章和经验分享。

    • Vue DevTools:Chrome/Firefox浏览器扩展,非常强大的Vue调试工具,可以查看组件层级、数据、Vuex/Pinia状态等。

  • 推荐书籍/课程

    • 《深入浅出Vue.js》:适合深入理解Vue原理。

    • 《Vue.js设计与实现》:更深入Vue 3响应式原理和编译原理。

    • 在线课程:B站、慕课网、极客时间等平台有很多Vue实战课程。

十二、课后练习与思考:挑战你的Vue技能

  1. 完善任务管理应用

    • 增加“编辑任务”功能(点击任务标题后变为可编辑状态,按回车保存)。

    • 增加“清空已完成任务”按钮。

    • 尝试将任务列表数据保存到浏览器**localStorage**,刷新页面后任务不丢失。

  2. 用Vue实现一个在线留言板

    • 要求:用户可以输入留言(姓名、内容),留言显示在列表下方。

    • 支持前端表单校验(非空)。

    • 留言数据存储在Pinia中,并尝试保存到localStorage

  3. 思考题

    • 在Vue中,组件之间常用的通信方式有哪几种?(props/emitprovide/inject、Pinia/Vuex)它们各自适用于什么场景?

    • v-ifv-show指令有什么区别?在什么情况下应该优先选择哪个?

    • computedwatch有什么区别?它们各自的适用场景是什么?