Vue

Vue快速上手

Vue概念

Vue是一个用于构建用户界面的渐进式框架

Vue两种使用方式

  1. Vue核心包开发

    场景:局部模块改造

  2. Vue核心包 & Vue插件 工程化

    场景:整站开发

image-20240218092624263

框架:

优点:大大提升开发效率

缺点:需要记忆规则 -> 官网

创建实例

创建Vue实例初始化渲染

步骤:

  1. 准备HTML容器
  2. 引包-开发版本/生产版本
  3. 创建Vue实例 new Vue()
  4. 指定配置项 ->渲染数据
    • el指定挂载点
    • data提供数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
{{ msg }}
</div>

<script src="../libs/vue.js"></script>
<script>
// 通过引入的Vue包,创建Vue实例对象
const app = new Vue({
// 通过el选择器,指定Vue管理的盒子
el:"#app",
// 通过data提供数据
data:{
msg:"Hello Vue"
}
})
</script>

插值表达式

插值表达式是一种vue的模板语法

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
{{ msg }}
</div>
<script>
const app = new Vue({
el:"#app",
data:{
msg:"Hello Vue"
}
})
</script>
  1. 作用:利用表达式进行插值,渲染到页面中

    表达式:是可以被求值的代码,JS会算出一个结果

  2. 语法:

    1
    2
    <h3>{{ age > 18 ? '成年' : '未成年' }}</h3>
    <p>{{ friend.name }}</p>

注意:

  1. 使用的数据必须存在于data

  2. 支持表达式,并非语句,例如 if for

    1
    <p>{{ if }}</p>
  3. 不能再标签属性中使用插值

    1
    <p title="{{ title }}">p标签</p>

响应式特性

响应式:数据变化,视图自动更新

修改数据 实例.属性名 = 新值

访问数据 实例.属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
{{ msg }}
</div>

<script src="../libs/vue.js"></script>
<script>
// 通过引入的Vue包,创建Vue实例对象
const app = new Vue({
// 通过el选择器,指定Vue管理的盒子
el:"#app",
// 通过data提供数据
data:{
msg:"Hello Vue"
}
})

document.querySelector("#app").addEventListener("click", ()=>{
app.msg = "你点我干嘛"
})
</script>

开发者工具

装插件调试Vue应用

Vue指令

Vue会根据不同的指令,对标签实现不同的功能

指令:带有v-前缀的特殊标签属性

v-show 和 v-if

  1. v-show

    作用:控制元素的显示隐藏

    语法:v-show=“表达式”, 表达式值true显示,false隐藏

    原理:切换display:none控制显示隐藏

    场景:频繁切换显示隐藏的场景

  2. v-if

    作用:控制元素的显示隐藏(条件渲染

    语法:v-if = “表达式”,表达式值true显示,false隐藏

    原理:基于条件判断,是否创建或移除元素节点

    场景:要么显示,要么隐藏,不频繁切换的场景

  3. v-show 和v-if 的区别

    v-show:通过控制css属性display:none来控制标签的显隐

    v-if:通过条件控制元素的创建和移除

v-else和v-else-if

作用:辅助v-if进行渲染判断

语法:v-else v-else-if=“表达式”

注意:需要紧挨着v-if一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div id="app">
<p v-if="gender === 1">性别:男</p>
<p v-else>性别:女</p>

<hr>

<p v-if="score >= 90">成绩A</p>
<p v-else-if="score >= 80">成绩B</p>
<p v-else-if="score >= 70">成绩C</p>
<p v-else-if="score >= 60">成绩D</p>
</div>

<script src="../libs/vue.js"></script>
<script>
// 通过引入的Vue包,创建Vue实例对象
const app = new Vue({
// 通过el选择器,指定Vue管理的盒子
el:"#app",
// 通过data提供数据
data:{
gender:1,
score:80
}
})
</script>

v-on

作用:注册事件 = 添加监听 + 提供处理逻辑

语法:

  1. v-on: 事件名 = “内联语句”

    1
    <button v-on:click="count++">按钮</button>

    可以使用 @ 来替换 v-on:

    1
    <button @click="count++">按钮</button>
  2. v-on: 事件名 = “methods的函数名”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <div id="app">
    <button @click="add">-</button>
    <button>{{ count }}</button>
    <button @click="sub">+</button>
    </div>

    <script src="../libs/vue.js"></script>
    <script>
    const app = new Vue({
    el:"#app",
    data:{
    count:100
    },
    methods:{
    add(){
    app.count++
    },
    sub(){
    app.count--
    }
    }
    })
    </script>
  3. 简写:@事件名=

Vue指令:v-on调用传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<button @click="fn(参数一,参数二)" >按钮</button>
<script>
const app = new Vue({
el:"#app",
data:{
count:100
},
methods:{
fn(a, b){
console.log(a, b)
}
}
})
</script>

v-bind

作用:动态设置html标签属性 -> src url title

语法:v-bind:属性名 = “表达式”

通过表达式的结果动态的给属性赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<img v-bind:src="imgUrl">
</div>

<script src="../libs/vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data:{
imgUrl:"./img/1.png"
}
})
</script>

**简写:**v-bind:属性名 可以简写为 :属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<img :src="imgUrl">
</div>

<script src="../libs/vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data:{
imgUrl:"./img/1.png"
}
})
</script>

点击切换图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div id="app">
<button v-show="index > 0" @click="index--">
上一页
</button>
<div>
<img :src="list[index]">
</div>
<button v-show="index < list.length-1" @click=index++>
下一页
</button>
</div>

<script src="../libs/vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data:{
index:0,
list:[
"./01.png",
"./02.png",
"./03.png",
"./04.png",
]
}
})
</script>

v-bind对样式控制的增强

操作class

语法::class=“对象/数组”

  1. 对象 -> 键就是类名,值就是布尔值,如果为true,有这个类,否则就没有这个类

    1
    <div class="box"  :class="{类名:布尔值, 类名:布尔值}"></div>
  2. 数组 -> 数组中所有的类,都会添加到盒子上,本质上就是添加一个class列表

    1
    <div class="box"  :class="[类名1, 类名2, 类名3]"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<ul>
<li v-for="(item, index) in list" :key="item.id" >
<a href="#" @click="activeIndex=index" :class="{active:activeIndex===index}">{{ item.name }}</a>
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
activeIndex:0,
list: [
{ id: 1, name: '京东秒杀' },
{ id: 2, name: '每日特价' },
{ id: 3, name: '品类秒杀' }
]

}
})
</script>

操作style

语法: :style=“样式对象”

1
<div class="box"  :style="{Css属性名:Css属性值}" ></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<div class="progress">
<div class="inner" :style="{width: persent + '%'}">
<span>{{ persent }}%</span>
</div>
</div>
<button @click="persent=25">设置25%</button>
<button @click="persent=50">设置50%</button>
<button @click="persent=75">设置75%</button>
<button @click="persent=100">设置100%</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
persent: 0
}
})
</script>

v-for

作用:基于数据循环,多次渲染整个元素 —> 数组,对象,数字

遍历数组的方式:

v-for = “(item, index) in 数组”

item表示数组的中的每一项,index表示数组下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<ul>
<li v-for="(itme, index) in list">{{ item }} {{ index }}</li>
</ul>
</div>

<script src="../libs/vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data:{
index:0,
list:[
"./01.png",
"./02.png",
"./03.png",
"./04.png",
]
}
})
</script>

v-for 中的 key

作用:给列表添加唯一标识,便于Vue进行列表项的正确排序复用

v-for的默认行为会尝试 原地修改元素(就地复用),如果我们删除的第一个元素,则Vue会默认删除最后一个,然后用剩下三个覆盖掉前面三个

1
2
3
4
5
<ul>
<li v-for="(item, index) in bookList" :key="item.id">
<span>{{ itme.name }}</span>
</li>
</ul>

注意:

  1. key的值只能是字符串或数字类型

  2. 可以的值必须具有唯一性

  3. 推荐使用id作为key(唯一), 不推荐使用index作为key(会变化,不对应)

    1
    <li v-for="(item, index) in xxx"  :key="唯一值"></li>

v-model

作用:给表单元素使用双向数据绑定 -> 可以快速获取或设置表单元素内容

  • 数据变化 -> 视图自动更新
  • 试图变化 -> 数据自动更新

语法:v-model = “变量”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div id="app">
账户:<input type="text" v-model="username"> <br><br>
密码:<input type="password" v-model="password"><br><br>
<button @click="login">登录</button>
<button @click="reset">重置</button>
</div>

<script src="../libs/vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data:{
username:"",
password:""
},
methods:{
login(){
console.log(this.username, this.password)
},
reset(){
this.username = ""
this.password = ""

}
}
})
</script>

应用于表单元素

所有表单元素都可以使用v-model来进行绑定

常见的表单元素都可以使用 v-model绑定关联 -> 快速获取或设置表单元素的值

它会根据控件类型自动选取正确的方法来更新元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div id="app">
<from>
姓名:<input type="text" v-model="username"><br>
是否单身:<input type="checkbox" v-model="isSingle"><br>
性别:<input type="radio" name="gender" value = 0 v-model="gender">
<input type="radio" name="gender" value = 1 v-model="gender">
所在城市:
<select v-model="city">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">成都</option>
</select>

自我描述
<textarea v-model="desc"></textarea>
</from>
</div>
<script>
const app = new Vue({
el:"#app",
data:{
username:"", // text
isSingle:"", // true, false
gender:"", // 0, 1
city:"", // 101, 102, 103
desc:"", // text
}
})
</script>

v-model原理

作用:提供数据双向绑定

原理:v-model本质上就是一种简写,例如在输入框上就是value属性和input事件的合写

注意:$event用于模板上获取事件的形参

1
2
3
4
5
6
<template>
<div id="app">
<input v-model="msg" type="text">
<input :value="msg" @input="msg=$event.target.value" type="text">
</div>
</template>

表单类组件

  1. 父传子: 数据 应该是组件 props 传递 过来的,v-model折解绑定数据,子元素不能使用v-model来进行数据绑定,因为数据时父元素的,子元素不能直接修改,而v-model是双向绑定
  2. 子传父:监听输入,子传父传值给父组件修改

父文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<!-- 主体区域 -->
<section id="app">
<!-- 通过$event来获取子元素传来的数据 -->
<BaseSelect :cityId="cityId" @changeId="cityId=$event"></BaseSelect>
</section>
</template>

<script>
import BaseSelect from "./components/BaseSelect.vue"
export default {
components:{
BaseSelect
},
data(){
return {
cityId:"102"
}
}
}
</script>

<style>
</style>

子文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<select :value="cityId" @change="changeId">
<option value="101">上海</option>
<option value="102">北京</option>
<option value="103">邯郸</option>
</select>
</template>

<script>
export default {
props:{
cityId:String
},
methods:{
changeId(e){
this.$emit("changeId", e.target.value)
}
}
}
</script>
<style>
</style>

父元素中的内容也可简化,通过v-model来对父元素中的内容进行双向绑定

v-model = :value="", @input=""

所以

父元素:

1
2
3
4
// 老
<BaseSelect :cityId="cityId" @changeId="cityId=$event"></BaseSelect>
// 新
<BaseSelect v-model="cityId"></BaseSelect>

子元素,接收的时候必须使用value接收,返回的时候触发事件也必须是input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 老
props:{
cityId:String
},
methods:{
changeId(e){
this.$emit("changeId", e.target.value)
}
}
// 新
props:{
value:String
},
methods:{
changeId(e){
this.$emit("input", e.target.value)
}
}

小黑记事本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./css/index.css" />
<title>记事本</title>
</head>
<body>

<!-- 主体区域 -->
<section id="app">
<!-- 输入框 -->
<header class="header">
<h1>小黑记事本</h1>
<input placeholder="请输入任务" class="new-todo" v-model="todoName" />
<button class="add" @click="add(todoName)">添加任务</button>
</header>
<!-- 列表区域 -->
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index">{{ index +1 }}</span> <label>{{ item.todoName }}</label>
<button class="destroy" @click="del(item.id)"></button>
</div>
</li>
</ul>
</section>
<!-- 统计和清空 -->
<footer class="footer" v-show="list.length != 0">
<!-- 统计 -->
<span class="todo-count">合 计:<strong> {{ list.length }} </strong></span>
<!-- 清空 -->
<button class="clear-completed" @click="clear">
清空任务
</button>
</footer>
</section>

<!-- 底部 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

const app = new Vue({
el: '#app',
data: {
todoName:"",
list:[

]
},
methods:{
clear(){
this.list = []
},
add(todoName) {
if(todoName.trim() === ""){
alert("任务名称不许为空!")
return;
}
this.list.unshift({
id:Date.now(),
todoName:todoName
})
this.todoName = ""
},
del(id){
this.list = this.list.filter(item=> item.id != id)
}
}
})

</script>
</body>
</html>

指令修饰符

通过 "."指明一些指令后缀,不同后缀封装了不同的操作处理 -> 简化代码

  1. 按键修饰符

    @keyup.enter -> 键盘回车监听

    例如小黑记事本中使用回车添加记录

    1
    <input @keyup.enter="add" placeholder="请输入任务" class="new-todo" v-model="todoName" />
  2. v-model修饰符

    v-model.trim -> 去除首位空格

    v-model.number -> 转数字

  3. 事件修饰符

    @事件名.stop -> 阻止冒泡

    1
    2
    3
    <button @click.stop = "fn">

    </button>

    @事件名.prevent -> 阻止默认型为

    1
    <a @click.privent>阻止默认行为</a>

计算属性

概念:基于现有的数据,计算出来的新属性依赖的数据变化,自动重新计算。

语法:

  1. 声明在computed配置项中,一个计算属性对应一个函数
  2. 使用起来和普通属性一样使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const app = new Vue(){
el:"#app",
data:{
list:[
{id:1, name:"篮球", num:1},
{id:1, name:"足球", num:6},
{id:1, name:"排球", num:4},
]
},
computed: {
totalCount(){
return this.list.reduce((sum, item)=>sum+item.nun, 0)
}
}
}

计算属性VS方法

computed计算属性:

作用:封装了一段对于数据的处理,求得一个结果

语法:

  1. 写在computed配置项中
  2. 作为属性,直接使用 → this.计算属性

methods方法:

作用:给实例提供了一个方法,调用业务逻辑

语法:

  1. 写在computed配置项中
  2. 作为方法,需要调用 → this.方法名 {{ 方法名() }} @事件名=“方法名”

computed:具有缓存特性(提升性能), 计算属性会对计算出来的结果缓存,再次使用直接读取缓存,依赖项变了,会自动重新计算,并再次缓存

计算属性的完整写法

计算属性默认的简写,只能读取访问,不能修改

如果要修改 → 需要写计算属性的完整写法

1
2
3
4
5
6
7
8
9
10
11
computed:{
计算属性名:{
get(){
计算属性
return 结果
},
set(value){
修改属性
}
}
}

watch侦听器

作用:监视数据变化,执行一些业务逻辑或异步操作

语法:

  1. 简单写法 → 简单类型数据,直接监视

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    data:{
    words: '苹果',
    obj: {
    words:'apple'
    }
    },
    watch:{
    属性名(newValue, oldValue) {
    一些业务逻辑 或 异步操作。
    },
    '对象.属性名' (newValue, oldValue) {
    一些业务逻辑或异步操作
    }
    }

    翻译案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const app = new Vue({
    el: '#app',
    data: {
    words: '',
    result:""
    },
    watch:{
    // 监听words的改变
    words(newValue) {
    // 设置防抖
    // 先清除计时器
    clearTimeout(this.timer)
    // 再从新设置计时器
    this.timer = setTimeout(async ()=>{
    // 进行异步请求
    const res = await axios({
    url:"https://applet-base-api-t.itheima.net/api/translate",
    parmers:{
    words:this.words
    }
    })
    this.result = res.data.data
    }, 300)
    }
    }
    })
  2. 完整写法 → 添加格外配置项

    1. deep:true 对复杂类型深度监视,只要对象中有一个元素修改了就会触发
    2. immediate:true 初始化立刻执行一次handler方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    data:{
    obj:{
    words:'apple',
    lang:"italy"
    }
    },
    watch:{
    数据属性名:{
    deep:true,
    immediate:true,
    handler (newValue){
    console.log(newValue)
    }
    }
    }

    翻译案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const app = new Vue({
    el: '#app',
    data: {
    obj:{
    words: '',
    lang:''
    },

    result:""
    },
    watch:{
    // 监听words的改变
    obj:{
    deep:true,
    immdiate:true,
    handler(newValue){
    // 设置防抖
    // 先清除计时器
    clearTimeout(this.timer)
    // 再从新设置计时器
    this.timer = setTimeout(async ()=>{
    // 进行异步请求
    const res = await axios({
    url:"https://applet-base-api-t.itheima.net/api/translate",
    parmers:newValue
    })
    this.result = res.data.data
    }, 300)
    }
    }
    }
    })

水果案例

  1. 渲染功能: v-if/v-else V-for :class
  2. 删除功能:点击传参 filter过滤覆盖原数组
  3. 修改个数:点击传参 find找对象
  4. 全选反选:计算属性computed 完整写法 get/set
  5. 统计选中的总价和总数量:计算属性computed reduce条件求和
  6. 持久化到本地: watch监视,localStorage,JSON.stringify,JSoN.parse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'http://autumnfish.cn/static/火龙果.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'http://autumnfish.cn/static/荔枝.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'http://autumnfish.cn/static/榴莲.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'http://autumnfish.cn/static/鸭梨.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'http://autumnfish.cn/static/樱桃.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
methods:{
sub(id){
const fruit = this.fruitList.find(item=>item.id === id)
if(fruit.num > 1 ){
fruit.num--
}else{
this.del(id)
}
},
add(id){
const fruit = this.fruitList.find(item=>item.id === id)
fruit.num++
},
del(id){
this.fruitList = this.fruitList.filter(item => item.id !== id)
},
},
computed:{
isAll:{
get(){
return this.fruitList.every(item=>item.isChecked === true)
},
set(value){
this.fruitList.forEach(item=>item.isChecked=value)
}
},
totalCount(){
return this.fruitList.reduce((sum, item)=>{
if(item.isChecked){
return sum+item.num
}else{
return sum
}
}, 0)
},
totalPrice(){
return this.fruitList.reduce((sum, item)=>{
if(item.isChecked){
return sum+item.num*item.price
}else{
return sum
}
}, 0)
}
},
watch:{
fruitList:{
deep:true,
immdiate:true,
handler(newValue){
localStorage.setItem(fruitList, fruitList)
}
}
}
})

自定义指令

自己定义的指令,可以封装一些dom操作,扩展额外的功能

全局注册,在

1
2
3
4
5
6
Vue.directive("指令名", {
// inserted,在元素插入到页面的时候生效
"inserted" (el) {
el.focus()
}
})

局部注册,在自己的组件中注册

1
2
3
4
5
6
7
8
directive{
"指令名", {
// inserted,在元素插入到页面的时候生效,相当于mounted(){}
"inserted" (el) {
el.focus()
}
}
}

使用:在input插入到页面的时候获取焦点

1
<input v-指令名 type="text">

指令的值

语法:

  1. 在绑定指令时,可以通过等号的形式为指令绑定具体参数值

    1
    2
    3
    <div v-color="color">

    </div>
  2. 通过binding.value可以拿到指令值,指令值修改会触发update函数

    通过update钩子,可以监听指令值的变化,进行dom操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    directives: {
    color: {
    inserted(el, binding){
    el.style.color = binding.value
    },
    // 指令的值修改完之后触发
    update(el, binding){
    el.style.color = binding.value
    }
    }
    }

v-loading指令封装

加载动画的使用

在实际开发场景的时候,发送请求需要时间,在请求数据未回来的时候,页面会处于空白状态=》用户体验不好

需求:封装一个v-loading指令实现加载中的效果

分析

  1. 本质loading效果就是一个蒙层,改在了盒子上面
  2. 数据请求过程中,开启loading蒙层
  3. 数据请求完毕之后,关闭loading蒙层

实现

  1. 准备一个loading类,通过伪元素定位,设置宽高,实现蒙层

  2. 准备开启loading状态,本质只是需要添加移除类即可

  3. 结合自定义的语法进行封装复用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    <template>
    <div class="main" v-loading="isLoading">

    </div>
    </template>

    <script>
    export default {
    data(){
    return{
    // 页面初始化的时候默认是加载
    isLoading:true
    }
    },
    async created(){
    const res = await axios.get("")
    // 请求成功的时候,将加载图标去掉
    this.isLoading=false
    },
    directives:{
    loading:{
    // 页面元素添加时候自动触发
    inserted(el, binding){
    binding.value ? el.classList.add("loading") : el.classList.remove("loading")
    },
    // 当isLoading修改的时候触发
    update(el, binding){
    binding.value ? el.classList.add("loading") : el.classList.remove("loading")
    }
    }
    }


    }
    </script>

    <style>
    .mian{
    width: 400px;
    height: 400px;
    }
    .loading::before{
    width: 100%;
    height: 100%;
    position: absolute;
    content:"";
    left:0;
    right:0;
    background: #fff url("./loading.gif") no-repeat center;
    }
    </style>

Vue特性

生命周期

Vue声明周期:一个Vue实例从创建到销毁的整个过程

分为四个阶段:创建,挂载,更新,销毁

image-20240223094624077

初始化渲染请求,需要Vue准备好数据之后在进行,所以最早可以在创建完之后,进行初始化渲染

操作dom,需要Vue将数据挂载到html中之后再进行操作,所以最早可以在挂载完毕之后在进行dom操作

钩子函数

在Vue声明周期中,会自动运行一些函数,被称为生命周期钩子,让开发者可以在特定的阶段运行自己的代码

四个阶段,八个钩子 -> 三个常用 created, mounted, beforDestory(释放资源:定时器,全局监听事件)

image-20240223095117693

image-20240223095147660

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const app = new Vue({
el:"#app",
data:{
apple:"苹果"
},
// 以下函数会自动执行
// 创建阶段
beforCreate () {
// 创建数据之前
},
created () {
// 创建数据之后
},

// 挂载阶段
beforMount () {
// 挂载Dom,模板渲染之前
},
mounted () {
// 挂载Dom,模板渲染之后
},

// 更新阶段
beforUpdate () {

},
updated () {

},

// 销毁阶段
beforDestroy () {

},
destroyed () {

}
})

案例

一进页面就立刻发送请求,获取数据并在页面上渲染

1
2
3
4
5
6
7
8
9
10
11
12
const app = new Vue({
el:"#app",
data:{
// 此时list中并没有数据
list:[]
},
async created () {
const res = await axios.get("网址")
// 通过自己获取的数据自动给list赋值
this.list = res.data.data
}
})

小黑商品列表

image-20240226093258014

基本渲染

在数据准备完成之后进行渲染

可以再created中使用

注意:渲染列表的方式可以提取出来,这样在修改数据的时候可以直接调用即可,注意:methods中的方法在别处调用的时候,一定要加this.方法名获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
created() {
// 方法也要通过this访问
this.getList()
}
methods: {
async getList() {
const res = await axios({
url: "https://applet-base-api-t.itheima.net/bill",
method: "GET",
params: {
creator: "张三"
}
})
// 一定要加this 否则赋值不上
this.priceList = res.data.data
}
},

添加功能

添加完毕之后需要使用 上面封装的获取列表函数来进行从新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
await axios({
url: "https://applet-base-api-t.itheima.net/bill",
method: "POST",
data: {
creator: "张三",
name: this.goodsName,
price: this.goodsPrice,
}
})
// 从新渲染获取的数据
this.getList()
this.goodsName = ""
this.goodsPrice = ""
}

删除功能

同样,删除完毕之后需要重新渲染

1
2
3
4
5
6
7
async del(id) {
await axios({
url: `https://applet-base-api-t.itheima.net/bill/${id}`,
method: "delete"
})
this.getList()
},

饼图渲染

初始化饼图

因为饼图需要绑定到页面上的dom对象中,所以最早可以再mounted钩子中使用

具体细节可以阅读官方文档

myChart可以定义到this中,这样方便更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mounted() {
this.myChart = echarts.init(document.getElementById('main'));
this.myChart.setOption({
title: {
text: '商品消费图',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '商品价格',
type: 'pie',
radius: '50%',
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}

更新饼图

每次在获取完数据之后可以通过覆盖的方式来更新饼图

通过this获取到mounted中定义的myCharts来进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async getList() {
const res = await axios({
url: "https://applet-base-api-t.itheima.net/bill",
method: "GET",
params: {
creator: "张三"
}
})
// 一定要加this 否则赋值不上
this.priceList = res.data.data
this.myChart.setOption({
series: [
{
data: this.priceList.map(item => ({value:item.price, name:item.name}))
}
]
})
},

工程化开发VUE CLI

开发Vue的两种方式:

  1. 核心包传统开发模式:基于html/css /js 文件,直接引入核心包,开发 Vue。
  2. 工程化开发模式:基于构建工具 (例如: webpack)的环境中开发 Vue

Vue CLI 是Vue 官方提供的一个全局命令工具,可以帮助我们快速创建一个开发Vue 项目的标准化基础架子。[集成了 webpack 配置]

好处:

  1. 开箱即用,零配置
  2. 内置babel等工具
  3. 标准化

使用步骤:

  1. 全局安装(一次): yarn global add @vue/cli 或 npmi @vue/cli -g
  2. 查看 Vue 版本: vue --version
  3. 创建项目架子:vue create project-name(项目名-不能用中文)
  4. 启动项目: yarn serve 或 npm run serve (找packagejson)

image-20240227091446788

组件开发

  1. 组件化:一个页面可以拆分成一个个组件,每个组件有着自己独立的结构、样式、行为

    好处:便于维护,利于复用 提升开发效率

    组件分类:普通组件、根组件

  2. 根组件:整个应用最上层的组件,包裹所有小组件

根组件:App.vue的三部分

  1. template:结构
  2. script:js逻辑
  3. style:样式

普通组件注册和使用

  1. 局部注册:只能在注册的组件内使用

    在components文件夹下创建 .vue文件(有三个组成部分)

    image-20240227103431739

    在使用的组件内导入并注册

    app.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <template>
    <div class="app">
    <!-- 头部 -->
    <HguHeader></HguHeader>
    <!-- 身体 -->
    <HguMain></HguMain>
    <!-- 尾部 -->
    <HguFooter></HguFooter>
    </div>
    </template>

    <script>
    import HguHeader from "./components/HguHeader.vue"
    import HguMain from "./components/HguMain.vue"
    import HguFooter from "./components/HguFooter.vue"
    export default {
    components:{
    HguHeader:HguHeader,
    HguMain:HguMain,
    HguFooter:HguFooter,
    }
    }
    </script>

    <style>
    .app {
    width: 600px;
    height: 600px;
    background-color: aqua;
    margin: 0 auto;
    padding: 20px;
    }
    </style>
  2. 全局注册:所有组件都能使用

    在components文件夹下创建 .vue文件(有三个组成部分)

    vue.component(组件名, 组件对象)

    在main.js组件内导入并注册

  3. 使用:

    当成html标签使用即可 <组件名></组件名>

    命名规范-大驼峰命名法

页面开发思路

页面开发思路

  1. 分析页面,按模块拆分组件,搭架子(局部或全局注册)
  2. 根据设计图,编写组件html结构 css样式(已准备好)
  3. 拆分封装通用小组件 (局部或全局注册

将来通过js动态渲染,实现功能

组件的三大部分组成

组件有三部分组成:结构,样式,逻辑

样式

默认情况:写在组件中的样式会全局生效 -> 因此容易造成多个组件之间样式冲突问题

全局样式(默认) :影响所有组件(即使写到单个组件中)

局部样式:scoped下的样式,只作用于当前组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>

</template>

<script>
export default {

}
</script>
// 加上 scoped可以只作用于当前文件中的内容
<style scoped>

</style>>

scoped原理

  1. 当前组件内标签都被添加 data-v-hash值 的属性
  2. css选择器都被添加[data-v-hash值] 的属性选择器

最终效果: 必须是当前组件的素,才会有这个自定义属性,才会被这个样式作用到

逻辑

  1. el根实例独有

  2. data是一个函数:一个组件的 data 选项必须是一个函数。> 保证每个组件实例,维护独立的一份数据对象每次创建新的组件实例,都会新执行一次 data 函数,得到一个新对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <template>
    <div>
    <button @click="count++">+</button>
    <span>{{ count }}</span>
    <button @click="count--">-</button>
    </div>
    </template>

    <script>
    export default {
    data(){
    return {
    count:999
    }
    }
    }
    </script>
    //
    <style scoped>

    </style>>
  3. 其他配置项一致

组件通信

组件与组件之间的数据传递

组件数据是独立的,无法直接访问其他组件的数据,想用其他组件的数据需要使用组件通信

不同的组件关系,组件通信解决方案不同

  1. 父子关系

    props和$emit

    父组件通过 props 将数据传递给子组件

    父组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <!-- 通过绑定属性来给子元素传递数据 -->
    <son :title="myTitle"></son>
    </div>
    </template>

    <script>
    import son from "./components/son.vue"
    export default {
    data(){
    return {
    myTitle:"你好"
    }
    },
    components:{
    son
    }
    }
    </script>
    //
    <style scoped>

    </style>>

    子组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <template>
    <div>
    {{ title }}
    </div>
    </template>

    <script>
    export default {
    props:["title"]
    }
    </script>
    //
    <style scoped>

    </style>>

    子组件利用 $emit 通知父组件修改更新

    子组件

    $emit(事件名,数据)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    {{ title }}
    <button @click="handleClick">
    修改title
    </button>
    </div>
    </template>

    <script>
    export default {
    props:["title"],
    methods:{
    handleClick(){
    // 子元素通过$emit来绑定触发事件,并传递参数
    this.$emit("changeTitle", "修改后的名字")
    }
    }
    }
    </script>
    //
    <style scoped>

    </style>>

    父组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <template>
    <div>
    <!-- 通过绑定属性来给子元素传递数据 -->
    <!-- 通过监听son的事件来进行修改 -->
    <son :title="myTitle" @changeTitle="changeFn"></son>
    </div>
    </template>

    <script>
    import son from "./components/son.vue"
    export default {
    data(){
    return {
    myTitle:"你好"
    }
    },
    components:{
    son
    },
    methods:{
    changeFn(newTitle){
    this.myTitle=newTitle
    }
    }
    }
    </script>
    //
    <style scoped>

    </style>>

  1. 非父子关系

    provide & inject:作用:跨层级共享数据

    简单数据类型:不是响应式的

    复杂数据类型:是响应式的

    1. 父组件:provide提供数据

      1
      2
      3
      4
      5
      6
      7
      8
      export default {
      provide() {
      return {
      color:this.color,
      userInfo:this.userInfor
      }
      }
      }
    2. 子孙组件inject取值使用

      1
      2
      3
      4
      5
      6
      export default {
      inject:["color", "userInfo"],
      created () {
      console.log(this.color, this.userInfo)
      }
      }

    eventbus:作用:非父子组件之间,进行简易的消息传递

    1. 创建一个都能访问到的事件总线 JS文件
    1
    2
    3
    import Vue from "vue"
    const Bus = new Vue()
    export default Bus
    1. 接收方,监听Bus实例的对象

      1
      2
      3
      4
      5
      created(){
      Bus.$on("sendMsg", (msg)=>{
      this.msg=msg
      })
      }
    2. 发送方,触发Bus实例事件

      1
      Bus.$emit("sendMsg", msg)

prop

prop定义:组件上注册的一些自定义属性

prop作用:向子组件传递数据

特点:

可以传递任意数量的prop

可以传递任意类型的prop

image-20240228155302543

prop校验

通过改变prop的方式来传递

  1. 类型校验

    prop:{校验的属性名:类型}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    {{ title }}
    <button @click="handleClick">
    修改title
    </button>
    </div>
    </template>

    <script>
    export default {
    props:{
    title:String
    },
    methods:{
    handleClick(){
    // 子元素通过$emit来绑定触发事件,并传递参数
    this.$emit("changeTitle", "修改后的名字")
    }
    }
    }
    </script>
    <style scoped>
    </style>
  2. 完整写法

    props:{ w :{约束条件}}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <template>
    <div>
    {{ title }}
    <button @click="handleClick">
    修改title
    </button>
    </div>
    </template>

    <script>
    export default {
    props:{
    title:{
    type:String, // 类型
    required:true,// 非空
    default:0, // 默认值
    validator (value) { // 自定义属性 ,返回true和false
    // 根据value判断
    return true
    }
    }
    },
    methods:{
    handleClick(){
    // 子元素通过$emit来绑定触发事件,并传递参数
    this.$emit("changeTitle", "修改后的名字")
    }
    }
    }
    </script>
    <style scoped>
    </style>

prop和data

共同点:都可以给组件提供数据

区别:

data的数据时自己的:可以随便改

prop的数据是外部的:不可以随便改

.sycn修饰符

作用:可以实现子组件和父组件的双向绑定,简化代码

特点:prop属性名可以自定义,非固定的value

本质:就是**:属性名@update:属性名**合写

父元素

1
2
3
4
5
6
<BaseDialog :visible.sync="isShow" />

<BaseDialog
:visible="isShow"
@update:visible="isShow=$event"
/>

子元素

1
2
3
4
props:{
visible:Boolean
},
this.$emit("update:visible", false)

ref和$refs

作用:可以获取dom元素,或组件实例

特点:查找范围更加精确,documen.queryselector() 查找全局的

获取dom

  1. 目标标签 添加ref属性

    1
    <div ref="chartRef">图片渲染容器</div>
  2. 恰当时机,通过this.$refs.xxx获取目标标签

    1
    2
    3
    mounted(){
    console.log(this.$refs.chartRef)
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <template>
    <div class="base-chart-box" ref="baseBox" ></div>
    </template>

    <script>
    import * as echarts from "echarts";
    export default {
    mounted() {
    // 通过refs精确获取指定盒子
    const myChart = echarts.init(this.$refs.baseBox);
    myChart.setOption({
    xAxis: {
    type: "category",
    data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    },
    yAxis: {
    type: "value",
    },
    series: [
    {
    data: [120, 200, 150, 80, 70, 110, 130],
    type: "bar",
    },
    ],
    });
    },
    };
    </script>

    <style scoped>
    .base-chart-box{
    width: 400px;
    height: 400px;
    }
    </style>

获取组件实例

  1. 目标组件添加ref属性

    1
    <BaseFrom ref="BaseFrom"></BaseFrom>
  2. 恰当时机,可通过this.$refs.xxx获取目标组件

    1
    this.$refs.BaseFrom.组件方法()

例如:

子表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<form>
账号:<input type="text" v-model="account">
密码:<input type="password" v-model="password">
</form>
</template>

<script>
export default {
data(){
return{
account:"",
password:"",
}
},
methods:{
getValues(){
return{
account:this.account,
password:this.password
}
},
reset(){
this.account="",
this.password=""
}
}
}
</script>

<style>

</style>

父表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div class="app">
<BaseFrom ref="basefrom"></BaseFrom>
<button @click="getValue">获取数据</button>
<button @click="reset">重置</button>
</div>
</template>

<script>
import BaseFrom from "./components/BaseFrom.vue"
export default {
components:{
BaseFrom
},
methods:{
getValue(){
console.log(this.$refs.basefrom.getValues())
},
reset(){
this.$refs.basefrom.reset()
}
}
}
</script>
<style>
</style>

Vue异步更新,$nextTick

需求:当点击一个按钮的时候,输入框立刻出现,并自动获取焦点

  1. 点击编辑,显示编辑框

  2. 让编辑框,立刻获取焦点

    1
    2
    this.isShowEdit = true // 显示输入框
    this.$refs.inp.focus() // 获取焦点

    Vue是异步更新的,所以在执行show操作的时候,Dom元素不会立刻渲染出来

    可使$nextTick来等待Dom元素加载完毕之后在获取焦点

    1
    2
    3
    4
    this.isShowEdit = true // 显示输入框
    this.$nextTick(()=>{
    this.$refs.inp.focus() // 获取焦点
    })

插槽

作用:让组件内部的一些结构支持自定义

需求:在页面中显示对话框,封装成一个组件

问题:组件内容部分,不希望写死,希望使用的时候自定义

语法

  1. 组件内需要定制的结构部分,改用< slot >< /slot >占位

  2. 使用组件的时候,< MyDialog ></ MyDialog >标签内容,传入结构替换slot

    组件Vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <div class="header">

    </div>
    <div class="main">
    <slot></slot>
    </div>
    <div class="footer">

    </div>
    </div>
    </template>

    <script>
    export default {

    }
    </script>

    <style>

    </style>

    使用Vue

    1
    2
    3
    4
    5
    6
    7
    8
     <MyDialog>
    <!-- 标签内部的文本会传入到改标签组件中slot的位置 -->
    你确定要删除吗
    </MyDialog>
    <MyDialog>
    <!-- 标签内部的文本会传入到改标签组件中slot的位置 -->
    你确定要修改吗
    </MyDialog>

默认内容

为slot设置默认内容,如果不设置默认内容,而且也没有传入相应的参数的话,则会显示空白

直接向slot中写入默认内容即可,如果外面有值传递进来,则会替换掉默认内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<div class="header">

</div>
<div class="main">
<slot>我是默认内容 </slot>
</div>
<div class="footer">

</div>
</div>
</template>

<script>
export default {

}
</script>

<style>

</style>

具名插槽

上述的默认插槽,只能替换一个位置,我们可以给插槽设置name来确认我们要填入的那个位置

语法:

  1. 通过多个slot使用name属性区分名字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div class="dialog-header">
    <slot name="head"></slot>
    </div>
    <div class="dialog-content">
    <slot name="content"></slot>
    </div>
    <div class="dialog-footer">
    <slot name="footer"></slot>
    </div>
  2. template配合v-slot:名字来分发对应的标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <MyDialog>
    <template v-slot:head>
    大标题
    </template>
    <template v-slot:content>
    内容
    </template>
    <template v-slot:footer>
    <button>
    按钮
    </button>
    </template>
    </MyDialog>

作用域插槽

是插槽的一个传参语法

在定义slot插槽的同时,是可以传值的,给插槽上可以绑定数据,将来使用组件的时候可以使用

场景:封装表格组件,样式相同,但是内容不同

  1. 父传子,动态渲染表格内容
  2. 利用默认插槽,定制操作列
  3. 删除或查看需要使用的当前项的id,属于组件内部数据,通过作用域插槽传值绑定,进而使用
  4. 子元素通过slot给父元素传递元素

使用步骤:

  1. 给slot标签以添加属性的方式传值

    1
    <slot :id="item.id"msg="测试文本"></slot>
  2. 所有添加的属性都会被收集到一个对象中

    1
    { id: 3,msg:"测试文本"}
  3. 在template中,通过#插槽名="obj”接收,默认插槽名为 default

    1
    2
    3
    4
    5
    <MyTable :list="list">
    <template #defaulte"obj">
    <button @click="del(obi.id)">删除</button>
    </template>
    </MyTable>

父元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div>
<MyTable :data="list">
<template #default="obj">
<button @click="del(obj.item.id)">
删除
</button>
</template>
</MyTable>
<MyTable :data="list2">
// 通过下述方法来获取子元素传递的参数
<template #default="obj">
<button @click="search(obj.item.id)">
查看
</button>
</template>
</MyTable>
</div>
</template>

<script>
export default {
data(){
return{
list:[
{name:"张三" , id:1},
{name:"张三1" , id:2},
{name:"张三2" , id:3}
],
list2:[
{name:"李四" , id:1},
{name:"李四1" , id:2},
{name:"李四2" , id:3}
]
}
}
}
</script>

<style>

</style>

子元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<thead>
<th>id</th>
<th>name</th>
<th>操作</th>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id">
<td>{{ item.id}}</td>
<td>{{ item.name}}</td>
<td>
// 通过下述方法来进行给父元素传值,传递的是一个对象 {row:item, msg:"你好"}
<slot :row="item" msg="你好"></slot>
</td>
</tr>
</tbody>
</template>

<script>
export default {
data(){
return{
list:[
{name:"张三" , id:1},
{name:"张三1" , id:2},
{name:"张三2" , id:3}
],
list2:[
{name:"李四" , id:1},
{name:"李四1" , id:2},
{name:"李四2" , id:3}
]
}
}
}
</script>

<style>

</style>

案例-商品列表

  1. my-tag 标签组件封装

    双击显示输入框,输入框获取焦点

    失去焦点,隐藏输入框

    回显标签信息

    内容修改,回车修改标签信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    <template>
    <div class="my-tag">
    <input
    class="input"
    v-if="isShow"
    ref="inp"
    v-focus
    type="text"
    placeholder="输入标签"
    :value="value"
    @keyup.enter="handleEnter"
    @blur="isShow=false"
    />
    <div class="text" v-else @dblclick="handleClick">{{ value }}</div>
    </div>
    </template>

    <script>
    export default {
    props:{
    value:String
    },
    data() {
    return {
    isShow: false,
    };
    },
    methods: {
    handleClick() {
    // 显示输入框
    this.isShow = true;

    },
    handleEnter(e) {
    this.$emit("input", e.target.value)
    }
    },
    };
    </script>

    <style lang="less" scoped>
    .my-tag {
    cursor: pointer;
    .input {
    appearance: none;
    outline: none;
    border: 1px solid #ccc;
    width: 100px;
    height: 40px;
    box-sizing: border-box;
    padding: 10px;
    color: #666;
    &::placeholder {
    color: #666;
    }
    }
    }
    </style>
  2. my-table 表格组件封装

    动态传递表格数据渲染

    表头支持用户自定义

    主体支持用户自定义

    数据不能写死,动态传递表格渲染的数据 props

    结构不能写死 - 多处结构自定义 [具名插槽]

    • 表头支持自定义,使用slot占位
    • 主体支持自定义,使用slot占位

    table组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    <template>
    <table class="my-table">
    <thead>
    <tr>
    <!--使用slot占位,支持自定义头部-->
    <slot name="head"></slot>
    </tr>
    </thead>
    <tbody>
    <tr v-for="item in list" :key="item.id">
    <!--使用slot占位,支持自定义主体-->
    <slot name="body" :item="item"></slot>
    </tr>
    </tbody>
    </table>
    </template>

    <script>
    export default {
    props:{
    list:Array
    },

    };
    </script>

    <style lang="less" scoped>
    .table-case {
    width: 1000px;
    margin: 50px auto;
    img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
    }
    }

    .my-table {
    width: 100%;
    border-spacing: 0;
    img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
    }
    th {
    background: #f5f5f5;
    border-bottom: 2px solid #069;
    }
    td {
    border-bottom: 1px dashed #ccc;
    }
    td,
    th {
    text-align: center;
    padding: 10px;
    transition: all .5s;
    &.red {
    color: red;
    }
    }
    .none {
    height: 100px;
    line-height: 100px;
    color: #999;
    }
    }
    </style>

    主组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    <template>
    <div class="table-case">
    <MyTable :list="goods">
    <template #head >
    <th>编号</th>
    <th>图片</th>
    <th>名称</th>
    <th width="100px">标签</th>
    </template>

    <template #body="{ item }">
    <td>{{ item.id }}</td>
    <td>
    <img
    :src="item.picture"
    />
    </td>
    <td>{{ item.name }}</td>
    <td>
    <!-- 标签组件 -->
    <MyTag v-model="item.tag"></MyTag>
    </td>
    </template>
    </MyTable>
    </div>
    </template>

    <script>
    import MyTable from "./components/MyTable.vue";
    import MyTag from "./components/MyTag.vue"
    export default {
    name: "TableCase",
    components: {
    MyTable,
    MyTag
    },
    data() {
    return {
    name: "茶壶",
    goods: [
    {
    id: 101,
    picture:
    "https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg",
    name: "梨皮朱泥三绝清代小品壶经典款紫砂壶",
    tag: "茶具",
    },
    {
    id: 102,
    picture:
    "https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg",
    name: "全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌",
    tag: "男鞋",
    },
    {
    id: 103,
    picture:
    "https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png",
    name: "毛茸茸小熊出没,儿童羊羔绒背心73-90cm",
    tag: "儿童服饰",
    },
    {
    id: 104,
    picture:
    "https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg",
    name: "基础百搭,儿童套头针织毛衣1-9岁",
    tag: "儿童服饰",
    },
    ],
    };
    },
    };
    </script>

    <style lang="less" scoped>
    </style>

自定义VueCli创建项目

  1. vue create test

  2. 选择Manually select features

  3. 选择需要添加的配置

    image-20240301210417623

  4. 选择Vue版本

    image-20240301210448705

  5. 选择是否使用history模式来配置路由,history模式访问地址没有#

    image-20240301210531494

  6. 选择样式格式

    image-20240301210558060

  7. 选择代码规范

    image-20240301210620049

  8. 确定校验时机,一个是保存的时候,另一个是提交的时候

    image-20240301210716106

  9. 选择配置项放在哪个文件中,是统一放在package.json中还是单独存放,建议单独存放

    image-20240301210804294

  10. 确认以后是否按照这个格式创建

    image-20240301210949858

ESlint代码规范

就是约束代码格式,如果格式不对的话,则会出现错误信息

两种解决方案

  1. 根据错误提示来一项一项手动修改纠正如果你不认识命令行中的语法报错是什么意思,

    根据错误代码去 ESLint 规则表中查找其具体含义

  2. 自动修正

    使用Vscode中的插件ESLint可以自动高亮,并通过配置自动帮我们修复错误

    配置Vscode来自动修复

    1
    2
    3
    4
    5
    6
    // 保存的时候,eslint自动帮我们修复错误
    "editor.codeActionsOnSave": {
    "source.fixAll":true
    },
    // 保存代码,不自动格式化
    "editor.formatOnSave":false

路由

单页应用程序

当网站内容发生改变的时候,只改变其中一部分,而不是整个页面都改变

开发分类 实现方式 页面性能 开发效率 用户体验 学习成本 首屏加载 SEO
单页 一个Html页面 按需更新
性能高
多页 多个Html页面 整页更新
新能低
中等 一般

路径和组件的映射关系

VueRouter

作用:修改地址栏路径时,切换显示匹配的组件

说明:Vue 官方的一个路由插件,是一个第三方包

官网

使用:5+2

5步骤,在main.js中配置 通用

  1. 下载:下载VueRouter模块到当前工程,版本3.6.5

    1
    npm install vue-router@3.6.5
  2. 引入

    1
    import VueRouter from "vue-router"
  3. 安装注册

    1
    Vue.use(VueRouter)
  4. 创建路由对象

    1
    const router = new VueRouter()
  5. 注入,将路由对象注入到new Vue实例中,建立关联

    1
    2
    3
    4
    new Vue({
    render: h=>h(app)
    router
    }).$mount("#app")

两核心

  1. 创建所需要的组件,配置路由规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import Find from "./views/Find.vue"
    import My from "./views/My.vue"
    import Friend from "./views/Friend.vue"

    const router = new VueRouter({
    routes:[
    {path:"/find", component:Find}
    {path:"/my", component:My}
    {path:"/friend", component:Friend}
    ]
    })
  2. 配置导航,配置路由出口,通过router-view来配置路由出口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div>
    <div class="footer_wrap">
    <a href="javascript:;">发现音乐</a>
    <a href="javascript:;">我的音乐</a>
    <a href="javascript:;">朋友</a>
    </div>
    <div class="top">
    <router-view></router-view>
    </div>
    </div>

组件存放目录

分类存放,更易维护

  • src/views文件夹
    • 页面组件-页面展示-配合路由使用
  • src/components文件夹
    • 组件复用-展示数据-常用于复用

路由的封装抽离

将main.js中的路由配置抽离出来,放到router文件夹中 的index.js文件中

在通过export default router 将router导出,并在main.js中引入

当相对路径较长的时候可以使用**@代指src目录**,从src目录出发寻找组件

声明式导航

实现导航高亮效果

vue-router提供了一个全局组件router-link(取代a标签)

router-link会自动封装一些js,当我们点击那个标签的时候,就会给那个标签动态添加类名

优点

  1. 能跳转配置to 属性指定路径(必须)。本质还是a标签,to 无需#

  2. 能高亮默认就会提供高亮类名,可以直接设置高亮样式

用法

1
<router-link to="/路径值"></router-link>

两个类名

  1. router-link-active 模糊匹配

    to="/my" 可以匹配 /my /my/a /my/b

  2. router-link-exact-active 精确匹配

    to="/my" 仅可以匹配 /my

也可以通过VueRouter来配置自定义两个类名

1
2
3
4
5
const router = new VueRouter({
routes:[...],
linkActiveClass:"类名1",
linkExactActiveClass:"类名2",
})

跳转传参

在跳转路由的时候进行传值

查询参数传参

  1. 语法格式:

    • to="/path?参数名=值"
  2. 获取对应页面传过来的值

    • $route.query.参数名
    • 通过axios传递参数方式 this.$route.query.参数名
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div>
    <div class="footer_wrap">
    <router-link to="/find?key=发现">发现音乐</router-link>
    <router-link to="/my?key=我的">我的音乐</router-link>
    <router-link to="/friend?key=朋友">朋友</router-link>
    </div>
    <div class="top">
    <router-view></router-view>
    </div>
    </div>

    获取参数

    1
    2
    3
    4
    5
    <div>
    <p>{{ $route.query.key }}</p>
    <p>发现音乐</p>
    <p>发现音乐</p>
    </div>

动态路由传参

  1. 配置动态路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const router = new VueRouter({
    routes: [
    ...,
    {
    path:"/search/:words",
    component:Search
    }
    ]
    })
  2. 配置导航连接

    to="/path/参数值"

  3. 对应页面组件接受传递过来的值

    $route.params.参数名

传参

1
2
3
4
5
6
7
8
9
10
<div>
<div class="footer_wrap">
<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>
</div>
</div>

可选符

问题:配了路由 path:"/search/:words" 为什么按下面步骤操作,会未匹配到组件,显示空白?
原因:/search/:words表示,必须要传参数。如果不传参数,也希望匹配,可以加个可选符“?"

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [
...,
{
path:"/search/:words?",
component:Search
}
]
})

重定向

匹配path之后,强制跳转到path路径

语法:{path:匹配路径, redirect:重定向到的路径}

1
2
3
4
5
6
7
8
9
10
11
12
import Find from "./views/Find.vue"
import My from "./views/My.vue"
import Friend from "./views/Friend.vue"

const router = new VueRouter({
routes:[
{path:"/", redirect:"/find"},
{path:"/find", component:Find},
{path:"/my", component:My},
{path:"/friend", component:Friend},
]
})

404

作用:当路径找不到匹配的时候,给个提示页面

位置:配在路由最后,当前面几个都不匹配的时候走最后一个

语法:path:"*"

1
2
3
4
5
6
7
8
9
10
11
12
13
import Find from "./views/Find.vue"
import My from "./views/My.vue"
import Friend from "./views/Friend.vue"

const router = new VueRouter({
routes:[
{path:"/", redirect:"/find"},
{path:"/find", component:Find},
{path:"/my", component:My},
{path:"/friend", component:Friend},
{path:"*", component:NotFind}
]
})

模式设置

路由路径上有#,可以通过切换路由模式来将其去掉

hash路由(默认) 例如: http://localhost:8080/#/home

history路由(常用) 例如: http://localhost:8080/home (以后上线需要服务器端支持)

1
2
3
4
const router = new VueRouter({
routes,
mode:"history"
})

基本跳转

两种语法

  1. path路径跳转(简易方便)

    1
    2
    3
    4
    this.$router.push("路由路径")
    this.$router.push({
    path:"路由路径"
    })
  2. name命名路由跳转(适合path路径长的场景

    1
    2
    3
    4
    5
    const router = new VueRouter({
    routes:[
    {name:路由名 ,path:"/find/a/b/c/d", component:Find},
    ]
    })

    跳转方式

    1
    2
    3
    this.$router.push({
    name:"路由名"
    })

路由传参

  1. path路径传参

    query传参, 获取参数方法 $route.query.key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    this.$router.push("/路径?参数名1=参数值1&参数名2=参数值2")

    this.$router.push({
    path:"/路径",
    query:{
    参数名1:参数值1,
    参数名2:参数值2
    }
    })

    动态路由传参, 获取参数方法 $route.params.key

    1
    2
    3
    4
    5
    this.$router.push("/路径/参数值")

    this.$router.push({
    path:"/路径/参数值",
    })
  2. name命名传参

    query传参

    1
    2
    3
    4
    5
    6
    7
    this.$router.push({
    name:"路由名字",
    query:{
    参数名1:参数值1,
    参数名2:参数值2
    }
    })

    动态路由传参,参数名要和路径中的名字相同

    1
    2
    3
    4
    5
    6
    7
    this.$router.push({
    name:"路由名字",
    params:{
    参数名1:参数值1,
    参数名2:参数值2
    }
    })

案例:面经基础班

配路由:

  1. 首页和面经详情页两个一级路由
  2. 首页嵌套四个可切换页面(嵌套二级路由),使用二级路由,在切换页面的时候,底部导航栏不动

image-20240301191137833

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import Vue from 'vue'
import VueRouter from "vue-router";
Vue.use(VueRouter)

// 引入页面
import Layout from "@/views/Layout"
import ArticleDetail from "@/views/ArticleDetail"
import Article from "@/views/Article"
import Collect from "@/views/Collect"
import Like from "@/views/Like"
import User from "@/views/User"


const router = new VueRouter({
routes: [
{
path:"/",
component:Layout,
// 配置子路由,当访问子路由的时候,只修改当前页面的一部分(子路由部分)
children:[
{
path:"/article",
component:Article
},
{
path:"/collect",
component:Collect
},
{
path:"/like",
component:Like
},
{
path:"/user",
component:User
}
]
},
// 配置一级路由
{
path:"/detail",
component:ArticleDetail
},
],
linkActiveClass:"active",
linkExactActiveClass:"exarct-active",
})

export default router

实现功能

  1. 首页请求渲染

    安装axios

    在created中 访问接口 获取数据 存起来

    页面动态渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    <template>
    <div class="article-page">
    <!--数据渲染-->
    <div
    class="article-item"
    v-for="item in articleList"
    :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>

    <script>

    // 请求地址: https://mock.boxuegu.com/mock/3083/articles
    // 请求方式: get
    import axios from "axios"
    export default {
    name: 'ArticlePage',
    data () {
    return {
    articleList:[]
    }
    },
    // 获取数据
    async created(){
    const res = await axios.get("https://mock.boxuegu.com/mock/3083/articles")
    console.log(res)
    this.articleList = res.data.result.rows
    console.log(this.articleList)
    }
    }
    </script>

    <style lang="less" scoped>
    .article-page {
    background: #f5f5f5;
    }
    .article-item {
    margin-bottom: 10px;
    background: #fff;
    padding: 10px 15px;
    .head {
    display: flex;
    img {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    }
    .con {
    flex: 1;
    overflow: hidden;
    padding-left: 15px;
    p {
    margin: 0;
    line-height: 1.5;
    &.title {
    text-overflow: ellipsis;
    overflow: hidden;
    width: 100%;
    white-space: nowrap;
    }
    &.other {
    font-size: 10px;
    color: #999;
    }
    }
    }
    }
    .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    }
    .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
    }
    }
    </style>
  2. 跳转传参到详情页,详情页渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    <template>
    <div class="article-detail-page">
    <!-- 通过使用$router.back()来返回到上一个页面 -->
    <nav class="nav"><span class="back" @click="$router.back()">&lt;</span> {{ detail.subjectName }}</nav>
    <header class="header">
    <h1>{{ detail.stem }}</h1>
    <p>{{ detail.createdAt }} | {{ detail.views }} 浏览量 | {{ detail.likeCount }} 点赞数</p>
    <p>
    <img
    :src="detail.creatorAvatar"
    />
    <span>{{detail.creatorName}}</span>
    </p>
    </header>
    <main class="body">
    {{ detail.content }}
    </main>
    </div>
    </template>

    <script>
    import axios from "axios"
    // 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id
    // 请求方式: get
    export default {
    name: "ArticleDetailPage",
    data() {
    return {
    detail:{}
    }
    },
    async created(){
    const res = await axios.get(`https://mock.boxuegu.com/mock/3083/articles/${this.$route.query.id}`)
    this.detail = res.data.result
    console.log(res)
    }
    }
    </script>

    <style lang="less" scoped>
    .article-detail-page {
    .nav {
    height: 44px;
    border-bottom: 1px solid #e4e4e4;
    line-height: 44px;
    text-align: center;
    .back {
    font-size: 18px;
    color: #666;
    position: absolute;
    left: 10px;
    top: 0;
    transform: scale(1, 1.5);
    }
    }
    .header {
    padding: 0 15px;
    p {
    color: #999;
    font-size: 12px;
    display: flex;
    align-items: center;
    }
    img {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    }
    }
    .body {
    padding: 0 15px;
    }
    }
    </style>
  3. 组件缓存,优化新能

    当我们返回到上一个页面的时候,上一个页面就从新加载了一个,而不是我们上一次的浏览位置,这时候需要使用组件缓存

    缓存组件keep-alive

    • keep-alive是什么

      keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

      keep-alive 是一个抽象组件:它自身不会染成一个 DOM 元素,也不会出现在父组件链中。

    • keep-alive的优点

      在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM

      减少加载时间及性能消耗,提高用户体验性

    1
    2
    3
    4
    5
    6
    7
    <template>
    <div class="h5-wrapper">
    <keep-alive>
    <router-view></router-view>
    </keep-alive>
    </div>
    </template>
    • keep-alive的三个属性

      include:组件名数组,只有匹配的组件会被缓存
      exclude:组件名数组,任何匹配的组件都不会被缓存
      max:最多可以缓存多少组件实例

      组件名最高优先级是export default 中的name名字,如果没有设置才使用文件名

      如果不设置的话默认是全部缓存,会出现我们不想要的元素也被缓存了

      被缓存的组件会多出两个生命周期钩子,且被缓存的组件只会触发一次 created 和 mounted 钩子

      • acitved:组件被激活时
      • deactived:组件失活时
      1
      2
      3
      4
      5
      6
      7
      <template>
      <div class="h5-wrapper" >
      <keep-alive :include="['Layout']">
      <router-view></router-view>
      </keep-alive>
      </div>
      </template>

Vuex

vuex 是一个 vue 的 状态管理工具,状态就是数据

大白话: vuex 是一个插件,可以帮我们管理 vue 通用的数据(多组件共享的数据) 例如:购物车数据 个人信息数据

使用场景:

  • 某个状态在很多个组件来使用(个人信息)
  • 多个组件 共同维护 一份数据(购物车)

优势:

  • 共同维护一份数据,数据集中化管理
  • 响应式变化
  • 操作简洁(vuex提供了一些辅助函数)

1.创建空仓库

安装vuex,初始化一个空仓库

  1. 安装vuex

    1
    npm i vuex@3 --legacy-peer-deps
  2. 新建vuex模块文件

    在src文件夹下创建一个store文件夹,并创建index.js文件

  3. 创建仓库

    stroe/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 导入vuex核心代码
    import Vue from "vue"
    import Vuex from "vuex"

    // 插件安装
    Vue.use(Vuex)

    // 创建仓库
    const store = new Vuex.Store()

    // 导出给main.js使用
    export default store

    main.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Vue from 'vue'
    import App from './App.vue'
    // 导入仓库文件
    import store from './store/index'

    Vue.config.productionTip = false

    new Vue({
    render: h => h(App),
    // 加载配置
    store
    }).$mount('#app')

  4. 获取仓库实例

    1
    this.$stroe

2.state状态-提供数据获取数据

  1. 提供数据

    State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 中的 State 中存储

    在state对象中可以添加我们要共享的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建仓库
    const store = new Vuex.Store({
    // state状态,即数据,类似于Vue组件中的data
    // 区别
    // 1. data是自己的数据
    // 2. state是所有组件共享的数据
    state: {
    属性名:属性值
    }
    })
  2. 使用数据

    • 通过store直接访问
    • 通过辅助函数

    获取store

    • this.$store
    • import 导入 store

    模板中:

    组件逻辑中:this.$store.state.xxx

    JS逻辑中:store.state.xxx

通过Store直接访问

app.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div id="app">
<h1>
<!-- 使用数据 -->
根组件 -{{ $store.state.title }}
</h1>
<input type="text">
<Son1></Son1>
<hr>
<Son2></Son2>
</div>
</template>

<script>
import Son1 from './components/Son1.vue'
import Son2 from './components/Son2.vue'

export default {
name: 'app',
data: function () {
return {

}
},
components: {
Son1,
Son2
}
}
</script>

<style>
#app {
width: 600px;
margin: 20px auto;
border: 3px solid #ccc;
border-radius: 3px;
padding: 10px;
}
</style>

辅助函数获取

mapstate是辅助函数,帮助我们把store中的数据自动映射到组件的计算属性中

  1. 导入mapstate

    1
    import {mapState} from 'vuex'
  2. 以数组方式引入state,返回值是计算属性,一个对象

    1
    mapState(['count', 'title'])
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //mapState(['count', 'title']) 返回值是下面的
    {
    count(){
    return this.$store.state.count
    },
    title(){
    return this.$store.state.title
    }
    }
  3. 通过展开运算符来给计算属性赋值

    不能直接 computed:mapState([‘count’, ‘title’]),这样会导致我们无法配置其他的计算属性

    1
    2
    3
    computed:{
    ...mapState(['count', 'title'])
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div class="box">
<h2>Son1 子组件</h2>
<!--直接使用count即可-->
从vuex中获取的值: <label>{{ count }}</label>
<br>
<button>值 + 1</button>
</div>
</template>

<script>
// 导入辅助函数
import { mapState } from 'vuex'

export default {
name: 'Son1Com',
computed: {
...mapState(['count', 'title'])
}
}
</script>

<style lang="css" scoped>
.box{
border: 3px solid #ccc;
width: 400px;
padding: 10px;
margin: 20px;
}
h2 {
margin-top: 10px;
}
</style>

3.mutations-修改数据

目标:明确vuex同样遵循单向数据流,组件中不能直接修改仓库的数据

通过strict:true可以开启严格模式

修改数据:只能由子组件发起申请,然后让store组件修改

使用方法:

  1. 定义mutations对象,对象中存入修改state的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 创建仓库
    const store = new Vuex.Store({
    state: {
    title: '大标题',
    count: 100
    },
    // 定义mutations对象
    mutations: {
    // 第一个参数是当前store的state属性
    addCount (state) {
    state.count++
    }
    }
    })
  2. 组件中提交调用mutations

    1
    this.$store.commit("addCount")

传参

不支持传递多个参数,如果需要传递多个参数则可以换成数组对象

语法:

1
this.$store.commit('xxx', 参数)
  1. 提供mutation函数

    1
    2
    3
    4
    5
    6
    mutations: {
    // 第一个参数是当前store的state属性
    addCount (state, n) {
    state.count += n
    }
    }
  2. 调用mutation

    1
    this.$store.commit("addCount", 5)

实时渲染

由于vuex中的数据是单向流动的,所以无法使用v-model来绑定数据

可以使用:value 和 @input来实现

  1. 输入框内容渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div id="app">
    <!--通过绑定数据渲染-->
    <input type="text" :value="count">
    </div>
    </template>

    <script>
    import { mapState } from 'vuex'

    export default {
    name: 'app',
    computed: {
    ...mapState(['count', 'title'])
    }
    }
    </script>

  2. @input监听输入获取内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <template>
    <div id="app">
    <!--通过绑定数据渲染-->
    <input type="text" :value="count" @input="handelInput">
    </div>
    </template>

    <script>
    import { mapState } from 'vuex'

    export default {
    name: 'app',
    computed: {
    ...mapState(['count', 'title'])
    },
    methods: {
    // 修改数据
    handelInput (e) {
    const newNum = +e.target.value
    this.$store.commit('changeCount', newNum)
    }
    },
    }
    </script>

mapMutations

它是把位于mutations中的方法提取了出来,映射到组件methods中

和mapState用法相同

1
2
3
4
5
mutations:{
addCount (state, n) {
state.count += n
}
}
1
2
3
4
5
6
7
8
9
10
11
import {mapMutations} from 'vuex'

methods: {
...mapMutations(["addCount"])
}
// 上述方法等价于
methods:{
addCount(n){
this.$store.commit("addCount", n)
}
}

调用方式为

1
this.addCount(10)

4.actions

actions用来处理异步操作

mutations必须是同步的,不能在其中设置setTimeOut等异步语法

使用action可以让数据在1s之后再改变

使用方法

  1. 提供actions方法,在store/index.js中添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     mutations: {
    changeCount (state, n) {
    state.count = n
    }
    }
    actions: {
    setAsyncCount(context, num){
    setTimeOut(()=>{
    context.commit("changeCount", num)
    }, 1000)
    }
    }
  2. 页面中调用dispatch

    1
    this.$store.dispatch("actions名字",额外参数)

辅助函数mapActions

mapActions 是把位于actions中的方法提取了出来,映射到组件methods

1
2
3
4
5
6
7
8
9
10
11
12
 mutations: {
changeCount (state, n) {
state.count = n
}
}
actions: {
setAsyncCount(context, num){
setTimeOut(()=>{
context.commit("changeCount", num)
}, 1000)
}
}
1
2
3
4
5
6
7
8
9
10
11
import {mapActions} from 'vuex'

methods: {
...mapActions(["setAsyncCount"])
}
// 上述方法等价于
methods:{
setAsyncCount(n){
this.$store.dispatch("setAsyncCount", n)
}
}

5.getters

getters和computed方式类似

  1. 定义getters

    1
    2
    3
    4
    5
    getters: {
    filterList(state){
    return state.list.filter(item=>item>5)
    }
    }
  2. 访问getters

    • 通过store访问getters

      1
      {{ $store.getters.filterList }}
    • 通过辅助函数mapGetters映射

      1
      2
      3
      4
      computed:{
      ...mapGetters(['filterList'])
      }
      // 经过计算属性封装之后我们就可以直接使用{{ filterList }}来访问数据

6.module(模块)

将不同模块的数据分开存放

放到 store/modules/文件夹下 以js形式存放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const state = {
userInfo:{
name:'zs',
age:18
}
}
const mutations = {}
const actions = {}
const gettters = {
UpperCaseName( state ){
return state.userInfo.name.toUpperCase()
}
}

export default {
state,
mutations,
actions,
getters,
}

在store下的index.js文件中引入

1
2
3
4
5
6
7
import user from "./modules/user"

const store = new Vuex.Store({
modules:{
user
}
})

访问模块数据

尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的 state 中,属性名就是模块名

访问方式

  1. 直接通过模块名访问 $store.state.模块名.xxx

  2. 通过mapState映射

    默认根级别的映射 mapState([“xxx”])

    1
    2
    3
    4
    5
    computed:{
    ...mapState(['user'])
    }
    // 访问方式
    user.userInfo.name

    子模块映射 mapState(“模块名”, [“xxx”]) - 需要开启命名空间,在对应的模块导出配置项上加上 namespaced:true

    1
    2
    3
    4
    5
    computed:{
    ...mapState('user',["userInfo"])
    }
    // 访问方式
    userInfo.name

模块Getters的访问

  1. 直接通过模块名访问 $store.getters[‘模块名/xxx’]

    1
    2
    3
    <div>
    {{ $store.getters['user/UpperCaseName'] }}
    </div>
  2. 通过mapGetters映射

    • 默认根级别映射 mapGetters([‘xxx’])

    • 子模块映射 mapGetters(“模块名”, [‘xxx’])-需要开启命名空间

      1
      2
      3
      computed:{
      ...mapGetters('user', ['UpperCaseName'])
      }

模块mutation调用

默认模块中的 mutation 和actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

  1. 直接通过 store 调用 $store.commit(“模块名/xxx”, 额外参数)

  2. 通过mapMutations映射

    默认根级别的映射 mapMutations([ xxx])

    子模块的映射 mapMutations( “模块名”,[‘xxx’])- 需要开启命名空间

模块actions调用

  1. 直接通过 store 调用 $store.dispatch(‘模块名/xxx’,额外参数)

  2. 通过mapActions映射

    默认根级别的映射 mapActions([‘xxx’])

    子模块的映射 mapActions( '模块名, [‘xxx’])-需要开启命名空间

购物车案例

创建cart模块

  1. 新建"store/modules/cart.js"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export default {
    namespaced: true,
    state () {
    return {
    // 购物车数据
    list: []
    }
    },
    mutations: {},
    actions: {},
    getters: {}
    }

  2. 挂载到Vuex中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Vue from 'vue'
    import Vuex from 'vuex'
    // 引入模块
    import cart from './modules/cart'

    Vue.use(Vuex)

    export default new Vuex.Store({
    modules: {
    // 挂载模块
    cart
    }
    })

准备Json-server工具,准备后端接口环境

  1. 安装全局工具json-server (全局工具仅需要安装一次)[官网]

    yarn global addjson-server 或 npm i json-server -g

  2. 代码根目录新建一个 db目录

  3. 将资料indexjson 移入 db 目录

  4. 进入db目录,执行命令,启动后端接口服务

    jison-server index.json

  5. 访问接口测试 http://localhost:3000/cart

请求数据存入Vuex,映射渲染

  1. 安装axios,用来获取数据

  2. 准备actions和mutations,因为axios是异步操作,所以要放在actions中,调用actions是需要使用mutations来修改数据

    store/module/cart.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    import axios from 'axios'
    export default {
    namespaced: true,
    state () {
    return {
    // 购物车数据
    list: []
    }
    },
    // 只要是修改数据都要通过mutations
    mutations: {
    UpdataList (state, newList) {
    state.list = newList
    },
    UpdateCount (state, obj) {
    const goods = state.list.find(item => item.id === obj.id)
    goods.count = obj.newCount
    }
    },
    actions: {
    async getList (context) {
    const res = await axios.get('http://localhost:3000/cart')
    context.commit('UpdataList', res.data)
    },
    // 修改count
    async updateCount (context, obj) {
    // 修改数据库
    await axios.patch(`http://localhost:3000/cart/${obj.id}`, {
    count: obj.newCount
    })

    // 修改vuex
    context.commit('UpdateCount', obj)
    }
    },
    // footer的总数和总价
    getters: {
    totalCount (state) {
    return state.list.reduce((sum, item) => sum + item.count, 0)
    },
    totalPrice (state) {
    return state.list.reduce((sum, item) => sum + item.count * item.price, 0)
    }
    }
    }

    App.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <template>
    <div class="app-container">
    <!-- Header 区域 -->
    <cart-header></cart-header>

    <!-- 商品 Item 项组件 -->
    <cart-item v-for="item in list" :key="item.id" :item=item></cart-item>

    <!-- Foote 区域 -->
    <cart-footer></cart-footer>
    </div>
    </template>

    <script>
    import CartHeader from '@/components/cart-header.vue'
    import CartFooter from '@/components/cart-footer.vue'
    import CartItem from '@/components/cart-item.vue'

    import { mapState } from 'vuex'

    export default {
    name: 'App',
    components: {
    CartHeader,
    CartFooter,
    CartItem
    },
    // 一进页面发起请求,将数据放到vuex中
    created () {
    this.$store.dispatch('cart/getList')
    },
    computed: {
    // 将子模块中的数据导出
    ...mapState('cart', ['list'])
    }
    }
    </script>

    <style lang="less" scoped>
    .app-container {
    padding: 50px 0;
    font-size: 14px;
    }
    </style>

  3. 调用actions获取数据

    components/cart-item.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export default {
    name: 'CartItem',
    props: {
    item: {
    type: Object,
    required: true
    }
    },
    methods: {
    // 数据加一减一操作
    updateCount (data) {
    const newCount = this.item.count + data
    if (newCount === 0) return
    const id = this.item.id
    // 调用actions
    this.$store.dispatch('cart/updateCount', {
    id,
    newCount
    })
    }
    }
    }
    <
  4. 动态渲染

案例-智慧商城

创建项目

通过vuecli创建项目

将初始目录调整到符合企业规范的目录

  1. 删除多余的文件

  2. 修改路由配置和app.vue

  3. 新增两个目录 api/utils

    • api接口模块:发送ajax请求的接口模块
    • utils工具模块:自己封装的一些工具方法模块

    image-20240302191615020

vent组件库(移动端)

官网

参考官网快速上手文档

安装

1
2
3
4
5
# Vue 3 项目,安装最新版 Vant
npm i vant

# Vue 2 项目,安装 Vant 2
npm i vant@latest-v2

引入:

  1. 自动按需引入(推荐)

    安装vant-ui

    1
    2
    # Vue 2 项目,安装 Vant 2
    npm i vant@latest-v2

    安装插件

    1
    npm i babel-plugin-import -D

    bable.config.js中配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    plugins: [
    ['import', {
    libraryName: 'vant',
    libraryDirectory: 'es',
    style: true
    }, 'vant']
    ]
    }

    按需引入main.js

    1
    2
    3
    import Vue from 'vue';
    import { Button } from 'vant';
    Vue.use(Button)

    如果导入很多组件的话,则会出现很多个Vue.use 导致main.js杂乱

    可以将这些配置文件放到utils/vant-ui.js 文件夹中

    然后在main.js中引入即可

    1
    import '@/utils/vant-ui'
  2. 手动按需引入

  3. 导入所有组件

    安装vant-ui

    1
    2
    # Vue 2 项目,安装 Vant 2
    npm i vant@latest-v2

    main.js中注册

    1
    2
    3
    4
    5
    import Vue from 'vue';
    import Vant from 'vant';
    import 'vant/lib/index.css';

    Vue.use(Vant);

    测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <div id="app">
    <van-button type="primary">主要按钮</van-button>
    <van-button type="info">信息按钮</van-button>
    <router-view></router-view>
    </div>
    </template>

    <script>
    export default {}
    </script>
    <style>
    </style>

vw适配

官网

安装

1
npm i postcss-px-to-viewport@1.1.1 -D

配置

1
2
3
4
5
6
7
8
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 移动端适配宽度
viewportWidth: 375
}
}
}

使用这个插件可以将px自动转换成vw

在设计网页的时候就可以直接使用px,不必担心适配问题

路由设计配置

创建一级路由并配置

image-20240302212939554

router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import MyOrder from '@/views/myorder'
import Pay from '@/views/pay'
import ProDetail from '@/views/prodetail'
import Search from '@/views/search'
import SearchList from '@/views/search/list'

Vue.use(VueRouter)

const router = new VueRouter({
routes: [
{ path: '/', component: Layout },
{ path: '/login', component: Login },
{ path: '/myorder', component: MyOrder },
{ path: '/pay', component: Pay },
// 动态路由传参,根据id获取指商品详情
{ path: '/prodetail/:id', component: ProDetail },
{ path: '/search', component: Search },
{ path: '/searchlist', component: SearchList }
]
})

export default router

通过vant实现底部导航栏

  1. 导入底部导航栏,vant官网上查看 utils/vant-ui.js

    1
    2
    3
    4
    5
    6
    import Vue from 'vue'

    import { Tabbar, TabbarItem } from 'vant'

    Vue.use(Tabbar)
    Vue.use(TabbarItem)
  2. 配置二级路由路径 router/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Layout from '@/views/layout'
    import Home from '@/views/layout/home'
    import Category from '@/views/layout/category'
    import Cart from '@/views/layout/cart'
    import User from '@/views/layout/user'

    Vue.use(VueRouter)

    const router = new VueRouter({
    routes: [
    {
    path: '/',
    component: Layout,
    redirect: '/home',
    children: [
    { path: '/home', component: Home },
    { path: '/category', component: Category },
    { path: '/cart', component: Cart },
    { path: '/user', component: User }
    ]
    },
    ]
    })
  3. 将对应代码复制到views/layout.vue文件中,并配置二级路由出口,写上二级路由地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <!-- 二级路由出口 -->
    <router-view></router-view>
    <van-tabbar active-color="#ee0a24" inactive-color="#000" route>
    <van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item>
    <van-tabbar-item to="/category" icon="apps-o">分类页</van-tabbar-item>
    <van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
    <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
    </div>

    </template>

    <script>
    export default {
    name: 'LayoutIndex'
    }
    </script>

    <style>

    </style>

登录静态页设计

image-20240303094325956

  1. 配置基础css样式,和图片素材

    styles/common.less

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 重置默认样式
    *{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    // 文字溢出省略号
    .text-ellipsis-2 {
    overflow: hidden;
    -webkit-line-clamp: 2;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    }

    在main.js中引入

    1
    import '@/styles/component.less'
  2. 引入头部导航栏vant组件

    utils/vant-ui.js

    1
    2
    3
    4
    import Vue from 'vue';
    import { NavBar } from 'vant';

    Vue.use(NavBar);

    引入代码 login/index.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <template>
    <div>
    <van-nav-bar
    title="会员登录"
    left-arrow
    @click-left="$router.go(-1)"
    />
    </div>
    </template>

    <script>
    export default {
    name: 'LoginIndex'
    }
    </script>

    <style>
    </style>

  3. 修改vant中的默认样式,找到对应类,并在common.less中使用样式覆盖即可