兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!上一节我们全面掌握了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样式。 * **优点**:高度内聚,模块化清晰,便于管理和理解。 * **示例**: ```vue <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:3000`或`localhost: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文本中。 * **示例**: ```vue <template> <p>当前消息:{{ message }}</p> </template> <script> export default { data() { return { message: '你好 Vue' } } } </script> ``` * **指令系统(Directives)**: * **语法**:`v-prefix` * **作用**:Vue提供的一系列特殊属性,以`v-`开头,用于在DOM元素上应用特殊的响应式行为。 * **常用指令**: * **`v-bind`**:**单向绑定**HTML属性到数据。可以简写为冒号`:`。 * **示例**: ```vue <img :src="imgUrl" :alt="imgAlt"> <button :disabled="isDisabled">点击</button> ``` * **`v-model`**:用于表单输入元素和组件上的**双向绑定**。它会根据输入类型自动选取正确的方式更新数据。 * **示例**: ```vue <input v-model="inputValue" type="text"> <p>你输入了:{{ inputValue }}</p> ``` * 当用户在输入框输入时,`inputValue`数据会自动更新。 * 当`inputValue`数据在JS中被修改时,输入框内容也会自动更新。 * **`v-on`**:监听DOM事件,并执行对应的方法。可以简写为`@`。 * **示例**: ```vue <button @click="handleClick">点击我</button> <input @input="handleInput" @keydown.enter="submitForm"> ``` * **`v-if` / `v-else-if` / `v-else`**:条件渲染,根据条件销毁或重建DOM元素(开销较大,但真实)。 * **示例**: ```vue <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`的值应该是列表中每个项的唯一标识。 * **示例**: ```vue <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`。 ```javascript import { ref } from 'vue'; const count = ref(0); // 创建一个响应式的number console.log(count.value); // 访问值 count.value++; // 修改值 ``` * **`reactive()`**:用于处理**对象和数组**,使其成为响应式对象。 * 直接访问/修改属性即可。 ```javascript import { reactive } from 'vue'; const state = reactive({ name: 'Alice', age: 30, hobbies: ['coding', 'reading'] }); console.log(state.name); // 访问属性 state.age++; // 修改属性 state.hobbies.push('hiking'); // 修改数组元素 ``` * **老师提示**:`ref`和`reactive`是Vue 3 Composition API中创建响应式数据的两种主要方式。`ref`更通用,因为它可以包装任何类型的值。 ##### 3.3 计算属性(Computed Properties)与侦听器(Watchers):数据的“派生”与“监控” * **计算属性(`computed`)**: * **作用**:基于已有的响应式数据,派生出新的数据。它的值是**惰性求值**的,只有当它所依赖的响应式数据发生变化时,才会重新计算。计算结果会被缓存。 * **用途**:处理复杂的数据逻辑、格式化数据、筛选数据等。 * **比喻**:你的工资(原始数据)会变化,但你的年薪(计算属性)会根据工资的变化自动计算,并且如果工资不变,年薪就从上次计算的结果中直接拿,不用重新算。 * **示例**: ```vue <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等场景。 * **比喻**:你是一个监控员,当某个指标(数据)发生变化时,你就触发一个报警(副作用操作)。 * **示例**: ```vue <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`选项中显式引入和注册,只在当前父组件及其子组件中可用。 * **优点**:按需引入,减少不必要的打包体积,模块化更清晰。 * **示例**: ```vue <!-- 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`): ```javascript 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`。如果子组件需要修改数据,应该通过事件通知父组件修改。 * **优点**:数据流清晰,易于调试。 * **示例**: ```vue <!-- 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"`来监听。 * **示例**: ```vue <!-- 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”(属性逐层透传)问题。 * **示例**: ```vue <!-- 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)**: * **原理**:对于大型复杂应用,当多个组件(特别是兄弟组件、无直接父子关系的组件)需要共享和修改同一份数据时,传统通信方式会变得非常复杂。全局状态管理模式(如**Pinia**或**Vuex**)提供一个**集中式存储**来管理所有组件的状态。 * **优点**:状态集中,数据流清晰,易于调试和维护。 * **比喻**:就像公司里的“中央数据库”,所有部门都可以从这里获取和更新数据,而不需要部门之间直接打电话。 * **将在路由与状态管理章节详细讲解**。 #### 4.2.3 插槽(Slots):内容的“占位符” * **原理**:允许父组件向子组件的指定位置**插入内容(HTML模板片段)**。这使得组件更加灵活和可组合。 * **类型**: * **默认插槽(Default Slot)**:没有名字的插槽,父组件插入的内容会渲染到子组件`slot`标签所在的位置。 ```vue <!-- 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的核心。 * **特点**: * 接收`props`和`context`作为参数。 * 在`setup`中定义的响应式数据、方法、计算属性等,**必须显式地`return`出去**,才能在模板中使用。 * **没有`this`上下文**:在`setup`函数内部,`this`不再指向组件实例,因为`setup`在组件创建之前执行。 * **响应式API**: * **`ref()`**:用于声明**基本数据类型**(或简单对象)的响应式变量。访问/修改值需要`.value`。 * **`reactive()`**:用于声明**复杂对象或数组**的响应式变量。访问/修改属性无需`.value`。 * **`computed()`**:定义计算属性。 * **`watch()` / `watchEffect()`**:定义侦听器。 * **生命周期钩子**:如`onMounted`、`onUnmounted`等,直接导入并调用。 * **示例:使用Composition API重写计数器组件** ```vue <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`文件中,作为一个普通函数导出。这个函数内部可以使用`ref`、`reactive`、`computed`等Vue响应式API。 * 在其他组件中,只需导入并调用这个函数,即可复用这段逻辑。 * **示例** (`useMousePosition.js`): ```javascript // 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 }; } ``` * 在组件中使用: ```vue <!-- 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`): ```javascript 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`中挂载路由**: ```javascript 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`或其他组件中使用**: ```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`): ```javascript 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**: ```javascript 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**: ```vue <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提供了`XMLHttpRequest`和`Fetch API`,但在Vue项目中,我们通常会使用更专业的库。 ##### 7.1 axios的使用:Promise-based的HTTP客户端 * **axios**: * **含义**:一个流行的、基于Promise的HTTP客户端,可用于浏览器和Node.js。 * **特点**: 1. **基于Promise**:天然支持Promise,便于使用`then/catch`或`async/await`处理异步请求。 2. **拦截器(Interceptors)**:可以全局拦截请求和响应,方便进行统一处理(如添加认证头、处理错误、显示加载动画)。 3. **请求取消、请求超时**。 4. **自动转换JSON数据**。 * **安装**:`npm install axios` * **集成方式**: 1. **全局引入**:直接`import axios from 'axios'`,然后`axios.get(...)`。 2. **封装实例(推荐)**:创建`axios`实例并配置基础URL、拦截器,方便管理和复用。 * **示例:封装`axios`实例** (`utils/request.js`) ```javascript // 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`**: ```vue <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(示例)**: ```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 API`和`axios`)与后端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`, `toggleTaskStatus`等`actions`。 * 包含`filteredTasks`, `uncompletedTasksCount`等`getters`。 * **路由**: * 简单路由:`/` (所有任务), `/active` (未完成任务), `/completed` (已完成任务)。 * **API请求**: * 初期可以**模拟数据**(假数据,不与后端交互),直接在Pinia的`actions`中操作数据。 * **进阶**:使用`axios`在`actions`中模拟异步请求或对接真实的后端API。 ##### 10.3 关键代码片段(简化与示意) **`main.js`** (入口文件) ```javascript 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) ```javascript 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`** (页面组件) ```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`** (子组件) ```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`** (子组件) ```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`** (子组件) ```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技能 * **官方文档是最好的老师**: * [Vue.js官方文档](https://cn.vuejs.org/):非常全面、清晰、易懂。 * [Vue Router官方文档](https://router.vuejs.org/zh/) * [Pinia官方文档](https://pinia.vuejs.org/zh/) * **动手实践,多做小项目**: * 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`/`emit`、`provide`/`inject`、Pinia/Vuex)它们各自适用于什么场景? * `v-if`和`v-show`指令有什么区别?在什么情况下应该优先选择哪个? * `computed`和`watch`有什么区别?它们各自的适用场景是什么? ---
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章