Vue2
文章目录
- Vue2
-
- 一:Vue2概述
-
- [1:hello world](#1:hello world)
- 2:数据响应式
- 3:vue基础指令
- 4:computed/watch/生命周期
- 二:工程化
-
- [1:vue cli脚手架](#1:vue cli脚手架)
- 2:核心文件和组件
- 3:组件通信
-
- 3.1:父子组件通信
- 3.2:非父子组件的通信
- 3.3:表单类组件的封装
- [3.4:ref和this.ref](#3.4:ref和this.ref)
- 4:插槽
- 5:路由和VueRouter
- 6:数据共享vuex
- 7:element组件库的使用
一:Vue2概述
1:hello world
vue是一个用于构建用户界面的渐进式框架
- 构建用户界面:基于数据动态渲染页面
- 渐进式:循序渐进的学习
- 框架:一套完整的项目解决方案,提升开发效率
创建Vue实例的4步:
- 准备一个容器
- 引入vue的包
- 创建vue的实例 -
new Vue() - 指定配置项 - el指定挂载点和data提供数据
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 步骤1:准备容器,到时候vue配置项的数据会在这里渲染 -->
<div id="app">
{{ msg }}
</div>
<!-- 步骤2:引入vue包 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
// 步骤3:创建Vue实例
const app = new Vue({
el: '#app', // 指定挂载点是id=app的容器, 也就是指定vue管理的是那个盒子
data: {
msg: "你好,世界" // 提供对应的数据,将会渲染到上面的挂载点
}
})
</script>
</body>
</html>
上面使用的{``{}}就是插值表达式,它是vue的一种模板语法,利用表达式进行插值,将数据渲染到页面中
注意插值表达式支持的只有表达式,不能用在if, for和标签属性
可以在浏览器中下载vue.js devtools这个插件,然后进入详情,打开允许访问文件地址开关,方便后面学习调试。
2:数据响应式
Vue的核心特性之一是响应式:数据变化,视图自动的更新
因为data中的数据是会被添加到实例上的,所以:
- 对于属性的访问:
实例.属性名 - 对于属性的修改:
实例.属性名 = 新值
html
<body>
<!-- 步骤1:准备容器,到时候vue配置项的数据会在这里渲染 -->
<div id="app">
{{ msg }}
</div>
<!-- 步骤2:引入vue包 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
// 步骤3:创建Vue实例
const app = new Vue({
el: '#app', // 指定挂载点是id=app的容器
data: {
msg: "你好,世界" // 提供对应的数据,将会渲染到上面的挂载点
}
})
console.log(app.msg); // 注意不是app.data.msg, 是app.msg
// 定时, 2秒后修改属性,可以发现视图自动变换
setTimeout(() => {
app.msg = "你好, vue";
}, 2000);
</script>
</body>
一个完整的vue2实例对象结构
js
const vm = new Vue({
el: '#app',
data() {
// 数据相关
}
},
computed: {
//计算属性
},
watch: {
message(newVal, oldVal) {
// 检测属性
}
},
methods: {
// 方法
},
created() {
// 生命周期方法
}
})
js
const vm = new Vue({
el: '#app', // 挂载目标
// ===================== 1. data() =====================
data() {
return {
message: 'Hello', // 响应式数据
count: 0, // 状态数据
list: [], // 数组数据
user: {} // 对象数据
}
},
// 作用:定义组件的响应式数据
// 特点:必须是函数(避免组件复用时的数据共享问题)
// 数据变化会自动触发视图更新
// ===================== 2. computed =====================
computed: {
// 计算属性 - 基于现有数据计算新值
reversedMessage() {
// 自动缓存,依赖的 data 变化时才重新计算
return this.message.split('').reverse().join('')
},
// 带 getter/setter 的计算属性
fullName: {
get() { return this.firstName + ' ' + this.lastName },
set(val) {
const names = val.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
},
// 作用:声明式地定义派生数据
// 特点:具有缓存、依赖追踪、可读写
// ===================== 3. watch =====================
watch: {
// 监听数据变化,执行异步或复杂操作
message(newVal, oldVal) {
console.log('消息从', oldVal, '变为', newVal)
// 适合:数据变化时执行异步请求、复杂逻辑
},
// 深度监听对象
user: {
handler(newVal) {
console.log('用户信息变化')
},
deep: true, // 深度监听对象内部变化
immediate: true // 立即执行一次
},
// 监听计算属性
'computedProperty'(newVal) {
console.log('计算属性变化')
}
},
// 作用:响应数据变化,执行副作用
// 特点:更灵活,适合异步操作
// ===================== 4. methods =====================
methods: {
// 事件处理方法
increment() {
this.count++ // 可修改数据
},
// 复杂逻辑处理
fetchData() {
// 可以调用其他方法
this.processData(this.data)
},
// 带参数的方法
updateMessage(newMsg) {
this.message = newMsg
}
},
// 作用:定义组件方法
// 特点:在模板中可直接调用,可修改数据,无缓存
// ===================== 5. 生命周期钩子(如 created) =====================
created() {
// 实例创建完成后调用
// 特点:可以访问 this,但 DOM 还未生成
console.log('组件已创建')
this.fetchData() // 初始化数据
this.setupEvents() // 设置事件监听
},
// 其他常用生命周期钩子:
mounted() {
// DOM 挂载后调用
// 适合:操作 DOM、初始化第三方库
},
updated() {
// 数据变化导致 DOM 更新后调用
// 注意:避免在此修改数据,可能导致无限循环
},
destroyed() {
// 实例销毁前调用
// 适合:清理定时器、取消事件监听
}
})
3:vue基础指令
vue会根据不同的指令,针对标签实现不同的功能,所谓的指令,就是带有v-前缀的特殊标签属性
熟练几个常用的,剩下边用边搜,要学会使用官方文档
| 指令 | 说明 |
|---|---|
v-text |
等价于插值表达式{``{}},更新元素的textContent |
v-html |
更新元素的 innerHTML,内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译 |
v-show |
根据表达式之真假值,切换元素的 display CSS property。 |
v-if |
根据表达式的值的来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建 |
v-else |
前一兄弟元素必须有 v-if 或 v-else-if |
v-else-if |
表示 v-if 的else if 块。可以链式调用。 |
v-for |
基于源数据多次渲染元素或模板块。 此指令之值,必须使用特定语法 alias in expression,为当前遍历的元素提供别名 |
v-on |
缩写是@, 绑定事件监听器。事件类型由参数指定 |
v-bind |
缩写是:, 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式 |
v-model |
随表单控件类型不同而不同 |
下面是其他需要重点注意的事项
3.1:v-if/v-show
- v-if:是"真正的"条件渲染,元素会在条件为真时被创建,条件为假时从 DOM 中完全移除
- v-show:只是切换 CSS 的
display: none属性,无论条件真假,元素始终存在于 DOM 中。
因此v-if的开销比较高,适用于不经常切换的场景(如一次性条件判断);而v-show:适合频繁切换的场景(如标签页切换、折叠面板)
v-if:可以在 <template> 标签上使用 / v-show:不能在 <template> 标签上使用
3.2:v-for
必须绑定
:key
使用 v-for 时必须为每个项提供一个唯一的 key 属性,这有助于 Vue 高效地更新虚拟 DOM。
key必须是字符串或者数字
html
<!-- 正确:使用唯一 key -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- 避免:使用索引作为 key(除非列表简单且不会重新排序) -->
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
</li>
在同一元素上同时使用
v-for和v-if会导致性能问题,因为v-for的优先级更高。
html
<!-- 不推荐:v-for 和 v-if 在同一元素 -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
<!-- 推荐方案1:使用计算属性过滤 -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
<!-- 推荐方案2:使用 template 包裹 -->
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
</li>
</template>
vue可以见到如下数组方法的变化
push, pop, shift, unshift, splice, sort, reverse, 这些方法vue都可以实时看到并响应
js
// Vue 能检测到这些变化
this.items.push(newItem)
this.items.splice(index, 1)
// Vue 不能检测到这些变化
this.items[index] = newItem // ✗ 不会触发更新
this.items.length = 0 // ✗ 不会触发更新
3.3:v-on
html
<!-- 完整写法 -->
<button v-on:click="handleClick">点击我</button>
<!-- 简写形式(推荐) -->
<button @click="handleClick">点击我</button>
<!-- 内联表达式 -->
<button @click="count++">增加 {{ count }}</button>
可以传递原生事件对象或者直接调用方法
html
<!-- 直接调用方法 -->
<button @click="sayHello('小明')">打招呼</button>
<!-- 传递原生事件对象 -->
<button @click="sayHello('小明', $event)">打招呼</button>
<!-- 多个操作 -->
<button @click="count++; logClick($event)">点击</button>
vue还提供了事件修饰符来处理DOM事件的细节
| 事件修饰符 | 作用 |
|---|---|
.prevent |
阻止默认行为 |
.stop |
阻止事件冒泡 |
.once |
事件只触发一次 |
.enter / .esc / .ctrl.enter /... |
按键类事件 |
.left / .right / .middle |
鼠标类事件 |
html
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit">
<button type="submit">提交</button>
</form>
<!-- 阻止事件冒泡 -->
<div @click="outerClick">
<button @click.stop="innerClick">点击我</button>
</div>
<!-- 事件只触发一次 -->
<button @click.once="doSomething">只执行一次</button>
<!-- 串联修饰符 -->
<form @submit.prevent.stop="onSubmit"></form>
<!-- 回车键 -->
<input @keyup.enter="submit">
<!-- 常用按键别名 -->
<input @keyup.enter="submit"> <!-- Enter -->
<input @keyup.esc="clear"> <!-- Esc -->
<input @keyup.tab="nextField"> <!-- Tab -->
<input @keyup.delete="deleteItem"><!-- Delete -->
<input @keyup.space="play"> <!-- Space -->
<!-- 系统修饰键 -->
<input @keyup.ctrl.enter="submit"> <!-- Ctrl + Enter -->
<input @keyup.alt.enter="submit"> <!-- Alt + Enter -->
<input @keyup.shift.enter="submit"> <!-- Shift + Enter -->
<input @keyup.meta.enter="submit"> <!-- Command/Win + Enter -->
<!-- 精确修饰符(只有该键被按下时触发) -->
<button @click.ctrl.exact="ctrlClick">Ctrl + 点击</button>
<button @click.exact="justClick">仅点击,无修饰键</button>
<!-- 鼠标按钮修饰符 -->
<button @click.left="leftClick">左键</button>
<button @click.right="rightClick">右键</button>
<button @click.middle="middleClick">中键</button>
3.4:v-bind
html
<!-- 完整写法 -->
<a v-bind:href="url">链接</a>
<img v-bind:src="imageSrc" alt="图片">
<!-- 简写形式(推荐) -->
<a :href="url">链接</a>
<img :src="imageSrc" alt="图片">
<!-- 动态属性名 -->
<button :[attributeName]="value">按钮</button>
html
<body>
<div id="root">
<button v-show="index > 0" @click="index--">上一张图片</button>
<div>
<img :src="image_list[index]">
</div>
<button v-show="index < image_list.length - 1" @click="index++">下一张图片</button>
</div>
<script>
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
data:{
index: 0, // 初始的index = 0
image_list: [
"./image/01.png",
"./image/02.png",
"./image/03.png",
"./image/04.png",
"./image/05.png",
]
}
})
</script>
</body>
为了方便开发者进行样式控制,Vue 扩展了 v-bind 的语法
可以针对 class 类名 和 style 行内样式 进行控制,语法是::class="对象或者数组"
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>:class测试</title>
<!-- 引入vue.js -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style>
.box {
width: 500px;
height: 500px;
border: 3px solid red;
font-size: 30px;
margin-top: 10px; /* 上侧外边距 - 10px */
}
.pink {
color: pink;
}
.bigFont {
font-size: 50px;
}
</style>
</head>
<body>
<!-- 列表渲染,删除任务功能,添加任务功能,清空任务,底部统计 -->
<div id="app">
<!-- <div class = "box" :class="{pink: true, bigFont: false}">-->
<!-- 测试一下.class-->
<!-- </div>-->
<div class="box" :class="['pink', 'bigFont']">
测试一下.class
</div>
</div>
<script>
new Vue({
el: '#app',
})
</script>
</body>
</html>
3.5:v-model
给表单元素使用,双向数据绑定
可以快速获取或设置表单元素内容:数据变化 <-> 视图变化 互相影响
html
<!-- 文本输入框 -->
<input v-model="message" placeholder="编辑我">
<p>输入的内容是: {{ message }}</p>
<!-- 多行文本 -->
<textarea v-model="message"></textarea>
<!-- 复选框 -->
<input type="checkbox" v-model="checked">
<label>复选框: {{ checked }}</label>
<!-- 多个复选框 -->
<input type="checkbox" value="篮球" v-model="hobbies">
<input type="checkbox" value="足球" v-model="hobbies">
<input type="checkbox" value="排球" v-model="hobbies">
<p>选择的爱好: {{ hobbies }}</p>
<!-- 单选框 -->
<input type="radio" value="男" v-model="gender">
<input type="radio" value="女" v-model="gender">
<p>选择的性别: {{ gender }}</p>
<!-- 下拉选择框 -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option value="A">选项A</option>
<option value="B">选项B</option>
<option value="C">选项C</option>
</select>
<p>选择的选项: {{ selected }}</p>
<!-- 多选下拉框 -->
<select v-model="selectedItems" multiple>
<option value="A">选项A</option>
<option value="B">选项B</option>
<option value="C">选项C</option>
</select>
<p>选择的项目: {{ selectedItems }}</p>
js
data() {
return {
message: '', // 文本
checked: false, // 复选框
hobbies: [], // 多个复选框
gender: '', // 单选框
selected: '', // 下拉框
selectedItems: [] // 多选下拉框
}
}
v-model的本质是语法糖,它结合了下面两个vue指令
v-bind:将数据绑定到元素的 value 属性(数据 -> 视图)v-on:监听输入事件来更新数据(视图 -> 数据)
对应v-bind属性和v-on事件列表如下:
| 元素类型 | 绑定的属性 | 监听的事件 | 值类型 |
|---|---|---|---|
<input type="text"> |
value |
input |
字符串 |
<textarea> |
value |
input |
字符串 |
<input type="checkbox"> |
checked |
change |
布尔值 |
多个<input type="checkbox"> |
value |
change |
数组 |
<input type="radio"> |
checked |
change |
字符串 |
<select> |
value |
change |
字符串 |
<select multiple> |
value |
change |
数组 |
html
<!-- v-model 语法糖 -->
<input v-model="message">
<!-- 等价于 -->
<input
:value="message"
@input="message = $event.target.value"
>
<!-- 对于组件 -->
<custom-input v-model="message">
<!-- 等价于 -->
<custom-input
:value="message"
@input="message = $event"
>
v-model常用修饰符
| 修饰符 | 说明 |
|---|---|
.lazy |
将input 事件转为 change 事件 |
.number |
自动将输入转为数字 |
.trim |
自动去除首尾空格 |
html
<!-- 默认:每次输入都更新(input事件) -->
<input v-model="message" placeholder="实时更新">
<!-- 使用 .lazy:失去焦点或回车时更新(change事件) -->
<input v-model.lazy="message" placeholder="失焦时更新">
<!-- 默认:获取的是字符串 -->
<input v-model="age" type="number">
<!-- 输入 "25" → this.age === "25"(字符串) -->
<!-- 使用 .number:转为数字类型 -->
<input v-model.number="age" type="number">
<!-- 输入 "25" → this.age === 25(数字) -->
<!-- 去除输入内容的首尾空格 -->
<input v-model.trim="username" placeholder="输入用户名">
<!-- 输入 " 张三 " → this.username === "张三" -->
3.6:小黑记事本
一个小的整合案例
html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小黑记事本</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<link rel="stylesheet" href="test.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div id="app">
<div class="todo-app">
<!-- 标题 -->
<div class="header">
<h1><i class="fas fa-book"></i> 小黑记事本</h1>
</div>
<!-- 输入区域 -->
<div class="input-section">
<!-- type属性表示表单的类型是输入框 -->
<!-- v-model表示双向绑定,绑定vue实例中的newTask变量 -->
<!-- 当键盘输入enter的时候触发vue实例中的addTask方法 -->
<!-- placeholder表示输入框的默认值 -->
<!-- class属性用于渲染和定位 -->
<input type="text" v-model="newTask" @keyup.enter="addTask" placeholder="请输入任务" class="task-input">
<!-- 点击按钮也是触发vue实例中的addTask方法 -->
<button @click="addTask" class="add-btn">
<i class="fas fa-plus"></i> 添加任务
</button>
</div>
<!-- 任务列表 -->
<div class="task-list" v-if="tasks.length > 0">
<!-- v-for循环任务列表tasks, tasks是vue实例data中的列表对象, key是index -->
<div class="task-item" v-for="(task, index) in tasks" :key="index">
<span class="task-number">{{ index + 1 }}.</span>
<span class="task-content">{{ task }}</span>
<!-- 点击按钮触发删除任务的操作 -->
<button @click="deleteTask(index)" class="delete-btn">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else>
<i class="fas fa-clipboard-list"></i>
<p>暂无任务,请添加任务</p>
</div>
<!-- 底部统计和操作 -->
<div class="footer">
<!-- 赋值表达式 -->
<div class="stats">
合计: <span class="count">{{ tasks.length }}</span>
</div>
<!-- 点击的时候触发clearAll方法 -->
<!-- 当列表长度是0的时候disabled -->
<button @click="clearAll" class="clear-btn" :disabled="tasks.length === 0">
<i class="fas fa-trash-alt"></i> 清空任务
</button>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
newTask: '',
tasks: [
'跑步锻炼20分钟',
'复习数组语法'
]
}
},
methods: {
// 添加任务
addTask() {
if (this.newTask.trim() === '') {
alert('请输入任务内容!');
return;
}
// push方法 -> vue可见并实时响应
this.tasks.push(this.newTask.trim());
this.newTask = '';
},
// 删除单个任务
deleteTask(index) {
if (confirm('确定要删除这个任务吗?')) {
this.tasks.splice(index, 1);
}
},
// 清空所有任务
clearAll() {
if (this.tasks.length === 0) return;
if (confirm('确定要清空所有任务吗?')) {
this.tasks = [];
}
}
},
// 生命周期
mounted() {
// 页面加载后让输入框自动获得焦点
document.querySelector('.task-input').focus();
}
});
</script>
</body>
</html>
css
css
/* 整体样式设计 */
* {
margin: 0; /* 清除所有元素的默认外边距 */
padding: 0; /* 清除所有元素的默认内边距 */
box-sizing: border-box; /* 现代盒子,将内边距和边框计入元素的总宽度和高度中,简化布局计算 */
}
body {
font-family: 'Arial', 'Microsoft YaHei', sans-serif; /* 设置字体,优先使用 Arial,其次是微软雅黑,最后是系统默认无衬线字体 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* 设置背景为135度角、从左到右(#667eea 到 #764ba2)的线性渐变 */
min-height: 100vh; /* 最小高度为视口高度的100%,确保背景铺满全屏 */
display: flex; /*启用弹性盒子布局 */
justify-content: center; /* 水平居中对齐子元素*/
align-items: center; /* 垂直居中对齐子元素 */
padding: 20px; /* 设置内边距,在屏幕边缘留出一些空间 */
}
.todo-app {
width: 100%; /* 宽度占满可用空间 */
max-width: 500px; /* 最大宽度限制为 500px,防止在大屏幕上过宽 */
background: white; /* 设置背景颜色为白色 */
border-radius: 15px; /* 设置圆角边框 */
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); /* 添加阴影效果,X偏移0、Y偏移10px、模糊30px、颜色为半透明黑色 */
overflow: hidden; /* 隐藏超出容器圆角边界的内容 */
}
/* 标题样式 */
.header {
background: linear-gradient(135deg, #2c3e50 0%, #4a6491 100%); /* 头部背景渐变(深蓝色系)*/
color: white; /* 文字颜色设为白色 */
padding: 25px 20px; /* 上下内边距25px,左右内边距20px */
text-align: center; /* 文字水平居中 */
}
.header h1 {
font-size: 24px; /* 标题字体大小 */
font-weight: 600; /* 字体粗细为 semi-bold */
display: flex; /* 启用弹性盒子布局 */
align-items: center; /* 垂直居中对齐子元素 */
justify-content: center; /* 水平居中对齐子元素 */
gap: 10px; /* 设置子元素之间的间距为 10px */
}
.header h1 i {
color: #ffd700; /* 图标颜色设为金色 */
}
/* 输入区域 */
.input-section {
padding: 25px; /* 内边距 */
border-bottom: 2px solid #f0f0f0; /* 底部边框,浅灰色 */
display: flex; /* 启用弹性盒子布局 */
gap: 10px; /* 子元素之间的间距 */
}
.task-input {
flex: 1; /* 占据剩余的所有可用空间 */
padding: 14px 18px; /* 内边距:上下14px,左右18px */
border: 2px solid #e0e0e0; /* 边框:2px宽的浅灰色实线 */
border-radius: 10px; /* 圆角边框 */
font-size: 16px; /* 字体大小 */
transition: all 0.3s; /* 所有 CSS 属性的变化在 0.3 秒内平滑过渡 */
outline: none; /* 移除默认的聚焦轮廓线 */
}
.task-input:focus {
border-color: #667eea; /* 聚焦时边框颜色变为主题蓝色 */
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); /* 聚焦时添加淡蓝色发光效果 */
}
.add-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* 按钮背景渐变(主题色)*/
color: white; /* 文字颜色 */
border: none; /* 无边框 */
padding: 0 25px; /* 左右内边距25px,上下为0 */
border-radius: 10px; /* 圆角 */
cursor: pointer; /* 鼠标悬停时显示手形指针 */
font-size: 16px; /* 字体大小 */
font-weight: 600; /* 字体粗细 */
display: flex; /* 启用弹性盒子布局 */
align-items: center; /* 垂直居中对齐子元素(图标和文字) */
gap: 8px; /* 图标和文字之间的间距 */
transition: all 0.3s; /* 所有属性变化过渡效果 */
}
.add-btn:hover {
transform: translateY(-2px); /* 鼠标悬停时按钮向上轻微移动2px */
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); /* 鼠标悬停时添加阴影,增强立体感 */
}
.add-btn:active {
transform: translateY(0); /* 按钮被点击时,取消向上移动,恢复原位置 */
}
/* 任务列表 */
.task-list {
padding: 0 25px; /* 左右内边距25px,上下为0 */
}
.task-item {
display: flex; /* 启用弹性盒子布局 /
align-items: center; /* 垂直居中对齐子元素 */
padding: 18px 0; /* 上下内边距18px,左右为0 */
border-bottom: 1px solid #f0f0f0; /* 底部边框,1px宽的浅灰色实线 */
animation: fadeIn 0.3s ease-out; /* 应用名为 'fadeIn'、时长0.3秒、缓动函数为 ease-out 的动画 */
}
.task-item:last-child {
border-bottom: none; /* 移除最后一个任务项底部的边框 */
}
.task-number {
color: #667eea; /* 数字颜色为主题蓝色 */
font-weight: bold; /* 字体加粗 */
min-width: 30px; /* 设置最小宽度,确保数字对齐 */
font-size: 16px; /* 字体大小 */
}
.task-content {
flex: 1; /* 占据剩余的所有可用空间 */
font-size: 16px; /* 字体大小 */
color: #333; /* 文字颜色为深灰色 */
word-break: break-word; /* 允许长单词或URL在任意字符间断行,防止内容溢出 */
padding: 0 15px; /* 左右内边距15px,上下为0 */
}
.delete-btn {
background: #ff6b6b; /* 背景色为警告红色 */
color: white; /* 图标颜色为白色 */
border: none; /* 无边框 */
width: 35px; /* 固定宽度 */
height: 35px; /* 固定高度,形成圆形按钮 */
border-radius: 50%; /* 50%的圆角,形成圆形 */
cursor: pointer; /* 鼠标悬停时显示手形指针 */
display: flex; /* 启用弹性盒子布局 */
align-items: center; /* 垂直居中对齐子元素(图标) */
justify-content: center; /* 水平居中对齐子元素(图标) */
transition: all 0.3s; /* 所有属性变化过渡效果 */
}
.delete-btn:hover {
background: #ff5252; /* 鼠标悬停时背景色变深(更深的红色) */
transform: scale(1.1); /* 鼠标悬停时按钮放大到1.1倍 */
}
.delete-btn:active {
transform: scale(0.95); /* 按钮被点击时缩小到0.95倍,模拟按压效果 */
}
/* 空状态(当没有任务时显示的提示) */
.empty-state {
text-align: center; /* 文字居中 */
padding: 50px 25px; /* 内边距:上下50px,左右25px */
color: #999; /* 文字颜色为浅灰色 */
}
.empty-state i {
font-size: 60px; /* 图标大小 */
margin-bottom: 15px; /* 图标底部外边距 */
opacity: 0.5; /* 图标半透明 */
}
.empty-state p {
font-size: 18px; /* 提示文字大小 */
color: #666; /* 提示文字颜色为灰色 */
}
/* 底部 */
.footer {
background: #f8f9fa; /* 背景为浅灰色 */
padding: 20px 25px; /* 内边距:上下20px,左右25px */
display: flex; /* 启用弹性盒子布局 */
justify-content: space-between; /* 子元素在主轴(水平方向)两端对齐,之间平均分配空间 */
align-items: center; /* 垂直居中对齐子元素 */
border-top: 2px solid #f0f0f0; /* 顶部边框,2px宽的浅灰色实线 */
}
.stats {
font-size: 16px; /* 统计文字大小 */
color: #666; /* 统计文字颜色为灰色 */
}
.count {
font-weight: bold; /* 任务数量数字加粗 */
color: #667eea; /* 数字颜色为主题蓝色 */
font-size: 20px; /* 数字字体稍大 */
margin-left: 5px; /* 数字左侧外边距 */
}
.clear-btn {
background: #ff6b6b; /* 背景色为警告红色 */
color: white; /* 文字颜色为白色 */
border: none; /* 无边框 */
padding: 12px 25px; /* 内边距:上下12px,左右25px */
border-radius: 10px; /* 圆角边框 */
cursor: pointer; /* 鼠标悬停时显示手形指针 */
font-size: 16px; /* 字体大小 */
font-weight: 600; /* 字体粗细 */
display: flex; /* 启用弹性盒子布局 */
align-items: center; /* 垂直居中对齐子元素(图标和文字) */
gap: 8px; /* 图标和文字之间的间距 */
transition: all 0.3s; /* 所有属性变化过渡效果 */
}
.clear-btn:hover:not(:disabled) {
background: #ff5252; /* 鼠标悬停且按钮未禁用时,背景色变深 */
transform: translateY(-2px); /* 鼠标悬停时向上轻微移动 */
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3); /* 添加红色调阴影 */
}
.clear-btn:disabled {
background: #cccccc; /* 按钮禁用时背景为灰色 */
cursor: not-allowed; /* 按钮禁用时鼠标显示禁用样式 */
opacity: 0.6; /* 按钮禁用时降低不透明度 */
}
.clear-btn:active:not(:disabled) {
transform: translateY(0); /* 按钮被点击且未禁用时,取消向上移动,恢复原位置 */
}
/* 动画 */
@keyframes fadeIn {
from {
opacity: 0; /* 动画开始时完全透明 */
transform: translateY(10px); /* 动画开始时向下偏移10px */
}
to {
opacity: 1; /* 动画结束时完全不透明 */
transform: translateY(0); /* 动画结束时回到原始位置 */
}
}
/* 响应式设计 - 适配小屏幕设备(如手机) */
@media (max-width: 600px) {
.todo-app {
margin: 10px; /* 在小屏幕上,应用容器周围添加10px外边距 */
border-radius: 12px; /* 稍微减小圆角半径 */
}
.header {
padding: 20px 15px; /* 减少头部内边距 */
}
.input-section {
padding: 20px; /* 减少输入区域内边距 */
flex-direction: column; /* 将子元素(输入框和按钮)垂直排列 */
}
.add-btn {
width: 100%; /* 按钮宽度占满容器 */
justify-content: center; /* 按钮内容居中对齐 */
padding: 15px; /* 调整按钮内边距 */
}
.task-item {
padding: 15px 0; /* 减少任务项的内边距 */
}
.footer {
flex-direction: column; /* 将底部子元素(统计和按钮)垂直排列 */
gap: 15px; /* 子元素之间添加间距 */
text-align: center; /* 文字居中对齐 */
}
.clear-btn {
width: 100%; /* 按钮宽度占满容器 */
justify-content: center; /* 按钮内容居中对齐 */
}
}

4:computed/watch/生命周期
4.1:computed/watch
一些属性是需要其他属性计算出来的,此时就需要用到computed, 例如合计这个total变量属性,就需要列表变量的length确定。可以自动追踪响应式依赖,只有被使用时才会计算,可以使用getter/setter
而watch属性用于监听响应式数据的变化,能拿到数据变化前后的值。可以监听 data、computed、props 等
| 特性 | Computed | Watch |
|---|---|---|
| 主要用途 | 计算衍生数据 | 监听数据变化执行操作 |
| 缓存 | 有缓存,依赖不变不重算 | 无缓存,每次变化都执行 |
| 同步/异步 | 只能同步 | 可以处理异步操作 |
| 返回值 | 必须返回值 | 无返回值 |
| 使用场景 | 模板显示计算值 | 数据变化时执行逻辑 |
js
computed: {
// 只读计算属性
fullName() {
return this.firstName + ' ' + this.lastName;
},
// 可读写的计算属性
computedValue: {
get() {
return this.value * 2;
},
set(newValue) {
this.value = newValue / 2;
}
}
}
watch: {
// 监听简单值
message(newVal, oldVal) {
console.log('消息变化了:', newVal);
},
// 深度监听对象
user: {
handler(newVal) {
console.log('用户信息变化了');
},
deep: true, // 深度监听
immediate: true // 立即执行一次
},
// 监听对象特定属性
'user.name': function(newVal) {
console.log('用户名变化了:', newVal);
}
}
4.2:生命周期

created
- 时机:实例创建完成,数据已观测
- 用途:数据初始化、API调用
- 注意:
$el还不存在($el是 Vue 实例的一个只读属性,表示 Vue 实例挂载的DOM 元素。)
mounted
- 时机:实例已挂载到DOM
- 用途:DOM操作、第三方库初始化
beforeDestroy
- 时机:实例销毁之前
- 用途:清理定时器、解绑事件
二:工程化
🔴 vue2的工程化已经渐渐淘汰,因为使用的是webpack, 现在都是vue3 + vite
核心包传统开发模式:基于html/css/js文件,直接引入核心包,开发Vue。
工程化开发模式:基于构建工具(例如:webpack)的环境中开发Vue。
1:vue cli脚手架
Vue CLI是Vue官方提供的一个全局命令工具,可以帮助我们快速创建一个开发Vue项目的标准化基础架子
Vue CLI集成了webpack配置,下面以npm为例:
1️⃣ 全局安装(一次):npm i @vue/cli -g,然后查看Vue版本:vue --version
2️⃣ 创建项目架子:先进入到指定文件夹,然后vue create project-name(项目名-不能用中文)
3️⃣ 启动项目:npm run serve(找package.json)
启动之后,就可以看到如下界面

下面是脚手架文件介绍:

2:核心文件和组件
2.1:工程核心文件
上面标绿的3个文件是核心文件,是整个工程的"根",分别是main.js / App.vue / index.html
index.html -> 模板文件
html
<body>
<!-- 给不支持JS的浏览器一个提示 -->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 挂载点,启动后将自动使用这个容器为vue的挂载点 -->
<!-- Vue管理的容器,将来创建结构动态渲染 -->
<div id="app">
<!-- 工程化中,这里不在直接编写 -->
<!-- 构建的文件将被自动注入 -->
</div>
</body>
main.js -> 入口文件,打包或者运行,第一个执行的文件
js
// 核心作用就是导入App.vue, 根据App.vue创建结构渲染index.html
// 1:导入vue核心包
import Vue from 'vue'
// 2:导入App.vue
import App from './App.vue'
// 阻止启动生产消息
Vue.config.productionTip = false
// 创建vue实例
new Vue({
// 提供render方法 -> 基于App.vue创建结构渲染index.html
render: h => h(App),
}).$mount('#app') // $mount指定挂载点为index.html中id=app的容器
App.vue -> 根组件,index.html加载这个组件,这个组件中加载其他组件
vue后缀的文件被称为vue组件,组件有三大部分构成,其实就是html + css + JS三合一文件
| 部分 | 说明 |
|---|---|
<template> |
在这里写结构 |
<style> |
在这里写样式 |
<script> |
在这里写逻辑行为 |

html
<template>
<!-- 在template中写html结构,这里可以使用其他的组件 -->
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<!-- 步骤3:组件使用,并传递msg参数 -->
<HelloWorld msg="hello vue2"/>
</div>
</template>
<script>
// 在script标签中写js, 一般是声明使用的组件
// 然后使用export default引入其他的组件,可以在上面html中使用
import HelloWorld from './components/HelloWorld.vue' // 步骤1:组件声明
// 步骤2:组件引入
// export default -> 全部导出,这样其他的文件才能用这些信息
// 可以写components -> 引用了哪些组件(子组件有哪些)
// 可以写name, props, data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
name: 'App',
components: {
HelloWorld // 声明使用的子组件
}
}
</script>
<style>
/* 这里声明样式,指定class = app的样式如下 */
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
HelloWorld.vue - 子组件,注意他和根组件App.vue的配合
html
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
// 属性导出,暴露出去,其他组件才能使用
// 可以写data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
name: 'HelloWorld', // 声明组件的名称
props: { // props接收父组件传过来的参数
msg: String // 声明属性,msg类型为String
}
}
</script>
<style scoped>
/* 这里写样式 */
</style>

2.2:组件化开发
通过上面可以看到,整个项目工程的根组件只有App.vue, 服务只会将App.vue渲染到index.html中

这就涉及到后面的重点,组件交互
2.3:组件注册
局部注册 -> 只能在注册的组件内使用
第一步:创建vue文件,例如模板工程的HelloWorld.vue,写好三个部分
第二步:在使用的组件内导入(import...from...)并注册(export default { components中 }),可见App.vue的<script>部分
html
<script>
// 在script标签中写js, 一般是声明使用的组件
// 然后使用export default引入其他的组件,可以在上面html中使用
import HelloWorld from './components/HelloWorld.vue' // 步骤1:组件声明
// 步骤2:组件引入
// export default -> 全部导出,这样其他的文件才能用这些信息
// 可以写data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
name: 'App',
components: {
HelloWorld // 声明使用的子组件
}
}
</script>
全局注册 -> 所有的组件中都能使用
第一步:创建vue文件,例如叫做CommonUseButton.vue,写好三个部分
第二步:在main.js这个入口文件中全局注册该组件,然后在任意的组件中都可以使用
js
import Vue from 'vue'
import App from './App.vue' // 声明App.vue为App
import CommonUseButton from "@/components/CommonUseButton.vue"; // 1:导入全局组件
Vue.config.productionTip = false // 阻止启动生产消息
// 2:进行全局注册(组件名:组件对象)
Vue.component("CommonUseButton", CommonUseButton);
new Vue({
render: h => h(App), // 创建一个Vue实例, 将App组件渲染到id为app的div上
}).$mount('#app')
🚀 通过上面就可以得到也买你开发的思路:例如
- 分析页面,按模块拆分组件,搭架子(局部或全局注册)
- 根据设计图,编写组件html结构css样式(已准备好)
- 拆分封装通用小组件(局部或全局注册)
- 将来 → 通过jS动态渲染,实现功能
3:组件通信
组件间的数据传递是如何实现的呢?
组件的数据是独立的,无法直接访问其他组件的数据。要想要使用其他组件的数据,就是组件通信
3.1:父子组件通信
🎉 回顾下上面说的v-model语法糖是如何实现双向绑定的,父子组件通信和它很像
v-bind:将数据绑定到元素的 value 属性(数据 -> 视图)v-on:监听输入事件来更新数据(视图 -> 数据)
html
<template>
<div id="app">
<!-- 当你写了这个 -->
<input v-model="msg" type="text">
<!-- 等价于下面这个 -->
<input :value="msg" @input="msg = $event.target.value" type=text>
</div>
</template>
因为数据是在父组件中定义的,父组件调用子组件,可以将父组件类比成上面的数据,子组件类比成视图。
父组件 -> 子组件传递数据 ->
v-bind
- 父组件在
<script>的data()中声明变量 - 在调用子组件的时候,
:子组件属性名 = "父组件声明的变量" - 子组件在
<script>的props中接收父组件的数据,也就是v-bind中指定的子组件属性名 - 子组件使用

子组件 -> 父组件传递数据 ->
v-on
- 子组件通过
$emit(事件名称, 更改的值)方法修改父组件的属性数据 - 父组件在调用子组件的时候声明事件
@事件名称 = "事件方法",这样拿到子组件修改的新值就能通过方法改掉父组件对应的数据了

prop进阶
prop是自定义的属性,用于父->子,可以传递任意数量,任意类型。

prop支持校验
ts
props: {
type: Number, // 类型校验
required: true, // 是否是必须传递的
default: 0, // 默认值
// 自定义校验,参数是属性的值,然后如果返回true,说明校验成功,否则校验失败
validator (value) {
if (value >= 0 && value <= 100) {
return true;
} else {
return false;
}
}
}
prop是单项数据流(父级prop更新,会向下流动,影响子组件,单向性)
也就是说子组件不能直接改prop中的变量
3.2:非父子组件的通信
基本废弃,非父子组件通信vue2使用vuex,vue3使用pinal
很少再使用eventbus / provide / inject
3.3:表单类组件的封装
我们经常要封装一些表单组件供其他组件使用,这时候就涉及到表达类组件的封装

父组件中可以使用v-model简化上面的操作


3.4:ref和this.$ref
想象一下你去图书馆借书:
- 每本书就是网页上的一个元素(输入框、按钮等)
ref就像是给这本书贴了个便利贴标签this.$refs就像是你的标签管理器,保存所有贴了标签的书的位置
html
<template>
<div>
<!-- 给这个输入框贴个标签叫 "myInput" -->
<input ref="myInput" type="text">
<button @click="focusInput">点我让输入框获取焦点</button>
</div>
</template>
<script>
export default {
methods: {
focusInput() {
// 通过标签找到这个输入框,然后让它获取焦点
this.$refs.myInput.focus();
}
},
mounted() {
// 页面加载完自动让输入框获取焦点
this.$refs.myInput.focus();
}
}
</script>
html
<template>
<input ref="username"> <!-- 贴标签:username -->
<button ref="submitBtn">提交</button>
<my-child ref="childComp"></my-child> <!-- 贴标签:childComp -->
</template>
<script>
// 那么 this.$refs 里面就有:
this.$refs = {
username: <input 元素>, // 真实的输入框
submitBtn: <button 元素>, // 真实的按钮
childComp: <子组件实例> // 子组件的全部内容
}
</script>
4:插槽
想象一下你要买一个手机壳:
- 普通手机壳:图案、颜色固定,不能改
- DIY手机壳:有个"插槽"可以放自己的照片
插槽就是:父组件可以在子组件中插入自定义内容的地方

4.1:默认插槽
默认插槽就是留一个坑,等着其他的父组件去填充,这在当代页面开发非常常用
例如一个页面,顶部和底部一直都是不变的,只有中间的内容会变,此时就可以使用插槽。

4.2:具名插槽
vue2.6+
子组件的插槽有名称,通过<slot name="插槽名称">,这样一个子组件中可以定义多个插槽,挖好几个坑,分别插旗命名
父组件中使用<template v-slot:插槽名称>来填充对应的插槽
html
<!-- 子组件 BlogPost.vue -->
<template>
<div class="blog-post">
<!-- 有名字的插槽 -->
<slot name="header"></slot>
<div class="date">{{ date }}</div>
<!-- 另一个有名字的插槽 -->
<slot name="content"></slot>
<!-- 没名字的默认插槽 -->
<slot></slot>
<!-- 第三个有名字的插槽 -->
<slot name="footer"></slot>
</div>
</template>
html
<!-- 父组件使用(Vue 2.6+ 新语法 v-slot) -->
<template>
<BlogPost date="2024-01-15">
<!-- 方式1:v-slot:名字 -->
<template v-slot:header>
<h1>文章标题</h1>
</template>
<!-- 方式2:#名字 是简写 -->
<template #content>
<p>这里是文章正文内容...</p>
</template>
<!-- 这个会放到默认插槽(没名字的那个) -->
<p>默认插槽的内容</p>
<template #footer>
<div>作者:张三</div>
<div>阅读量:1000</div>
</template>
</BlogPost>
</template>
4.3:作用域插槽
这是最神奇的插槽!子组件可以把数据传给父组件。
html
<!-- 子组件 UserList.vue -->
<template>
<div class="user-list">
<!-- 把 user 数据通过 slot 传给父组件 -->
<div v-for="user in users" :key="user.id">
<slot :user-data="user"></slot>
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: '小明', age: 20 },
{ id: 2, name: '小红', age: 22 },
{ id: 3, name: '小刚', age: 25 }
]
}
}
}
</script>
html
<!-- 父组件使用 -->
<template>
<UserList>
<!-- 接收子组件传来的数据 -->
<!-- slotProps 可以随便取名,这里收到的是 { user-data: user } -->
<template v-slot="slotProps">
<div class="user-item">
姓名:{{ slotProps['user-data'].name }}
年龄:{{ slotProps['user-data'].age }}
</div>
</template>
</UserList>
</template>
作用域插槽可以简写
html
<!-- 直接解构 -->
<template v-slot="{ userData }">
<div>姓名:{{ userData.name }}</div>
</template>
<!-- 或者 -->
<template #default="{ userData }">
<div>姓名:{{ userData.name }}</div>
</template>
5:路由和VueRouter
5.1:使用说明
基础步骤,操作一次即可,配置vueRouter
1️⃣ 下载 - 下载VueRouter模块到当前工程,版本3.6.5
- vue2 + vueRouter3 + vueX3;
- vue3 + vueRouter4 + vueX4
bash
# 打开工程终端,然后输入(只在当前工程安装vue-router,如果是要再全局安装,后面加上 -g)
npm install vue-router@3.6.5
# 如果使用的yarn
yarn add vue-router@3.6.5
2️⃣ 再main.js中
引入VueRouter -> 安装注册VueRouter -> 创建路由对象 -> 注入
js
// 核心作用就是导入App.vue, 根据App.vue创建结构渲染index.html
// 1:导入vue核心包
import Vue from 'vue'
// 2:导入App.vue
import App from './App.vue'
import VueRouter from "vue-router"; // 步骤1:引入VueRouter
// 阻止启动生产消息
Vue.config.productionTip = false
Vue.use(VueRouter); // 步骤2:安装注册VueRouter
const router = new VueRouter(); // 步骤3:创建路由对象
// 创建vue实例
new Vue({
// 提供render方法 -> 基于App.vue创建结构渲染index.html
render: h => h(App),
router, // 步骤4:将路由对象注入new Vue()实例中,建立关联
}).$mount('#app') // $mount指定挂载点为index.html中id=app的容器
核心步骤 - 使用vue-router
1️⃣ 创建所需要的组件(和视图切换相关的组件,最好放置在views文件夹下),配置路由的规则
假设现在已经有三个组件,分别是Find.vue, My.vue, Friend.vue
在main.js中(这里先放在这里,正常不会放在main.js)
js
import Find from './views/Find.vue'
import My from './views/My.vue'
import Firend from './views/Friend.vue'
const router = new VueRouter({
routes: [
{path: '/find', component: Find},
{path: '/my', component: My},
{path: '/friend', component: Friend}
]
})
2️⃣ 配置导航,配置路由出口(路径匹配的组件显示的位置)
vue-router提供了一个全局组件router-link来取代a标签,有下面两个作用
- 能够跳转 - 配置to属性指定路径,to属性无需#,其实本质还是a标签
- 能够高亮 - 默认就会提供高亮类名(自带激活时的类名-
router-link-active),可以直接设置高亮样式
html
<div class="my_music">
<router-link to='/find'>发现音乐</router-link>
<router-link to='/my'>我的音乐</router-link>
<router-link to='/friend'>朋友</router-link>
</div>
<div class="top">
<!--
导航出口配置router-view, router-view是控制组件所展示的位置
如果在导航html部分的下面写,说明导航在上,切换的内容在下
如果在导航html部分的上面写,说明导航在下,切换的内容在上
-->
<router-view></router-view>
</div>
5.2:router抽离
显然所有的路由配置都堆在main.js中不合适,所以我们应该将路由模块抽离出来
新建src/router文件夹,创建index.js
js
// 1:引入视图相关的组件,准备映射
// 小技巧:因为vue的位置层级可能比较深,为了不来回改动,不推荐使用相对路径
// 这里使用绝对路径 - @ - @表示的就是当前工程的src目录
import MyHeader from "@/views/MyHeader.vue";
import MyBody from "@/views/MyBody.vue";
import MyFooter from "@/views/MyFooter.vue";
// 2:引入vue & vue-router
import Vue from 'vue'
import VueRouter from "vue-router";
// 3: 注册vue-router
Vue.use(VueRouter)
// 创建路由实例,并声明映射
const router = new VueRouter({
routes: [
{path: '/header', component: MyHeader},
{path: '/body', component: MyBody},
{path: '/footer', component: MyFooter}
]
})
// 暴露出去,为了外面的main.js能够使用
export default router
然后main.js中引入这个就可以了
js
import Vue from 'vue'
import App from './App.vue' // 声明App.vue为App
import router from "@/router/index.js"; // 导入index.js
Vue.config.productionTip = false // 阻止启动生产消息
new Vue({
render: h => h(App), // 创建一个Vue实例, 将App组件渲染到id为app的div上
router, // 将路由信息放入vue实例中
}).$mount('#app')
在根组件或者指定组件中的<template>中使用
html
<div class="my_music">
<router-link to='/header'>发现音乐</router-link>
<router-link to='/body'>我的音乐</router-link>
<router-link to='/footer'>朋友</router-link>
</div>
<div class="top">
<!--
导航出口配置router-view
router-view是控制组件所展示的位置
如果在导航html部分的下面写,说明导航在上,切换的内容在下
如果在导航html部分的上面写,说明导航在下,切换的内容在上
-->
<router-view></router-view>
</div>
5.3:路由模式
hash路由:默认的路由模式,例如http://localhost:8080/#/home
history路由:常用,例如http://localhost:8080/home, 没有了/#这层
js
const router = new VueRouter({
routes,
mode: "history"
})
5.4:重定向和404
网页打开的时候,url打开的默认是/根路径,此时如果根路径没有配置和任何组件的映射,就会产生一进入导航栏下面空白的情况。而重定向的作用就是,匹配到指定的path之后,强制跳转path的路径
对于404,就是所有的路由都没有配置成功,这个时候就要404页面组件了,404路由一定要配置在所有的路由的最后面,同时指定path:'*'
js
// 创建路由实例,并声明映射
const router = new VueRouter({
routes: [
// 注意这一行.... 匹配到根路径之后,也就是一进入系统页面,就自动跳转到/header路径,然后触发/header路径对应的组件
{path: '/', redirect: '/header'},
{path: '/header', component: MyHeader},
{path: '/body', component: MyBody},
{path: '/footer', component: MyFooter},
// 404,一定要放在最后,否则会匹配到其他路径,path : '*'
{path: '*', component: NoFoundError}
]
})
5.5:编程式导航和路由传参
基本跳转
可以使用path方式进行跳转
js
// 等效于 router-link 的点击
this.$router.push('/user/123')
// 替换当前路由(不会添加历史记录)
this.$router.push({ path: '/login', replace: true })
// 或使用 replace 方法
this.$router.replace('/login')
还可以根据name命名路由跳转(适合path路径长的场景)
js
this.$router.push({
name: '路由名称'
})
// 同时在路径组件映射文件中指定name - router/index.js
{name: "路由名称", path: '路径', component: 组件},
可以在历史记录中前进或者后退 - router.go()
js
// 前进 1 步,相当于 history.forward()
this.$router.go(1)
// 后退 1 步,相当于 history.back()
this.$router.go(-1)
// 后退 3 步
this.$router.go(-3)
导航传参
如果是静态路由传参,可以使用下面的方式
js
this.$router.push("/路径?参数名1=参数值1&参数名2=参数值2")
// 或者
this.$router.push({
path: "/路径",
query: {
参数名1: '参数值1',
参数名2: '参数值2'
}
})
// 或者
this.$router.push({
name: "路由名字",
query: {
参数名1: '参数值1',
参数名2: '参数值2'
}
})
此时,router/index.js应该是类似于这样的
js
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 定义路由
const routes = [
{
path: '/', // 首页
name: 'Home', // 名称,根据name命名路由跳转的时候用的就是这个
component: () => import('../views/Home.vue') // 声明对应的组件是那个
},
{
path: '/search', // 搜索页
name: 'Search',
component: () => import('../views/Search.vue')
},
{
path: '/user', // 用户详情页
name: 'User',
component: () => import('../views/User.vue')
}
]
// 创建路由实例
const router = new VueRouter({
mode: 'hash', // 使用 hash 模式,最简单,不用配服务器
routes // 简写,相当于 routes: routes
})
export default router
下游组件通过$router.query.参数名就能拿到对应的值
html
<!-- 方式1:在组件中直接使用 $route.query -->
<template>
<div>
<!-- 模板中使用 -->
<p>关键词: {{ $route.query.keyword }}</p>
<p>页码: {{ $route.query.page }}</p>
</div>
</template>
<script>
export default {
created() {
// JS中获取查询参数
const keyword = this.$route.query.keyword
const page = this.$route.query.page
console.log('搜索参数:', keyword, page)
// 获取路径参数(如果有的话)
const userId = this.$route.params.id
console.log('用户ID:', userId)
}
}
</script>
html
<!-- 方式2:使用计算属性 -->
<template>
<div>
<p>关键词: {{ keyword }}</p>
<p>页码: {{ page }}</p>
</div>
</template>
<script>
export default {
computed: {
// 将查询参数转为计算属性
keyword() {return this.$route.query.keyword || ''},
page() {return this.$route.query.page || 1},
// 路径参数
userId() {return this.$route.params.id}
},
created() {
// 直接使用计算属性
console.log('搜索:', this.keyword, '页码:', this.page)
}
}
</script>
如果是动态路由传参,可以在上游组件这么写
js
this.$router.push("/路径/参数值")
this.$router.push({
path: "/路径/参数值"
})
this.$router.push({
name: "路由名称",
params: {
参数名: "参数值"
}
})
此时,router/index.js应该是类似于这样的
js
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/', // 首页
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/user/:id', // 动态路由,:id 是参数占位符
name: 'User',
component: () => import('../views/User.vue')
},
{
path: '/article/:category/:id', // 多个参数
name: 'Article',
component: () => import('../views/Article.vue')
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
下游组件使用同静态路径传参
html
<template>
<div>
<h2>用户详情</h2>
<p>用户ID: {{ userId }}</p>
<!-- 显示参数 -->
<p>动态参数: {{ $route.params.id }}</p>
<!-- 多个参数 -->
<div v-if="$route.params.category">
<p>分类: {{ $route.params.category }}</p>
<p>文章ID: {{ $route.params.id }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'UserDetail',
computed: {
// 推荐:使用计算属性
userId() {
return this.$route.params.id
},
category() {
return this.$route.params.category
},
articleId() {
return this.$route.params.id // 注意:这是文章ID,不是用户ID
}
},
created() {
// 方法1:直接通过 $route.params 获取
console.log('用户ID:', this.$route.params.id)
// 方法2:使用计算属性
console.log('用户ID:', this.userId)
// 多个参数
console.log('参数对象:', this.$route.params)
},
watch: {
// 监听参数变化
'$route'(to, from) {
if (to.params.id !== from.params.id) {
this.loadUserData(to.params.id)
}
}
},
methods: {
loadUserData(userId) {
console.log('加载用户数据:', userId)
// 调用 API 获取用户信息
}
}
}
</script>
5.6:路由配置思路
🎉 二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边

js
...
import Article from '@/views/Article.vue'
import Collect from '@/views/Collect.vue'
import Like from '@/views/Like.vue'
import User from '@/views/User.vue'
...
const router = new VueRouter({
routes: [
{path: '/', redirect: '/home/article'}, // 首页的重定向
// 二级路由属于谁,就是谁的children
{path: '/home', component: Layout, children: [
{path: 'article', component: Article},
{path:'collect', component:Collect},
{path:'like', component:Like},
{path:'user', component:User}
]},
{path: '/detail', component:ArticleDetail},
{path: "*", component:PageError} // 404
]
})
5.7:路由守卫和配置
路由守卫是在路由跳转前、跳转后或跳转过程中执行的钩子函数,用于控制路由的访问权限、参数验证等
to-> 要跳转到的路径from-> 跳转前的路径next-> 是否放行或者跳转到指定的path
全局前置守卫 -
router.beforeEach
js
const router = new VueRouter({ ... })
// 全局前置守卫
router.beforeEach((to, from, next) => {
console.log('全局前置守卫')
console.log('从', from.path, '到', to.path)
// 1. 验证登录状态
const isAuthenticated = localStorage.getItem('token')
if (to.meta.requiresAuth && !isAuthenticated) {
// 需要登录但未登录,跳转到登录页
next({
path: '/login', // 要跳转到的页面
query: { redirect: to.fullPath } // 保存目标路径,登录后跳转回来
})
} else if (to.path === '/login' && isAuthenticated) {
// 已登录但访问登录页,跳转到首页
next('/')
} else {
// 放行
next()
}
})
全局解析守卫 -
router.beforeResolve
js
// 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用
router.beforeResolve((to, from, next) => {
// 可以在这里进行一些全局的数据预取或权限验证
if (to.meta.requiresData) {
// 预取数据
fetchInitialData().then(() => {
next()
}).catch(() => {
next(false) // 取消导航
})
} else {
next()
}
})
全局后置钩子 -
router.afterEach
js
// 导航完成后调用,没有 next 函数
router.afterEach((to, from) => {
// 修改页面标题
document.title = to.meta.title || '默认标题'
// 页面访问统计
console.log(`用户从 ${from.path} 访问到 ${to.path}`)
// 滚动到顶部
window.scrollTo(0, 0)
})
路由独享守卫
js
const routes = [
{
path: '/user/:id',
name: 'UserProfile',
component: UserProfile,
// 路由独享守卫
beforeEnter: (to, from, next) => {
// 验证参数格式
if (!/^\d+$/.test(to.params.id)) {
next({ name: 'NotFound' }) // 参数格式错误,跳转到404
return
}
// 检查权限
const userRole = localStorage.getItem('role')
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
next({ path: '/403' }) // 无权限页面
return
}
// 验证用户是否存在
checkUserExists(to.params.id).then(exists => {
if (exists) {
next()
} else {
next({ name: 'UserNotFound' })
}
})
},
meta: {
title: '用户详情',
roles: ['admin', 'user'] // 允许访问的角色
}
}
]
自定义 query 序列化/反序列化
js
const router = new VueRouter({
mode: 'history',
routes,
/**
* stringifyQuery - 将查询对象转换为 URL 查询字符串
* @param {Object} query - 查询参数对象
* @returns {string} - 查询字符串
*/
stringifyQuery(query) {
// 默认实现(Vue Router 内部实现)
// 你可以自定义序列化逻辑
const parts = []
for (const key in query) {
const value = query[key]
if (value === null || value === undefined) {
continue
}
// 处理数组
if (Array.isArray(value)) {
value.forEach(item => {
parts.push(
`${encodeURIComponent(key)}[]=${encodeURIComponent(item)}`
)
})
}
// 处理对象
else if (typeof value === 'object') {
parts.push(
`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`
)
}
// 处理基本类型
else {
parts.push(
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
)
}
}
return parts.length ? `?${parts.join('&')}` : ''
},
/**
* parseQuery - 将 URL 查询字符串解析为对象
* @param {string} queryString - 查询字符串(包含 ?)
* @returns {Object} - 查询参数对象
*/
parseQuery(queryString) {
const query = {}
if (!queryString) return query
// 移除开头的 ? 或 #
const search = queryString.replace(/^[?#]/, '')
if (!search) return query
// 分割参数
const pairs = search.split('&')
for (const pair of pairs) {
if (!pair) continue
let [key, value = ''] = pair.split('=')
// URL 解码
key = decodeURIComponent(key)
value = decodeURIComponent(value)
// 处理数组参数:key[]
if (key.endsWith('[]')) {
const realKey = key.slice(0, -2)
if (!query[realKey]) {
query[realKey] = []
}
query[realKey].push(value)
}
// 处理 JSON 字符串
else if (value.startsWith('{') || value.startsWith('[')) {
try {
query[key] = JSON.parse(value)
} catch {
query[key] = value
}
}
// 普通参数
else {
query[key] = value
}
}
return query
}
})
滚动行为配置
js
const router = new VueRouter({
mode: 'history',
routes,
/**
* scrollBehavior - 控制路由跳转时的滚动位置
* @param {Route} to - 目标路由
* @param {Route} from - 来源路由
* @param {Object|null} savedPosition - 浏览器前进/后退时保存的位置
*/
scrollBehavior(to, from, savedPosition) {
// 1. 浏览器前进/后退时,恢复到之前的位置
if (savedPosition) {
return savedPosition
}
// 2. 如果有 hash,滚动到对应元素
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth', // 平滑滚动
offset: { x: 0, y: 100 } // 偏移量
}
}
// 3. 如果路由有 meta 属性,根据 meta 处理
if (to.meta.scrollToTop === false) {
// 某些页面不需要滚动到顶部
return false
}
// 4. 默认滚动到顶部
return { x: 0, y: 0 }
// 5. 异步滚动(返回 Promise)
// return new Promise(resolve => {
// setTimeout(() => {
// resolve({ x: 0, y: 0 })
// }, 500)
// })
}
})
5.8:基础案例关键点
整合下的上面的知识点,整个面试经验页面跳转的设计关键点

路由配置思路:二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边

js
...
import Article from '@/views/Article.vue'
import Collect from '@/views/Collect.vue'
import Like from '@/views/Like.vue'
import User from '@/views/User.vue'
...
const router = new VueRouter({
routes: [
{ path: '/', redirect: '/home/article' }, // 首页的重定向
// 二级路由属于谁,就是谁的children
{ path: '/home', component: Layout, children: [
{ path:'article', component:Article },
{ path:'collect', component:Collect },
{ path:'like', component:Like },
{ path:'user', component:User }
]},
{ path: '/detail', component: ArticleDetail },
{ path: "*", component: PageError }
]
})
html
<!-- 导航栏 -->
<nav class="tabbar">
<router-link to="/home/article">面经</router-link>
<router-link to="/home/collect">收藏</router-link>
<router-link to="/home/like">喜欢</router-link>
<router-link to="/home/user">我的</router-link>
</nav>
<!-- 高亮设置 -->
<style>
a.router-link-active {
color: orange;
}
</style>
文章页面的请求和渲染
js
data() {
return {
articleList: [], // 初始化文章列表为空,从后端获取
}
},
async created() {
// 调用后端,拿到其中的rows,然后赋值给articleList
const rows = await axios.get(
'https://mock.boxuegu.com/mock/3083/articles'
)
this.articleList = rows
},
html
<template>
<div class="article-page">
<!-- 使用v-for渲染 -->
<div class="article-item" v-for="item in articelList" :key="item.id">
<div class="head">
<img :src="item.creatorAvatar" alt="" />
<div class="con">
<p class="title">{{ item.stem }}</p>
<p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p>
</div>
</div>
<div class="body">
{{item.content}}
</div>
<div class="foot">点赞 {{item.likeCount}} | 浏览 {{item.views}}</div>
</div>
</div>
</template>
详情页跳转思路,跳转详情页需要把当前点击的文章id传给详情页,获取数据
- 查询参数传参 -
this.$router.push('/detail?参数1=参数值&参数2=参数值') - 动态路由传参 - 先改造路由在传参
this.$router.push('/detail/参数值')
html
<template>
<div class="article-page">
<div class="article-item"
v-for="item in articelList"
:key="item.id"
@click="$router.push(`/detail?id=${item.id}`)">
</div>
</div>
</template>
详情页通过this.$route.query.id使用
动态路由传参 - 需要该index.js路由映射为动态路由
json
{
path: '/detail/:id',
component: ArticleDetail
}
html
<div class="article-item"
v-for="item in articelList" :key="item.id"
@click="$router.push(`/detail/${item.id}`)">
....
</div>
详情页通过this.$route.params.id使用
点击回退跳转到上一页。在详情页中,可以设置点击事件$router.back()回到上一步
html
<template>
<div class="article-detail-page">
<nav class="nav"><span class="back" @click="$router.back()"><</span> 面经详情</nav>
....
</div>
</template>
详情页渲染,先通过axios携带id发送请求获得文章详情,然后传递给当前组件的属性
js
data() {
return {
articleDetail:{}
}
},
async created() {
const id = this.$route.params.id
const {data:{result}} = await axios.get(
`https://mock.boxuegu.com/mock/3083/articles/${id}`
)
this.articleDetail = result
},
拿到数据之后,进行页面渲染就可以了
html
<template>
<div class="article-detail-page">
<!-- 回到上一页面 -->
<nav class="nav">
<span class="back" @click="$router.back()"><</span> 面经详情
</nav>
<!-- 头部渲染 -->
<header class="header">
<h1>{{articleDetail.stem}}</h1>
<p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p>
<p>
<img
:src="articleDetail.creatorAvatar"
alt=""
/>
<span>{{articleDetail.creatorName}}</span>
</p>
</header>
<!-- 文章主体渲染 -->
<main class="body">
{{articleDetail.content}}
</main>
</div>
</template>
缓存组件和keep-alive
从面经列表 点到 详情页,又点返回,数据重新加载了 → 希望回到原来的位置

当路由被跳转后,原来所看到的组件就被销毁了,重新返回后组件又被重新创建了,所以数据被加载了,所以可以利用keep-alive把原来的组件给缓存下来
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件中。
html
<template>
<div class="h5-wrapper">
<!-- 将文章列表的数据进行keep-alive,防止被销毁 -->
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
优点
在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM,
减少加载时间及性能消耗,提高用户体验性。
缺点
缓存了所有被切换的组件
keep-alive的三个属性
- include : 组件名数组,只有匹配的组件会被缓存
- exclude : 组件名数组,任何匹配的组件都不会被缓存
- max : 最多可以缓存多少组件实例
html
<template>
<div class="h5-wrapper">
<!-- 只有LayoutPage这个组件会被缓存 -->
<keep-alive :include="['LayoutPage']">
<router-view></router-view>
</keep-alive>
</div>
</template>
keep-alive的使用会触发两个生命周期函数
- activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发
- deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发
组件缓存后就不会执行组件的created, mounted, destroyed 等钩子了
所以其提供了actived 和deactived钩子,帮我们实现业务需求。
6:数据共享vuex
Vuex 是一个 Vue 的 状态管理工具,状态就是数据。可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如:购物车数据个人信息数据
⚠️ 不是所有的场景都适用于vuex,只有在必要的时候才使用vuex
⚠️ 使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度
⚠️ vue3中已被pinal取代
6.1:使用说明
bash
npm i vuex@3
为了维护项目目录的整洁,在src目录下新建一个store目录其下放置一个index.js文件
js
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
// 创建vuex实例,并导出
export default new Vuex.Store({
// =============== 这五部分也会是vuex的五大核心概念 ==================
state: {
// ~ data
// state => State提供唯一的公共数据源
// 所有共享的数据都要统一放到Store中的State中存储。
},
getters: {
// ~ computed
// getters => Getters是Store的计算属性,可以理解为Store的计算属性,
// 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
// 这些数据是依赖state的,此时会用到getters
},
mutations: {
// mutations => Mutations是Store的变更处理器,
// 所有对state的修改,都要通过mutations来完成
},
actions: {
// actions => Actions是Store的异步变更处理器,
},
modules: {
// modules => 模块化
}
})
main.js中引入并挂载vuex到vue实例上
js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
store
}).$mount('#app')
🎉 如果在自定义工程的时候勾选了vuex,上面的都不用做,已经创建好对应的模板了
6.2:共享数据-state
提供数据
State提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。
打开项目中的store/index.js文件,在state对象中可以添加我们要共享的数据。
js
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
// 创建vuex实例,并导出
export default new Vuex.Store({
state: {
// state => State提供唯一的公共数据源
// 所有共享的数据都要统一放到Store中的State中存储。
count: 100
}
})
访问数据
s t o r e − > 指向了 s t o r e 仓库的位置,可以帮助我们获取数据,所以我们可以通过 store -> 指向了store仓库的位置,可以帮助我们获取数据,所以我们可以通过 store−>指向了store仓库的位置,可以帮助我们获取数据,所以我们可以通过store访问到vuex的数据
- 在
<template>中,也就是html使用的话 ->{``{$store.state.变量名}} - 在组件的
<script>中,this.$store.state.变量名,如果是单个的JS中,使用store.state.变量名
还可以通过辅助函数mapState映射计算属性,帮助我们使用共享数据
1️⃣ 在当前要使用共享数据的组件下导入mapState
2️⃣ 通过es6的展开运算符(...)声明当前组件要使用的共享数据,放到计算属性中
js
computed: {
// 2: 通过es6中的导出运算符号映射state中的count属性
// 然后就可以直接通过共享数据的名称在当前组件中进行操作了
...mapState(['变量名'])
},
⚠️ vuex中的state在组件中直接修改是不生效的,因为state遵循单项数据流
6.3:同步修改-mutations
声明同步方法
Vuex中mutations中要求不能写异步代码,如果有异步的ajax请求,应该放置在actions中
mutations是一个对象,对象中存放修改state的方法
js
mutations: {
// mutations => Mutations是Store的变更处理器,
// 所有对state的修改,都要通过mutations来完成
addCount(state) {
state.count++
},
minusCount(state) {
state.count--
}
},
使用同步方法
在组件中可以使用this.$store.commit()提交
js
handle () {
this.$store.commit('addCount', 10) // 如果是一个要提交的参数
}
// 如果是多个
this.$store.commit('addCount', {
count: 10,
title: '小标题'
})
还可以使用辅助函数mapMutations把位于mutations中的方法提取了出来,我们可以将它导入
js
import { mapMutations } from 'vuex'
methods: {
// 使用es6中的...
...mapMutations(['addCount'])
}
6.4:异步变更-actions
定义异步方法
actions则负责进行异步操作
js
mutations: {
changeCount (state, newCount) {
state.count = newCount
}
},
actions: {
setAsyncCount (context, num) {
// 一秒后, 给一个数, 去修改 num
setTimeout(() => {
context.commit('changeCount', num)
}, 1000)
}
},
使用异步方法
组件中通过this.$store.dispatch进行调用
js
setAsyncCount () {
this.$store.dispatch('setAsyncCount', 666)
}
当然也提供了辅助函数辅助mapActions, 同mapMutations一样,放在下游组件的methods中
js
import { mapActions } from 'vuex'
methods: {
...mapActions(['changeCountAction'])
}
//mapActions映射的代码 本质上是以下代码的写法
//methods: {
// changeCountAction (n) {
// this.$store.dispatch('changeCountAction', n)
// },
//}
6.5:数据筛选-getter
除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
这些数据是依赖state的,此时会用到getters
定义getter
js
getters: {
// getters函数的第一个参数是 state
// 必须要有返回值
filterList: state => state.list.filter(item => item > 5)
}
使用getter
可以使用辅助函数mapGetters,将其放在下游组件的computed中
html
computed: {
...mapGetters(['filterList'])
}
<div>{{ filterList }}</div>
6.6:统一例子实例
js
// store/index.js
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
// 创建vuex实例,并导出
export default new Vuex.Store({
// 严格模式,严格模式下,不允许在mutations之外进行state的修改
strict: true,
state: {
// state => State提供唯一的公共数据源
// 所有共享的数据都要统一放到Store中的State中存储。
count: 100,
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
getters: {
// getters => Getters是Store的计算属性,可以理解为Store的计算属性,
// 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
// 这些数据是依赖state的,此时会用到getters
filterList: state => state.list.filter(item => item > 5)
},
mutations: {
// mutations => Mutations是Store的变更处理器,
// 所有对state的修改,都要通过mutations来完成
addCount(state) {
state.count++
},
minusCount(state) {
state.count--
}
},
actions: {
// actions => Actions是Store的异步变更处理器
setAsyncCount(context, num) {
// 一秒后, 给一个数, 去修改 num
setTimeout(() => {
// 提交一个mutations
context.commit('addCount', num)
}, 1000)
}
},
modules: {
// modules => 模块化
}
})
html
<template>
<div class="box">
<!-- 3: 模板中的使用 -->
<h2>{{count}}</h2>
<button @click="addCount">点我 + 1</button>
<button @click="setAsyncCount">异步 + 1</button>
<!-- 遍历filterList -->
<div v-for="item in filterList" :key="item">
{{item}}
</div>
</div>
</template>
<script>
// 引入mapState & mapMutations & mapActions & mapGetters
import { mapState } from "vuex";
import { mapMutations } from "vuex";
import { mapActions } from "vuex";
import { mapGetters } from "vuex";
export default {
name: 'Son1Com',
computed: {
...mapState(['count']),
...mapGetters(['filterList'])
},
methods: {
...mapMutations(['addCount']),
...mapActions(['setAsyncCount'])
}
}
</script>
<style lang="css" scoped>
.box {
border: 3px solid #ccc;
width: 400px;
padding: 10px;
margin: 20px;
}
h2 {
margin-top: 10px;
}
</style>
6.7:模块-module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。也就是说,如果把所有的状态都放在state中,当项目变得越来越大的时候,Vuex会变得越来越难以维护
此时就可以写好几个store的文件,然后暴露出去,最后导入到index.js中由index.js统一暴露

/store/modules/user.js
js
const state = {
userInfo: {
name: 'zs',
age: 18
}
}
const mutations = {}
const actions = {}
const getters = {}
// 将自己的属性都暴露出去,为了后面在store/index.js中集成
export default {
state,
mutations,
actions,
getters
}
/store/modules/setting.js
js
const state = {
theme: 'dark',
desc: '描述真呀真不错'
}
const mutations = {}
const actions = {}
const getters = {}
export default {
state,
mutations,
actions,
getters
}
/store/index.js
js
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
import user from '@/modules/user'
import setting from '@/modules/setting'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
// 创建vuex实例,并导出
export default new Vuex.Store({
// 严格模式,严格模式下,不允许在mutations之外进行state的修改
strict: true,
state: {
// state => State提供唯一的公共数据源
// 所有共享的数据都要统一放到Store中的State中存储。
count: 100,
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
getters: {
// getters => Getters是Store的计算属性,可以理解为Store的计算属性,
// 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
// 这些数据是依赖state的,此时会用到getters
filterList: state => state.list.filter(item => item > 5)
},
mutations: {
// mutations => Mutations是Store的变更处理器,
// 所有对state的修改,都要通过mutations来完成
addCount(state) {
state.count++
},
minusCount(state) {
state.count--
}
},
actions: {
// actions => Actions是Store的异步变更处理器
setAsyncCount(context, num) {
// 一秒后, 给一个数, 去修改 num
setTimeout(() => {
// 提交一个mutations
context.commit('addCount', num)
}, 1000)
}
},
modules: {
// modules => 模块化
user,
setting
}
})
在下游组件,就可以通过辅助函数轻松获取到每一个模块的指定内容:
- state -> 辅助函数
mapState('模块名', ['xxx']) - geeter -> 辅助函数
mapGetters('模块名', ['xxx']) - mutations -> 辅助函数
mapMutations('模块名', ['xxx']) - actions -> 辅助函数
mapActions('模块名', ['xxx'])
或者直接使用:
- state ->
$store.state.模块名.数据项名 - getters ->
$store.getters['模块名/属性名'] - mutations ->
$store.commit('模块名/方法名', 其他参数) - actions ->
$store.dispatch('模块名/方法名', 其他参数)
7:element组件库的使用
所谓组件库就是封装好了很多很多的组件,整合到一起就是一个组件库
bash
npm i element-ui -S
在main.js中全局导入
js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'; // 引入elementUI
import 'element-ui/lib/theme-chalk/index.css'; // element ui的css
Vue.config.productionTip = false // 生产提示关闭
Vue.use(ElementUI, { size: "small" }); // 初始化ElementUI
new Vue({
router, // vue路由
render: h => h(App) // 渲染到模板
}).$mount('#app') // 渲染位置 id = app
剩下看文档就行https://element.eleme.cn/#/zh-CN。其实就是提供了布局方案和一些设计好的标签