同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!上一节我们全面掌握了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)。
-
优点:
-
代码复用:一次编写,多处使用。
-
降低复杂度:将大问题分解为小问题,便于开发和维护。
-
提高可维护性:组件之间相互独立,修改一个组件通常不会影响其他组件。
-
团队协作:不同成员可以独立开发不同组件。
-
-
比喻:你不再建造一座整体大楼,而是搭建一个个标准化的“乐高积木”,然后用这些积木组装出你想要的复杂结构。
-
-
响应式数据系统(Reactivity System):
-
含义:Vue能够追踪JavaScript数据对象的变化。当数据发生修改时,所有依赖于这些数据的地方(如模板中的绑定、计算属性、侦听器)都会自动收到通知,并触发相应的更新。
-
Vue 2实现:基于
Object.defineProperty来劫持(Hook)对象的属性访问和修改。 -
Vue 3实现:基于ES6的
Proxy对象,提供了更强大、更高效的响应式能力,可以监听对象的所有操作(包括属性的增删),而无需预先遍历所有属性。
-
二、Vue项目结构与开发环境:Vue项目的“骨骼”与“工具”
2.1 单文件组件(.vue文件):Vue的“积木单元”
-
特点:Vue独有的文件格式,将一个组件的所有相关部分(HTML、JavaScript、CSS)集中在一个文件中。
-
结构:一个
.vue文件通常由三部分组成:-
<template>:包含组件的HTML模板结构。 -
<script>:包含组件的JavaScript逻辑(数据、方法、生命周期钩子等)。 -
<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: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文本中。
-
示例:
<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对象**来重写响应式系统。-
优点:
-
更全面的劫持:
Proxy可以直接监听整个对象,包括属性的添加、删除、数组的索引修改和length属性的变化,解决了Vue 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'); // 修改数组元素 -
老师提示:
ref和reactive是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单文件组件中编写即可。 -
注册组件:组件必须先被注册,才能在模板中使用。
-
局部注册(Local Registration):
-
特点:在父组件的
components选项中显式引入和注册,只在当前父组件及其子组件中可用。 -
优点:按需引入,减少不必要的打包体积,模块化更清晰。
-
示例:
<!-- ParentComponent.vue --> <template> <div> <MyButton /> <!-- 使用MyButton组件 --> </div> </template> <script> import MyButton from './MyButton.vue'; // 引入子组件 export default { components: { // 局部注册 MyButton // 注册后才可以在模板中使用 <MyButton /> } }; </script>
-
-
全局注册(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):
-
原理:对于大型复杂应用,当多个组件(特别是兄弟组件、无直接父子关系的组件)需要共享和修改同一份数据时,传统通信方式会变得非常复杂。全局状态管理模式(如Pinia或Vuex)提供一个集中式存储来管理所有组件的状态。
-
优点:状态集中,数据流清晰,易于调试和维护。
-
比喻:就像公司里的“中央数据库”,所有部门都可以从这里获取和更新数据,而不需要部门之间直接打电话。
-
将在路由与状态管理章节详细讲解。
-
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新增):
-
目标:解决上述问题,提供一种更灵活、更强大的方式来组织和复用组件逻辑。它允许你将同一功能相关的逻辑代码(包括数据、方法、计算属性、侦听器等)组织在一起,无论它们来自哪个“选项”。
-
优点:
-
更好的逻辑复用:可以将可复用的逻辑封装成独立的“组合式函数”(Composable functions),在不同组件中复用。
-
更好的代码组织:同一功能的代码聚合在一起,提高了可读性和可维护性。
-
更好的类型推断:对TypeScript支持更友好。
-
更灵活的生命周期钩子:直接在
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重写计数器组件
<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):// 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>组件,用于生成导航链接,避免浏览器刷新。
-
-
安装与使用:
-
安装:
npm install vue-router@4(Vue 3版本) -
配置路由表:通常在
router/index.js中定义。 -
在
main.js中引入并挂载到Vue应用实例。 -
在
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提供了XMLHttpRequest和Fetch API,但在Vue项目中,我们通常会使用更专业的库。
7.1 axios的使用:Promise-based的HTTP客户端
-
axios:
-
含义:一个流行的、基于Promise的HTTP客户端,可用于浏览器和Node.js。
-
特点:
-
基于Promise:天然支持Promise,便于使用
then/catch或async/await处理异步请求。 -
拦截器(Interceptors):可以全局拦截请求和响应,方便进行统一处理(如添加认证头、处理错误、显示加载动画)。
-
请求取消、请求超时。
-
自动转换JSON数据。
-
-
-
安装:
npm install axios -
集成方式:
-
全局引入:直接
import axios from 'axios',然后axios.get(...)。 -
封装实例(推荐):创建
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:在项目根目录运行此命令,打包工具会执行以下操作:-
代码转换:将ES6+语法转换为兼容旧浏览器的ES5。
-
模块打包:将所有JavaScript、CSS、图片等资源打包成优化后的静态文件。
-
代码优化:
-
压缩(Minification):移除空格、注释、缩短变量名,减小文件体积。
-
混淆(Obfuscation):使代码难以阅读,保护源代码。
-
Tree Shaking:移除JavaScript中未被使用的代码,进一步减小体积。
-
代码分割(Code Splitting):将代码分割成多个小块,按需加载,提升首屏加载速度。
-
-
生成结果:通常会生成一个
dist/(或build/)目录,里面包含优化后的HTML、CSS、JavaScript、图片等静态资源。
-
8.2 项目部署流程:让你的网站上线
打包后的dist/目录包含了所有可以部署的静态文件。部署Vue前端项目通常有两种方式:
-
部署到静态服务器(如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集成,可自动部署你的静态站点。
-
-
集成到后端应用:
-
原理:将打包后的
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 (入口文件)
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技能
-
官方文档是最好的老师:
-
Vue.js官方文档:非常全面、清晰、易懂。
-
-
动手实践,多做小项目:
-
TodoMVC:这是一个经典的框架实现任务管理应用的示例,可以作为参考。
-
尝试制作一个个人博客的前端界面、一个简单的管理后台界面等。
-
-
关注社区与最新动态:
-
掘金、知乎、SegmentFault等技术社区,有很多Vue相关的文章和经验分享。
-
Vue DevTools:Chrome/Firefox浏览器扩展,非常强大的Vue调试工具,可以查看组件层级、数据、Vuex/Pinia状态等。
-
-
推荐书籍/课程:
-
《深入浅出Vue.js》:适合深入理解Vue原理。
-
《Vue.js设计与实现》:更深入Vue 3响应式原理和编译原理。
-
在线课程:B站、慕课网、极客时间等平台有很多Vue实战课程。
-
十二、课后练习与思考:挑战你的Vue技能
-
完善任务管理应用:
-
增加“编辑任务”功能(点击任务标题后变为可编辑状态,按回车保存)。
-
增加“清空已完成任务”按钮。
-
尝试将任务列表数据保存到浏览器**
localStorage**,刷新页面后任务不丢失。
-
-
用Vue实现一个在线留言板:
-
要求:用户可以输入留言(姓名、内容),留言显示在列表下方。
-
支持前端表单校验(非空)。
-
留言数据存储在Pinia中,并尝试保存到
localStorage。
-
-
思考题:
-
在Vue中,组件之间常用的通信方式有哪几种?(
props/emit、provide/inject、Pinia/Vuex)它们各自适用于什么场景? -
v-if和v-show指令有什么区别?在什么情况下应该优先选择哪个? -
computed和watch有什么区别?它们各自的适用场景是什么?
-