Vue

1
2
3
4
5
6
const cartList = store.state.cartList   情况1

const { cartList } = toRefs(store.state) 情况2

以上两者有什么区别
为什么computed中情况2 不能监视数据的变化??????
  • 模板中函数调用传参数时为什么要用箭头函数

一、Vue基础

Vue官网-Vue2.x

Vue官网-Vue3

Vue官网导航使用指南视频

1 Vue安装及使用

1.1 直接使用<script>引入

以下安装及使用基于Vue2.x

*引入方式: *

  • 方式一:直接CDN引入
    • 官网位置:官网-学习-教程-安装-直接用<script>引入-CDN
    • 可以选择引入开发环境版本还是生产环境版本,将相应地址在html中通过<script>引入
1
2
3
4
<!-- 开发环境版本,包含了有帮助的命令行警告 --> 
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
  • 方式二: 下载和引入
    • 官网位置:官网-学习-教程-安装-直接用<script>引入
    • 选择开发版本或生产版本直接下载一个js文件(开发阶段下载开发版本)
    • 将下载的文件再在html文件中通过<script>引入
1
2
开发环境 https://vuejs.org/js/vue.js   //下载的文件名:vue.js
生产环境 https://vuejs.org/js/vue.min.js //下载的文件名:vue.min.js

使用Vue:

目录结构

  • 根文件夹
    • test.html
    • src
      • vue.js
  1. 在项目中新建js文件夹 ,若是以上方式二引入Vue则需要将下载的vue.js文件 放入js文件夹下
  2. 在项目中新建html文件,并通过<script>引入Vue
  • 方式一则引入的src为网络地址
    • 方式二则引入地址为本地地址
1
2
3
4
5
6
7
8
9
10
11
12
13
<!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">
<title>vue-demo</title>
<script src="./src/vue.js"></script> //方式二引入
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>//方式一引入
</head>
<body>
</body>
</html>
  1. 创建Vue对象,传入options:{}

    • {}中包含了el属性:该属性决定了这个Vue对象挂载到哪一个元素上,以”id”确定元素。

    • {}中包含了data属性:该属性中通常会存储一些数据

      • 这些数据可以是我们直接定义出来的。
      • 也可能是来自网络,从服务器加载的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!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">
<title>vue-demo</title>
<script src="./src/vue.js"></script>
</head>
<body>
<div id="app">{{message}}
<h1>{{name}}</h1>
</div>
<script>
const app = new Vue({
el:'#app',
data:{
message:'你好呀 hello vue',
name : 'maxthon'
}
})
</script>
</body>
</html>

1.2 npm及CLI安装

npm直接安装vue

1
npm install vue

使用脚手架

  1. (仅第一次执行)∶全局安装@vue/cli
    npm install -g @vue/cli

  2. 切换到要创建项目的目录,然后使用命令创建项目, 执行后会弹出选择安装vue2.x或vue3,自行选择;创建好的项目就已经接受git托管了

    vue create 项目名称

  3. 启动项目
    npm run serve

1.3 单页组件

单页组件视频教程

1.4 初始化项目

2 Vue中的MVVM

  • MVVM维基百科

  • View

    • DOM层。
    • 给用户展示各种信息。
    • 对应Vue中的模板:el
  • Model

    • 数据层
    • 对应Vue的options中的data
  • VueModel

    • 视图模型层:View和Model沟通的桥梁。
    • 实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中
    • 实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情况下改变对应的Data。
    • 对应Vue中的Vue实例对象

3 传入Vue的options

  • 创建Vue对象的时候,传入了一些options:{}
    • {}中包含了el属性:该属性决定了这个Vue对象挂载到哪一个元素上。
    • {}中包含了data属性:该属性中通常会存储一些数据
      • 这些数据可以是我们直接定义出来的。
      • 也可能是来自网络,从服务器加载的。
    • {}中包含了methods属性:该属性中定义了函数
    • {}中包含了computed 属性: 计算属性
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
<div id="app">{{message}}
<h1>{{name}}</h1>
<ul>
<li v-for="item in movies">{{item}}</li>
</ul>
<h1>当前计数:{{count}}</h1>
<button v-on:click="add">+</button>
<button v-on:click="sub">-</button>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
message:'你好呀 hello vue',
name : 'jack ma',
movies:['心灵捕手','阿甘正传','星际穿越'],
count:0
},
methods:{
add(){
console.log('add')
this.count++
},
sub(){
console.log('sub')
this.count--
}
}
})
</script>

4 Vue生命周期

  • 挂载
  • 更新
  • 销毁

4.1 生命周期各时间节点易错点

  1. created()中获取不了组件及元素

报错原因:

  • el是在created()周期函数之后才挂载到Vue中的
  • 所以在created()中获取的组件或元素为空

解决办法:

  • 将获取组件及元素的操作放到mounted()周期函数中执行
  1. mouted()中可能数据还没有加载完成
  • mouted()在el挂载完成后执行
  1. updated()中DOM渲染还没有完成
  • updated()在数据更新到DOM后执行
  1. $nextTick()中图片还没有加载完成
  • $nextTick(()=>{回调函数}) 在DOM渲染完成后执行

    • updated()钩子函数是在是数据更新到DOM完成的时间节点
    • nextTick()函数是在数据更新且DOM渲染完成后的时间节点(DOM渲染完成 不包括图片加载完成)
1
2
3
this.$nextTick(() => {
//DOM渲染完成后需要执行的操作
})

5 vue.config.js配置

  • vue.config.js 文件放在根目录下
1
2
3
4
5
6
7
module.exports = {
devServer:{
port: 8999, //默认打开端口
open: tru //自动在浏览器打开
}

}

二、Vue基础语法

1 插值操作

mustache语法(双大括号)

  • 数据绑定是响应式

v-once

  • 该指令表示元素和组件只渲染一次,view层展示的数据不会随着model层数据的变化而改变。
1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<h1 v-once>{{name}}</h1>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
name : 'jack ma'
}
})
</script>

v-html

  • 该指令用于解析html代码
  • 当数据内容本身就是html代码时可以只用这个指令解析并展示
  • v-html :会有XSS风险,会覆盖子组件

1590900171394

v-text

  • v-text作用和Mustache比较相似:都是用于将数据显示在界面中
  • v-text通常情况下,接受一个string类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<h1 >{{name}}</h1>---------1
<h1 v-text="name"></h1>----2
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
name : 'jack ma'
}
})
</script>


//展示效果相同
jack ma------1
jack ma------2

v-pre

  • v-pre用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<h1 >{{name}}</h1>---------1
<h1 v-pre >{{name}}</h1>----2
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
name : 'jack ma'
}
})
</script>


//展示效果相同
jack ma------1
{{name}}------2

v-cloak

  • 在vue实例编译完成之前,浏览器会直接显示未编译的Mustache标签。
  • 为了避免这个问题,设置v-cloak指令可以 不显示Mustache标签,直到编译结束再显示编译之后的效果
  • [v-cloak]{} 设置未编译完成之前的显示效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<h1 v-cloak>{{name}}</h1>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
name : 'jack ma'
}
})
</script>
<style>
[v-cloak] {
display: none; //设置为不显示
}
</style>

2 绑定属性

v-bind

  • v-bind用于: 1绑定一个或多个属性;2向组件传递props值

  • 可以动态绑定的属性:图片链接src、网站链接href、类、样式等

  • v-bind的语法糖::

    1
    v-bind:src="http:\\a.png"    等同于    :src="http:\\a.png"

v-bind绑定class

  • 1 对象语法

    1
    2
    3
    //'active'、'line'为类名
    //isActive、isLine 为标志位 只有true、false取值 控制类是否生效
    <h2 class="title" :class="{'active': isActive, 'line': isLine}">Hello World</h2>
  • 2 数组语法

    • 为类名时带引号
    1
    2
    //'active'、'line'为类名
    <h2 class="title" :class="['active’, 'line']">Hello World</h2>
    • 为变量名时不带引号
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div id="app">
    <h1 :class="[active, line]">{{name}}</h1>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    active : {color: red},
    line: {font-size: 10px}
    }
    })
    </script>

v-bind绑定style

  • 1 对象语法

    • style后面跟的是一个对象类型
      • 对象的key是CSS属性名称
      • 对象的value是具体赋的值,值可以来自于data中的属性
    1
    :style="{color: currentColor, fontSize: fontSize + 'px'}"
  • 2 数组语法

    1
    2
    3
    4
    5
    <div :style="[baseStyles, overridingStyles]"></div>

    data: {
    baseStyles:{backGroundColor:'red'} //注意转换成驼峰形势
    }

3 计算属性

computed

  • computed用于将数据进行转化、多个数据相结合再提供一个简单的属性绑定到html模块中

    • 案例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="app">
    <h1>{{fullName}}</h1>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    firstName: 'jack',
    lastName: 'ma'
    },
    computed: {
    fullName(){
    return this.firstName+ ' '+ lastName
    }
    }
    })
    </script>
    //效果
    jack ma
  • compooutedgetset

    • computed中定义的计算属性本来是包含getset方法,get用于获取计算属性的结果,set用于设置计算属性,但是一般都只需要获取get,所以就简写成上面的函数形式,直接返回计算结果
    • 双向绑定(v-model)时,必须有set 和 get,否则会报错
  • 案例

    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
    <div id="app">
    <h1 >{{fullName}}</h1>
    <input v-model="double2"/>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    firstName: 'jack',
    lastName: 'ma',
    num: 0
    },
    computed: {
    fullName: {
    get(){
    return this.firstName+ ' '+ lastName
    },
    set(newName) {
    const names = newName.split(' ‘)
    this.firsName = names[0]
    this,lastName = names[1]
    }
    },
    double2: {
    get() {
    return this.num * 2
    },
    set(val) {
    this.num = val/2
    }
    }
    }
    })
    </script>
    //效果
    jack ma

computed VS methods

  • computed 和 methods中定义的方法都可以达到对data数据加工的效果
  • 但是:计算属性会进行缓存,如果多次使用时,计算属性只会调用一次 即只会计算一次 然后将计算结果保存,避免了重复无用的计算,节省了计算能力。

4 watch

当属性发送变化时就会触发

  • watch 监听引用类型,拿不到oldVal, 因为oldVal 和 val都是存储的引用地址,而引用地址是没有变化的只是引用的堆中的值类型发生了变化
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>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>

<script>
export default {
data() {
return {
name: '双越',
info: {
city: '北京'
}
}
},
watch: {
name(oldVal, val) {
// 值类型,可正常拿到 oldVal 和 val
console.log('watch name', val, oldVal)
},
info: {

handler(oldVal, val) {
// 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
console.log('watch info', val, oldVal)
},
deep: true, // 深度监听
immediate: true // 该回调将会在侦听开始之后被立即调用

},
// 'info.city':{
// handler(oldVal, val) {
// 此处是值类型,也可正常拿到 oldVal 和 val
// console.log('watch city', val, oldVal)
// }
// },
'info.city'(oldVal, val) {
// 此处是值类型,也可正常拿到 oldVal 和 val
console.log('watch city', val, oldVal)
}
}
}
</script>

5 事件监听

v-on

  • v-on官方教程

  • v-on监听的事件包括DOM原生事件和组件自定义事件

  • v-on的语法糖:@

  • 案例

    • 三个点击按钮的效果一样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="app">
    <h1>{{counter}}</h1>
    <button v-on:click="counter++">点击counter+1</button>
    <button v-on:click="onCounter">点击counter+1</button>
    <button @:click="onCounter">点击counter+1</button>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    counter: 0
    },
    monteds: {
    onCounter(){
    this.counter++
    }
    }
    })
    </script>

v-on参数

  • 如果v-on方法不需要额外参数,那么方法后的()可以不添加

    • 不带()时,如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
  • 如果v-on需要传入某个参数,同时需要event时,可以通过$event传入原生事件。

  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <div id="app">
    <h1>{{counter}}</h1>
    <button @:click="addOne">点击counter+1</button>
    <button @:click="addTen(10, $event)">点击counter+10</button>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    counter: 0
    },
    monteds: {
    addOne(event){
    console.log(event)
    this.counter++
    },
    addTen(count, event){
    this.counter += count
    console.log(event)
    }
    }
    })
    </script>

v-on 修饰符

  • .stop: 阻止冒泡
  • .prevent: 阻止默认事件的调用 例如form的submit(提交)
  • .native:监听组件根元素的原生事件。
  • .once :只触发一次回调。

DOM原生事件列表

DOM原生事件列表

6 条件判断

v-if、v-else-if、v-else

  • v-if的原理

  • v-if后面的条件为false时,对应的元素以及其子元素不会渲染。也就是根本不会有对应的标签出现在DOM中。

  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <div id="app">
    <div v-if=“score>=90”>优秀</div>
    <div v-else-if=“score>=60”>及格</div>
    <div v-else>不及格</div>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    score: 99
    }
    })
    </script>

v-if VS v-show

  • v-if条件为false时,对应的元素以及其子元素不会渲染。(通过Vue控制)
  • v-show条件为false时,对应的元素以及其子元素会渲染。只是将元素的display属性设置为none,所以没有显示出来。(通过css控制)
  • 总结:
    • 当需要在显示与隐藏之间切片很频繁时,使用v-show
    • 当只有少次切换时,通常使用v-if

7 遍历循环

v-for

  • 遍历数组

    • item: 数组中的元素
    • index: 数组中元素的索引
    1
    v-for=(item, index) in items
  • 遍历对象

    1
    2
    (属性值,属性名,属性索引)  in   info 
    (value, key, index) in info

:key

  • key的作用主要是为了高效的更新虚拟DOM。

  • 当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点,在B和C之间加一个F

    1590913636403

    • Vue的虚拟DOM的Diff算法的处理方式是:C更新成F,D更新成C,E更新成D,最后再插入E,所以效率很低

    • 使用key

      • key相当于给每个节点做了唯一的标识
      • 这样就可以直接插入到B、C直接 效率更高

8 双向绑定

v-model

  • v-model指令用于实现表单元素和数据的双向绑定

  • 可使用标签:inputtextarea

  • 案例

    • 双向绑定流程
      • 因为input中的v-model绑定了message,所以会实时将输入的内容传递给message,message发生改变。
      • 当message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div id="app">
    <input type="text" v-model="message">
    <h2>{{message}}</h2>
    </div>
    <script src="../js/vue.js"></script>
    <script>
    const app = new Vue({
    el:'#app',
    data:{
    message: ''
    }
    })
    </script>
  • v-model原理

    • 1.v-bind绑定一个value属性
    • 2.v-on指令给当前元素绑定input事件,将输入赋值给data中的数据
    1
    2
    3
    <input type="text" v-model="message">
    等同于
    <input type="text" v-bind:value="message" v-on:input="message = $event.target.value">

v-model:radio

  • 用于多个单选框

v-model:checkbox

  • 单个勾选框:
    • v-model即为布尔值.
    • 此时input的value并不影响v-model的值。
  • 多个复选框:
    • 当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
    • 当选中某一个时,就会将input的value添加到数组中。

v-model:select

  • 单选:
    • 只能选中一个值。
    • v-model绑定的是一个值。
    • 当我们选中option中的一个时,会将它对应的value赋值到mySelect中
  • 多选:可以选中多个值。
    • v-model绑定的是一个数组。
    • 当选中多个值时,就会将选中的option对应的value添加到数组mySelects中

v-model修饰符

  • lazy修饰符:
    • 默认情况下,v-model默认是在input事件中同步输入框的数据的。
    • 也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。
    • lazy修饰符可以让数据在失去焦点或者回车时才会更新:
  • number修饰符:
    • 默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。
    • 但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。
    • number修饰符可以让在输入框中输入的内容自动转成数字类型:
  • trim修饰符:
    • 如果输入的内容首尾有很多空格,通常我们希望将其去除
    • trim修饰符可以过滤内容左右两边的空格
1
2
3
v-model.lazy="message"
v-model.number="message"
v-model.trim="message"

9本地存储

  • 存: 任何位置直接使用
1
2
3
4
5
localStorage.islogn = 'xxxx'


const cartListString = JSON.stringify(cartList) // 将对象转换为字符串, 因为本地缓存不能存储对象
localStorage.cartList = cartListString
  • 取: 任何位置直接使用
1
2
3
4
5
6
const islogn localStorage.islogn
或者
const { isLogin } = localStorage


JSON.parse(localStorage.cartList) // 将字符串转换为d

三、Vue组件化开发

1 组件化开发基础内容

1.1 注册组件

  1. 创建组件构造器
  • Vue.extend()
    • 传入自定义组件的模板 template
    • Vue2.x后这种写法就不再使用
  1. 注册组件
  • Vue.component()
    • 将组件构造器注册为一个组件,并且给它起一个组件的标签名称。
    • 传递两个参数:1、注册组件的标签名 2、组件构造器
  1. 使用组件
    • 组件必须挂载在Vue实例下
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">
<!-- 3 使用组件 -->
<m-cpn></m-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1 创建组件构造器
const myComponent = Vue.extend({
template: `
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
`
})
// 2 注册组件 并定义组件标签的名称
Vue.component('m-cpn', myComponent)

const app = new Vue({
el:'#app'
})
</script>

1.2 全局组件与局部组件

  1. 全局组件
  • 调用Vue.component()注册组件时,组件的注册是全局的。通该方法注册的组件可以在任意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
// app1、app2两个实例都可以使用这个组件
<div id="app1">
<!-- 3 使用组件 -->
<m-cpn></m-cpn>
</div>
<div id="app2">
<!-- 3 使用组件 -->
<m-cpn></m-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1 创建组件构造器
const myComponent = Vue.extend({
template: `
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
`
})
// 2 注册组件 并定义组件标签的名称
Vue.component('m-cpn', myComponent)

const app1 = new Vue({
el:'#app1'
})

const app2 = new Vue({
el:'#app2'
})
</script>
  1. 局部组件
  • 若组件是在某个Vue实例中注册的,那么这个组件是局部组件,只能在这个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
// app1可以使用这个组件   app2不会渲染出该组件
<div id="app1">
<!-- 3 使用组件 -->
<m-cpn></m-cpn>
</div>
<div id="app2">
<!-- 3 使用组件 -->
<m-cpn></m-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1 创建组件构造器
const myComponent = Vue.extend({
template: `
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
`
})

const app1 = new Vue({
el:'#app1',
components: {
'my-cpn': myComponent // 2 注册组件 并定义组件标签的名称
}
})

const app2 = new Vue({
el:'#app2'
})
</script>

1.3 父组件与子组件

  • 传入组件构造器可以有: 模板template,还可以注册子组件components
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
<div id="app">
<!-- 3 使用组件 -->
<parent-cpn></parent-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1 创建子组件构造器
const myComponent1 = Vue.extend({
template: `
<div>
<h2>我是子组件</h2>
<p>我是子组件内容</p>
</div>
`
})
// 2 创建父组件构造器
const myComponent2 = Vue.extend({
template: `
<div>
<h2>我是父组件</h2>
<p>我是父组件内容</p>
</div>
`,
components: {
'child-cpn': myComponent1 //父组件中注册子组件
}
})

const app = new Vue({
el:'#app',
components: {
'parent-cpn': myComponent2 //Vue实例中注册父组件
}
})
</script>

1.4 注册组件语法糖

  1. 注册全局组件语法糖
1
2
3
4
5
6
7
8
Vue.component('m-cpn', {
template: `
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
`
})
  1. 注册局部组件语法糖
1
2
3
4
5
6
7
8
9
10
11
12
13
const app = new Vue({
el:'#app',
components: {
'parent-cpn': {
template: `
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
`
}
}
})

1.5 模板分离写法

  1. 使用<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
<!-- 使用组件 -->
<div id="app">
<m-cpn></m-cpn>
</div>

<!-- 组件模板 -->
<script type="text/x-template" id="myCpn">
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
</script>
<script src="../js/vue.js"></script>

<!-- Vue实例 -->
<script>
const app = new Vue({
el:'#app',
components: {
'm-cpn': {
template: '#myCpn'
}
}
})
</script>
  1. 使用<template>标签实现
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">
<m-cpn></m-cpn>
</div>

<!-- 组件模板 -->
<template id="myCpn">
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
</div>
</template>

<script src="../js/vue.js"></script>
<!-- Vue实例 -->
<script>
const app = new Vue({
el:'#app',
components: {
'm-cpn': {
template: '#myCpn'
}
}
})
</script>

1.6 组件数据存储

  • data在组件中必须是一个函数
    • 因为组件会在多个位置使用,若data是对象形式,那么多个使用的位置就会指向同一个数据内存地址,一个位置修改数据,其他地方也会发生改变,导致组件相互影响,不能单独使用。若data是函数形式,在多个位置使用该组件时,会data函数会返回一个属于使用时组件自己的数据存储地址,使得组件使用是隔离的,不会互相影响。
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
// 使用组件 
<div id="app">
<m-cpn></m-cpn>
</div>

// 组件模板
<template id="myCpn">
<div>
<h2>组件标题</h2>
<p>我是组件内容</p>
<p>{{message}}</p>
</div>
</template>
<script src="../js/vue.js"></script>

// Vue实例
<script>
const app = new Vue({
el:'#app',
components: {
'm-cpn': {
template: '#myCpn',
data(){
return {
message: '我是组件数据'
}
}
}
}
})
</script>

2 父子间通信

2.1 父级向子级传递

  • 在组件中,使用选项props来声明需要从父级接收到的数据。

  • props的两种写法

    • 1 props: {数据名: 数据类型声明}
    • 2 props: {数据名: {type:数据类型声明, default: 该数据默认值 }}
  • 注意:

    • 数据可以有多个类型: props: {message: [string, Number]}

    • 对象和数组默认值必须是函数

      1
      2
      3
      4
      5
      6
      7
      8
      props: {
      cInfo: {
      type: Object,
      default() {
      return {}
      }
      }
      }
  • 案例:

    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
    <div id="app">
    <cpn :c-info="info" ></cpn>
    </div>

    <template id="cpn">
    <div>
    <h2>{{cInfo}}</h2>
    <h2>{{childMyMessage}}</h2>
    </div>
    </template>

    <script src="../js/vue.js"></script>
    <script>
    const cpn = {
    template: '#cpn',
    props: {
    cInfo: {
    type: Object,
    default() {
    return {}
    }
    }
    }
    }

    const app = new Vue({
    el: '#app',
    data: {
    info: {
    name: 'why',
    age: 18,
    height: 1.88
    }
    },
    components: {
    cpn
    }
    })
    </script>

2.2 子级向父级传递

通过自定义事件实现子级向父级传递数据和事件

  • 自定义事件流程:

    • 1 在子组件中,通过$emit()来触发事件。(出发事件 同时可将数据传递出去)

      1
      this.$emit('自定义事件名',数据)
    • 2 在父组件中,通过v-on来监听子组件事件。

      1
      2
      3
      //父组件中 
      // <cpn>为子组件
      <cpn @自定义事件名="cpnClick"></cpn>
  • 案例:

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
<!--父组件模板-->
<div id="app">
<cpn @item-click="cpnClick"></cpn>
</div>

<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in categories"
@click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>

<script src="../js/vue.js"></script>
<script>

// 1.子组件
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{id: 'aaa', name: '热门推荐'},
{id: 'bbb', name: '手机数码'},
{id: 'ccc', name: '家用家电'},
{id: 'ddd', name: '电脑办公'},
]
}
},
methods: {
btnClick(item) {
// 发射事件: 自定义事件
this.$emit('item-click', item)
}
}
}

// 2.父组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn
},
methods: {
cpnClick(item) {
console.log('cpnClick', item);
}
}
})
</script>

3 父子组件的直接访问

3.1 父访问子

3.1.1 $children

语法:

1
2
3
4
5
6
7
8
9
//返回值为数组  拿到当前组件的所有子组件
this.$children

//以索引值拿到特定的子组件
this.$children[i]

//拿到特定组件后可以:1获取、修改子组件的数据 2调用子组件的方法 等
this.$children[i].属性
this.$children[i].方法

$children的缺陷

  • 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。

  • 需要快速且明确获取其中一个特定的组件,这个时候就可以使用$refs

3.1.2 $refs

语法:

  • $refs和ref指令配合使用

  • 1 通过ref给某一个子组件绑定一个特定的ID。

    1
    <child-cpn ref="child"></child-cpn>
  • 2 通过this.$refs.ID就可以访问到该组件

    1
    this.$refs.child.属性/方法

3.2 子访问父

3.2.1 $parent

语法:

1
2
3
4
5
6
//在子组件中  使用先的语法可以拿到父组件  
this.$parent

//拿到特父组件后可以:1获取、修改父组件的数据 2调用父组件的方法 等
this.$parent.属性
this.$parent.方法
  • 不建议使用该方式操作父组件数据 因为这样会提高耦合度 容易引起一些问题 也不便于维护

3.2.2 $root

  • $root可以在子组件中直接访问根组件也就是Vue对象,可以访问Vue对象下的数据和方法

  • 语法:

    1
    this.$root

3.3 非父子访问

  • 非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。

3.3.1 中央事件总线

事件总线类似于Vuex, 只是Vuex用于管理状态, 事件总线用于管理事件

应用场景:

  • 相隔一个或多个组件的两个组件之间的通信

使用:

  • main.js中新建Vue实例作为事件总线: Vue.prototype.$bus = new Vue()
1
2
3
4
5
6
Vue.prototype.$bus = new Vue()

new Vue({
router,
render: h => h(App),
}).$mount('#app')
  • 组件发出全局事件:
1
this.$bus.$emit('发出事件名',参数)
  • 组件监听全局事件:(一般在生命周期函数总监听)
1
2
3
this.$bus.$on('发出事件名', (参数) => {
//处理事件
})
  • 组件取消全局事件的监听
    • 注意在组件销毁的生命周期函数(beforeDestory)中执行取消该组件自定义的全局事件
1
this.$bus.$off('发出事件名', 函数名)   //函数名:需要取消事件监听的函数

4 插槽

插槽官网教程

  • 基本使用
1
2
3
4
5
6
7
8
//父组件
<template>
<div>
<Son >//子组件
<div>{{值}}</div>//放入插槽的内容
</Son>
</div>
</template>
1
2
3
4
5
6
7
8
//子组件
<template>
<a>
<slot>
默认值显示当前这行话 ,即父组件不传内容时显示
</slot>
</a>
</template>
  • 作用域插槽
    • 父组件可以拿到子组件data中的值并将该值作为插入的一部分插入
1
2
3
4
5
6
7
8
9
10
11
//父组件
<template>
<div>
<Son >//子组件
<template v-slot="slotProps">//自定义名
//此时的slotProps.slotData及指向了子组件data的website对象并可访问
{{slotProps.slotData.title}}//slotData子组件中定义,title子组件data中属性
</template>
</Son>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//子组件
<template>
<a>
<slot :slotData="website">//父组件可以通过slotData拿到data中的website对象
{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
</slot>
</a>
</template>

<script>
export default {
data() {
return {
website: {
url: 'http://wangEditor.com/',
title: 'wangEditor',
subTitle: '轻量级富文本编辑器'
}
}
}
}
</script>
  • 具名插槽
    • 当有多个插槽时便于区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//父组件
<template>
<div>
<Son >//子组件
<!--缩写<template #header> -->
<template v-slot:header>
<h1>将插入header slot中</h1>
</template>

<p>将插入到main slot 中,即未命名的slot</p>

<template v-slot:footer>
<p>将插入到footer slot中</p>
</template>
</Son>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//子组件
<template>
<div>
<header>
<slot name="header"></slot>
</ header>
<main>
<slot></slot>//默认插入位置
</ main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>

脚手架3中 配置文件别名

  1. 在根目录下新建vue.config.js
  • ‘@’表示: ‘src’
  • alias下配置别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//vue.config.js
module.exports = {
configureWebpack:{
resolve: {
alias: {
'assets': '@/assets',
'common': '@/common',
'components': '@/components',
'network': '@/network',
'store': '@/store',
'views': '@/views',
}
}
}
}

混入

混入提供了一种将组件中共有的部分抽离实现可复用的功能。组件中的任何部分都可抽离(data,methods,生命周期函数等)

定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
},
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}

使用:

1
2
3
4
//app对象中就包含了 mixin对象中的data、methods、生命周期函数
const app = new Vue({
mixins: [mixin],
})

混入规则:

  • 1 data(数据)合并冲突 以组件数据优先
  • 2 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
  • 3 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

getters方法->组件计算属性

将vuex中的getter中的方法直接转化成组件的计算属性

store定义:

1
2
3
4
5
6
7
8
9
10
11
//state
const state = {
cartList: []
}

//getters.js
export default {
cartLength(state){
return state.cartList.length
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//导入
import { mapGetters } from 'vuex'

//使用
computed: {
//1 不改别名用法
...mapGetters(['cartLength']),
//2 改别名用法
...mapGetters({
length: 'cartLength'
})
}

//html
<p>{{cartLength}}</p> //1 不改别名用法
<p>{{length}}</p> //2 改别名用法

actions方法->组件方法

将vuex中的actions中的方法直接转化成组件的方法中的方法

  • 注意:actions可以返回promise

store定义:

1
2
3
4
5
6
7
8
9
10
11
12
//actions.js
export default {
addCart(context, payload){
return new Promise((resolve, reject) => {
if(...){
resolve('成功')
}else{
resolve('成功')
}
})
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//导入
import { mapActions } from 'vuex'

//使用
methods: {
//1 不改别名用法
...mapActions(['addCart'])
//2 改别名用法
...mapActions({
add: 'addCart'
})
}

//调用
//原始调用
this.$store.dispatch('addCart', cart).then((res) => {
console.log(res)
})
//引入后调用
this.addCart(cart).then((res) => {
console.log(res)
})

零碎知识点

  • 拿到组件的元素$el
1
2
//例:获取scroll组件到顶部的距离
this.$refs.scroll.$el.offsetTop

四、Vue3

源码:L264-code

不熟悉的特性

Non-Props

非 Prop 的 Attribute

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>lesson 17</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// Non-prop 属性
const app = Vue.createApp({
template: `
<div>
<counter msg="hello" msg1="hello1" @update="change"/>
<counter1 msg="hello" msg1="hello1" />
</div>
`,
methods:{
change(){
console.log('update')
}
}
});

app.component('counter', {
// inheritAttrs: false,
mounted() {
console.log(this.$attrs.msg);
console.log(this.$attrs)
this.$attrs.onUpdate() //事件也可通过Non-Pro的方式传入子组件并调用,注意要加onXxxx()调用
},
// 子组件的根元素是多元素时,父组件传入的Non-Prop属性不会自动添加到根元素上
// 需要指定Non-Prop属性绑定到哪个元素上
template: `
<div :msg="$attrs.msg">Counter</div>
<div v-bind="$attrs">Counter</div> //所有属性都绑定到该元素上
<div :msg1="$attrs.msg1">Counter</div>
`
});

app.component('counter1', {
// inheritAttrs: false, //不接受父元素传入的Non-Prop属性自动绑定再g
// 子组件的根元素是单元素时,父组件传入的Non-Prop属性会自动添加到根元素上
template: `
<div>Counter</div>
`
});

const vm = app.mount('#root');
</script>
</html>

v-model

自定义组件上使用v-model

v-model作用于子组件 —- 直接参考官方文档

  • 默认写法,单个v-model
  • 多个绑定,多个v-model
  • 处理v-model修饰符
  • 下面案例即默认情况
1
<my-component v-model="bookTitle"></my-component>
1
2
3
4
5
6
7
8
9
10
11
12
app.component('my-component', {
props: {
modelValue: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
`
})
  • 关键有两点
    • :value="modelValue" 绑定传入的属性
    • @input="$emit('update:modelValue', $event.target.value)" 内容修改时传出修改的内容
  • 自定义组件响应式案例
    • 实现输入框内容在组件内是双向绑定的,且与外部的变量也是双向绑定的,主要利用了computed的get和set函数
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 class="pb-3">
<input
v-model="inputRef.val"
>
</div>
</template>

<script lang="ts">
import { defineComponent, reactive, computed } from 'vue'
export default defineComponent({
name: 'ValidateInput',
props: {
modelValue: String
},
setup (props, context) {
const inputRef = reactive({
val: computed({
get: () => props.modelValue || '',
set: val => {
context.emit('update:modelValue', val) // 当inputRef.value.val变化时触发
}
})
})
return {
inputRef
}
}
})
</script>

Provide / Inject

Provide / Inject

将父组件的参数直接传递到孙子组件

  • 传递data中的属性时,需要以返回对象的方式传递
  • 响应式的处理方式

directive

自定义指令

plugin

插件

源码:plugin (数据校验案例)

defineComponent

可以使得包裹的对象有类型提示, 例如打set 就会自动提示setup

1
2
3
4
5
6
7
8
9
const compontent = defineComponent({
name: '',
props:{
msg:{
required: true, //要求一定是string类型
type: String
}
}
})

setup参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const compontent = defineComponent({
name: '',
props:{
msg:{
required: true, //要求一定是string类型
type: String
}
}
setup(props, context){
//props拿到的就是上面的props,props.m 就是自动提示propsmsg
context.attrs
context.slots
context.emit
}
})

Teleport

传送门,即将一些弹窗组件挂载在别的地方,而不是嵌在当前组件中,这样弹窗组件样式和当前组件样式就不会相互影响

手动挂载

定义弹窗组件

  • 通过标签<teleport>表明当前组件是传送门组件 通过to指明要挂载的位置
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>
<teleport to="#modal">
<div id="center" v-if="isOpen">
<h2><slot>this is a modal</slot></h2>
<button @click="buttonClick">Close</button>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
isOpen: Boolean,
},
emits: {
'close-modal': null
},
setup(props, context) {
const buttonClick = () => {
context.emit('close-modal')
}
return {
buttonClick
}
}
})
</script>
<style>
#center {
width: 200px;
height: 200px;
border: 2px solid black;
background: white;
position: fixed;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
</style>

挂载弹窗组件

  • 挂载位置:根目录/public/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!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="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="modal"></div> <!-- 挂载弹窗组件 -->
<!-- built files will be auto injected -->
</body>
</html>

使用弹窗组件

  • 直接在需要的位置使用即可, 需要引入组件
1
2
3
4
5
6
7
8
9
10
const modalIsOpen = ref(false)
const openModal = () => {
modalIsOpen.value = true
}
const onModalClose = () => {
modalIsOpen.value = false
}

<button @click="openModal">Open Modal</button><br/>
<modal :isOpen="modalIsOpen" @close-modal="onModalClose"> My Modal !!!!</modal>
自动挂载

应用场景:开发者在使用弹窗组件时,还需要去根目录/public/index.html手动挂载,这会非常麻烦,所以需要自动挂载,即使用组件时,组件自动去挂载

  • setup()生命周期是先于页面初始化的,所以可以在setup()直接拿到document.body并在其中添加挂载的节点,这样就完成了自动挂载
  • 注意:组件销毁前要移除挂载的这个节点,因为组件可能多次使用,只添加不移除就会导致document.body中有冗余的挂载节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<teleport to="#loader"> //挂载节点的id
<div>
组件内容
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent, onUnmounted } from 'vue'
export default defineComponent({
setup () {
const node = document.createElement('div') //新建节点 即组件要挂载的节点
node.id = 'loader' //将新建节点的id设置为相应值
document.body.appendChild(node) //将新节点添加到 document.body中
onUnmounted(() => {
document.body.removeChild(node) //组件销毁前要移除挂载的这个节点
})
}
})
</script>

Suspense

等待异步组件时渲染一些额外内容,让应用有更好的用户体验

定义异步组件

  • 在 setup 返回一个 Promise
  • 这个promise包裹的对象自动具有响应式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//AsyncShow.vue
<template>
<img :src="result && result.message">
</template>

<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
async setup() {
const rawData = await axios.get('https://dog.ceo/api/breeds/image')
return {
result: rawData.data
}
}
})
</script>

使用异步组件

  • #default : 展示异步组件,可以展示多个异步组件,且当多个异步组件都加载完成后才一起显示
  • #fallback: 在异步组件还没加载完成时展示的内容
1
2
3
4
5
6
7
8
9
<Suspense>
<template #default>
<async-show />
<dog-show />
</template>
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>

捕获异步组件的错误

  • 通过onErrorCaptured捕获错误
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
<template>
<div id="app">
<p>{{error}}</p> // 将捕获的错误展示在页面上
<Suspense>
<template #default>
<div>
<async-show />
<dog-show />
</div>
</template>
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
</div>
</template>

<script lang="ts">
import { ref, onErrorCaptured } from 'vue'
import AsyncShow from './components/AsyncShow.vue'
import DogShow from './components/DogShow.vue'
export default {
name: 'App',
components: {
AsyncShow,
DogShow,
},
setup() {
const error = ref(null)
onErrorCaptured((e: any) => {
error.value = e
return true //错误是否向上传递
})
return {
error
}
}
};
</script>

PropType

构造函数断言成一个类型

1
2
 //这里特别有一点,我们现在的 Array 是没有类型的,只是一个数组,我们希望它是一个 ColomnProps 的数组,那么我们是否可以使用了类型断言直接写成 ColomnProps[],显然是不行的 ,因为 Array 是一个数组的构造函数不是类型,我们可以使用 PropType 这个方法,它接受一个泛型,将 Array 构造函数返回传入的泛型类型。
type: Array as PropType<ColumnProps[]>,

父子间通信

子级向父级传递

emits

1
2
3
4
5
6
7
8
9
10
<script lang="ts">
import { defineComponent} from 'vue'
export default defineComponent({
emits: ['file-uploaded', 'file-uploaded-error'], //组件内定义的向外部发送信息的事件名
setup (props, context) {
context.emit('file-uploaded')// 向外部发送事件,不传递参数
context.emit('file-uploaded-error', res)// 向外部发送事件,传递参数res
}
})
</script>

事件总线

mitt

  • vue3取消了vue2中自带的事件总线$bus
  • vue3中通过第三方库mitt实现事件总线

VueX实现

Vue3中的动画

Animate.css

新脚手架

  • 卸载老版本的脚手架
1
npm uninstall vue-cli -g
  • 安装新的脚手架
  1. (仅第一次执行)∶全局安装@vue/cli
    npm install -g @vue/cli

    npm install -g @vue/cli@版本号

  2. 切换到要创建项目的目录,然后使用命令创建项目, 执行后会弹出选择安装vue2.x或vue3,自行选择;创建好的项目就已经接受git托管了

    vue create 项目名称

  3. 启动项目
    npm run serve

五、vue-router

Vue-Router官方文档

1 安装

脚手架创建项目时就安装vue-router

  1. vue create 项目名称
  2. 选择Manually select features自定义安装的插件
  3. 空格选择Router
image-20210815162248328
  1. 选择vue3:3.x
  2. 是否选择history模式路由,不选择,则默认选择为hash模式路由

image-20210815162457644

  1. 选择仅错误时提醒ESlint

image-20210815162645470

使用过程中安装

1
npm install vue-router@版本号

2 使用路由

2.1 配置控制跳转

  • main.js中引入router
1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' //注意是从下面的/router/index.js中引入

createApp(App).use(store).use(router).mount('#app')
  • /router/index.js设置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/home/Home')
}
]

const router = createRouter({
history: createWebHashHistory(),
routes
})

export default router
  • app.js中使用路由
1
2
3
4
5
6
7
8
9
<template>
<router-view/> //自动根据路由地址显示相应的组件
</template>

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

2.2 js控制跳转

跳转到指定页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="wrapper__login-button" @click="handleLogin">登录</div>
</template>

<script>
import { useRouter } from 'vue-router' //引入
export default {
name: 'Login',
setup () {
const router = useRouter() //实例化
const handleLogin = () => {
router.push({ name: 'Home' }) //设置要跳转的页面, Home是路由名
//router.push('/home')
}
return { handleLogin }
}
}
</script>

返回上一个页面

  • router.back()
1
2
3
4
5
6
7
8
import { useRouter } from 'vue-router'
const useBackEffect = () => {
const router = useRouter()
const handleBack = () => {
router.back()
}
return { handleBack }
}

2.3 标签控制跳转

  • <to="/shop"> </router-link>
  • 去掉a标签的下划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 路由跳转不带参数的情况
<template>
<div class="nearby">
<router-link to="/shop"> // <router-link :to="{name: 'shop'}"> || <router-link :to="{path: '/shop'}">
<ShopInfo />
</router-link>
</div>
</template>

<style lang="scss" scoped>
.nearby{
a{
text-decoration: none;
}
}
</style>

2.4 路由参数

2.4.1 传递参数

配置

1
2
3
4
5
6
7
const routes = [
{
path: '/shop/:id', //参数名为:id
name: 'Shop',
component: () => import(/* webpackChunkName: "shop" */ '../views/shop/Shop')
}
]

js跳转带参

  • 使用router.push方法,并用${}添加参数
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="nearby">
<ShopInfo v-for="item in nearbyList" :key="item._id" :item="item" @click="handleToShop(item._id)"/>
</div>
</template>

<script>
import { useRouter } from 'vue-router'

// 跳转到shop页面
const useToShopEffect = () => {
const router = useRouter()
const handleToShop = (id) => {
router.push(`/shop/${id}`)
}
return { handleToShop }
}

export default {
name: 'Nearby',
components: { ShopInfo },
setup () {
return { nearbyList, handleToShop }
}
}
</script>

标签跳转带参

  • 通过:to=" "指定路径,并用${}添加参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 路由跳转不带参数的情况
<template>
<div class="nearby">
<router-link v-for="item in nearbyList" :key="item._id" :to="`/shop/${item._id}`">
<ShopInfo :item="item" />
</router-link>
</div>
</template>

<style lang="scss" scoped>
.nearby{
a{
text-decoration: none;
}
}
</style>

2.4.2 获取参数

  • 通过useRoute获取当前路由相关的信息,例如:paramspathquery
    • 通过route.params获取路由带的参数
1
2
3
4
5
6
7
import { useRoute } from 'vue-router'

// 获取当前商铺数据
const useShopInfoEffect = () => {
const route = useRoute()
console.log(route.params.id)
}

3 路由守卫

全局守卫

  • 任何路由每次跳转之前调用
  • 使用场景:只有登录后才能正常跳转,如果没有登录,则会默认跳转到登录页面
  • /router/index.js中使用router.beforeEach设置全局路由守卫
    • to : 路由跳转的目标路由
    • from : 路由跳转的原始路由
    • next : 调用则表示允许跳转
      • 接收跳转到的路由的组件名,则干预跳转的页面
      • next(), 不传参数则表示不做干预,保持原来的跳转
1
2
3
4
5
6
7
router.beforeEach((to, from ,next) => {
const { isLogin } = localStorage; //用本地存储记录是否登录了
const { name } = to;
const isLoginOrRegister = (name === "Login" || name === "Register");
//如果登录了,或者路由是Login或者Register则跳转到相应页面不做干预,否则跳转到Login页面
(isLogin || isLoginOrRegister) ? next() : next({ name: 'Login'}); //接受将要跳转到的路由页面的组件名:Login
})

单独守卫

  • 跳转到当前路由之前调用
  • 使用场景: 已经登录了,地址为登录页时,不应该显示登录页,而是自动跳转到首页
  • /router/index.js中的routes模块中使用router.beforeEach设置单独路由守卫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter (to, from, next) { //当路由跳转到login页面时调用
const isLogin = localStorage.isLogin //是否登录
if (isLogin) {
next({ name: 'Home' }) //如果登录了则跳转到Home界面
} else {
next() //如果没有登录则正常跳转,即跳转到Login页面
}

//上面的部分可以简化为下面的两行代码
//const { isLogin } = localStorage
//isLogin ? next({ name: 'Home' }) : next()
}
}
]

元信息

官网

使用场景仍然是权限控制

  • 其实元信息可以理解为标识需要同权限(要求)的路由,便于路由判断
  • 注意:’./router/index.ts’中使用store直接导入
1
2
3
4
5
//正确方式
import store from '../store'
//错误方式 g
import { useStore } from 'Vuex'
const store = useStore()
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
import { createRouter, createWebHashHistory } from 'vue-router'
import store from '../store'
const routes = [
{
path: '/',
name: 'Home', // 登录与否都可以访问
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "home" */ '../views/Login.vue'),
meta: { redirectAlreadyLogin: true } // 元信息 必须非登录才可以访问 若已经登录则跳转到首页
},
{
path: '/column/:id',
name: 'ColumnDetail', // 登录与否都可以访问
component: () => import(/* webpackChunkName: "home" */ '../views/ColumnDetail.vue')
},
{
path: '/create',
name: 'CreatePost',
component: () => import(/* webpackChunkName: "home" */ '../views/CreatePost.vue'),
meta: { requiredLogin: true } // 元信息 必须登录才可以访问 若未登录则跳转到登录页
}
]

const router = createRouter({
history: createWebHashHistory(),
routes
})

router.beforeEach((to, from, next) => {
const { isLogin } = store.state.user
console.log(isLogin)
if (to.meta.requiredLogin && !isLogin) { // 有需要登录的元信息且当前未登录 则跳转到登录页
next({ name: 'Login' })
} else if (to.meta.redirectAlreadyLogin && isLogin) { // 有非登录的元信息且当前已经登录 则直接跳转到首页
next('/')
} else {
next()
}
})

export default router

4 动态路由

又叫异步路由

在需要展示页面的时候才加载相关资源

  • /* webpackChunkName: "about" */: about 用于调试显示
1
2
3
4
5
6
7
8
9
10
11
12
13
import Home from '../views/home/Home'
const routes = [
{
path: '/',
name: 'Home',
component: Home //普通路由
},
{
path: '/shop',
name: 'Shop',
component: () => import(/* webpackChunkName: "shop" */ '../views/shop/Shop') //动态路由
}
]

六、VueX

Vuex官方文档

1安装

终端命令安装

1
npm install vuex --save

脚手架创建项目时就安装VueX

  1. vue create 项目名称
  2. 选择Manually select features自定义安装的插件
  3. 空格选择Vuex
image-20210815163117742

2 使用

2.1 基本概念

main.js中注册使用

1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App).use(store).mount('#app')

store/index.js基础代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createStore } from 'vuex'

export default createStore({
state: {
},
mutations: {
},
getters: {
},
actions: {
},
modules: {
}
})

2.2 数据支持ts

  • createStore支持泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createStore } from 'vuex'
import { testData, testPosts, ColumnProps, PostProps } from '../testData'
export interface UserProps {
isLogin: boolean;
name?: string;
id?: string;
}
export interface GlobalDataProps { // 泛型
columns: ColumnProps[];
posts: PostProps[];
user: UserProps;
}
export default createStore<GlobalDataProps>({
state: {
columns: testData,
posts: testPosts,
user: { isLogin: false }
}
})

2.3 使用store数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from 'vuex'
import { GlobalDataProps } from '../store' // GlobalDataProps数据类型

export default defineComponent({
name: 'Home',
setup () {
const store = useStore<GlobalDataProps>() //支持泛型
const list = computed(() => store.state.columns) // 用计算属性获取,才能实时获取变化的store数据
return { list }
}
})
</script>

2.4 mutations

需要修改store中数据时 则需要在mutations中写方法进行修改,便于记录对store的每次修改

Mutation 必须是同步函数

定义修改store中数据的方法

  • 第一个参数为state, 即store中的state
  • 第二个参数开始自定义,也可以没有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createStore } from 'vuex'
export interface UserProps {
isLogin: boolean;
name?: string;
id?: string;
}
export interface GlobalDataProps {
user: UserProps;
}
export default createStore<GlobalDataProps>({
state: {
user: { isLogin: false }
},
mutations: {
login (state, user) {
state.user.isLogin = true
state.user.name = user.name
state.user.id = user.id
}
}
})

调用修改store数据的方法

  • 使用store.commit调用mutations中的方法
  • 第一个参数为要调用的mutations中的方法名
  • 第二个参数为自定义的参数值
1
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
export default defineComponent({
name: 'Login'
setup () {
const store = useStore()
store.commit('login', { name: 'chuckie', id: 1 })
}
}
})
</script>

2.5 getters

官网教程

Getter使用场景:获取store中数据,且需要对store中的数据过滤时使用

类似computed,计算结果会缓存,只有当依赖值改变才会重新计算

定义

  • 获取store数据不传参情况: 直接返回计算结果
  • 获取store数据传参情况: 返回一个函数,函数接受参数并计算结果返回
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 { createStore } from 'vuex'
import { testData, testPosts, ColumnProps, PostProps } from '../testData'
export interface UserProps {
isLogin: boolean;
name?: string;
id?: string;
}
export interface GlobalDataProps {
columns: ColumnProps[];
posts: PostProps[];
user: UserProps;
}
export default createStore<GlobalDataProps>({
state: {
columns: testData,
posts: testPosts,
user: { isLogin: false }
}
getters: {
getColumnsLen (state) { //获取store数据不传参情况
return state.columns.length
},
getColumnById: (state) => (id: number) => { //获取store数据传参情况
return state.columns.find(c => c.id === id)
}
}
})

使用

  • 通过store.getters.方法调用getters方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
import { useStore } from 'vuex'
export default defineComponent({
name: 'ColumnDetail',
setup () {
const store = useStore()
const currentId = ref(0)
let columnLen = ref(0)
columnLen = store.getters.getColumnsLen //获取不传参属性
const column = computed(() => store.getters.getColumnById(currentId)) //获取传参属性
return {
columnLen,
column
}
}
})
</script>

2.6 actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态
  • Action 可以包含任意异步操作
单个action

定义

  • actions中定义异步请求函数,函数中通过axios发送异步请求
  • 异步请求成功后,在回调里调用mutations中的方法修改store中的数据
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
import { createStore } from 'vuex'
import axios from 'axios'

export interface ImageProps {
_id?: string;
url?: string;
createdAt?: string;
fitUrl?: string;
}
export interface ColumnProps{
_id: string,
title: string,
avatar?: ImageProps,
description: string
}
export interface PostProps {
id: number;
title: string;
content: string;
image?: string;
createdAt: string;
columnId: number;
}
export interface GlobalDataProps {
columns: ColumnProps[];
posts: PostProps[];
}
export default createStore<GlobalDataProps>({
state: {
columns: [],
posts: []
},
mutations: {
fetchColums (state, rawData) { // 服务于actions修改store数据
state.columns = rawData.data.list
}
},
actions: {
fetchColums (context) {
axios.get('/columns').then(res => { // 异步请求
context.commit('fetchColums', res.data)// 调用mutations 修改store数据
})
}
}
})

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="ts">
import { computed, defineComponent, onMounted } from 'vue'
import { useStore } from 'vuex'
import { GlobalDataProps } from '../store'

export default defineComponent({
name: 'Home',
setup () {
const store = useStore<GlobalDataProps>()
onMounted(() => {
store.dispatch('fetchColums') // 调用actions中方法发送异步请求
})
const list = computed(() => store.state.columns)
return { list }
}
})
</script>
组合action

定义

案例说明:

  • 先登录,再获取用户信息

  • 将这两个操作可以合并到一个action中

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
import { createStore } from 'vuex'
import { instance, get, post } from '../utils/resques'

export interface UserProps {
isLogin: boolean;
_id?: string;
email?: string;
nickName: string;
column?: number;
}
export interface GlobalDataProps {
token: string;
user: UserProps;
}
export default createStore<GlobalDataProps>({
state: {
token: '',
user: { isLogin: false, column: 1, nickName: 'chuckie!!!' }
},
mutations: {
login (state, rawData) {
console.log('login', rawData)
const { token } = rawData.data
state.token = token
console.log('login', token)
instance.defaults.headers.common.Authorization = token
},
fetchCurrentUser (state, rawData) {
console.log('fetchCurrentUser', rawData)
state.user = { isLogin: true, ...rawData.data }
}
},
actions: {
// 登录action
async login (context, payload) {
const data = await post('user/login', payload)
context.commit('login', data)
return data
},
// 获取用户信息action
async fetchCurrentUser (context) {
const data = await get('user/current')
context.commit('fetchCurrentUser', data)
return data
},
// 将以上两个action合并到组合action中提供给用户 用户调用loginAndFetch就会先调用login再调用fetchCurrentUser
loginAndFetch ({ dispatch }, loginData) {
return dispatch('login', loginData).then((res) => {
return dispatch('fetchCurrentUser') // 只返回了fetchCurrentUser的结果, login的结果在res中没有处理
})
}
}
})

使用

1
2
3
4
// payload是传递给login的参数 即上面接受的loginData
store.dispatch('loginAndFetch', payload).then((res) => {
console.log(res) // 打印的是fetchCurrentUser请求的结果
})

3 数据持久化

将VueX的数据通过本地缓存持久化

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
import { createStore } from 'vuex'

// 将VueX数据存储到缓存中 每次修改VueX中数据后调用该方法
const setLocalCartList = (state) => {
const { cartList } = state
const cartListString = JSON.stringify(cartList) // 将对象转换为字符串, 因为本地缓存不能存储对象
localStorage.cartList = cartListString
}

// 从本地缓存拿取数据初始化VueX
const getLocalCartList = () => {
try {
return JSON.parse(localStorage.cartList)
} catch (e) {
return {}
}
}

export default createStore({
state: {
cartList: getLocalCartList() // 从本地缓存中拿取数据
// cartList: {
// shopId:{
// shopName: '沃尔玛',
// productList:{
// productId:{
// _id: '1',
// name:'番茄250g/份',
// imgUrl: 'http: / / www.dell-lee.com/ imgs/vue3/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 2, //加入购物车商品数量
// check: false, // 购物车选中状态
// }
// }
// }
// }
},
mutations: {
// 初始化商品名字
changeShopName (state, payload) {
const { shopId, shopName } = payload
const shopInfo = state.cartList[shopId] || {
shopName: '',
productList: {}
}
shopInfo.shopName = shopName
state.cartList[shopId] = shopInfo
setLocalCartList(state) // 数据修改后要更新本地缓存
}
},
actions: {
},
modules: {
}
})

七、插件

normalize.css

移动端不同浏览器的标签样式不一样

normalize.css用于使得不同浏览器上标签样式一直

类似css适配中用到的reset css

  • 安装
1
npm install normalize.css --save
  • 使用: 在main.js中引入即可
1
import 'normalize.css'

八、开发常用操作

*的方式写多个一样的标签

1
span.item*4

vue中全局引入样式

  • 在根目录下新建style文件,文件夹下可以新建css、lcss、scss的文件
  • 再在style下新建index.css将其它样式文件都引入到这个文件中,
  • 再在main.js中引入index.css即可, 使得在main.js中只会引入一次,不用每个都引入,因为所有要引入的样式文件都在index.css中引入了
1
import './style/index.css'

vue中使用scss

scss官网

  • 在创建项目时,选择安装css预处理: CSS Pre-processors
  • 在引入文件时同css只是文件名后缀为.scss
  • style部分添加lang="scss"
1
2
<style lang="scss">
</style>

vue中scoped

  • scoped 表示当前样式只对当前组件有效
1
2
<style scoped>
</style>

vue中引入本地资源

  • 通过 require(‘@/assets/….’)
1
column.avatar = require('@/assets/column.jpg') //引入根目录下/assets/column.jpg

vue中的<pre>

  • <pre>展示vue对象内容到页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<pre>{{route}}</pre>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'ColumnDetail',
setup () {
const route = useRoute()
return {
route
}
}
})
</script>
<style scoped>

</style>

image-20220606213853218

vue中开发环境获取

在main.ts中通过以下代码可获取当前是开发版本还是上线版本

1
process.env.NODE_ENV === 'development' 表示是开发b

vue中组件的函数化调用

  • 1 位置是 main.ts中挂载的app实例
  • 2 位置是 组件函数化调用中创建的节点和挂载的vue实例(用组件创建)
  • 3 位置是2中的组件的传送门创建的node节点

image-20220629211158281

  • createApp创建vue实例
    • 第一个参数是组件
    • 第二个参数是传入组件的参数
  • mount(): 挂载实例到node节点上 参数为node节点
  • unmount():卸载实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'
import Message from './Message.vue'
export type MessageType = 'success' | 'error' | 'default'

const createMessage = (message: string, type: MessageType, timeout = 2000) => {
// createApp创建vue实例,实例上挂载Message组件,message,type是传入组件的属性
const messageInstance = createApp(Message, {
message,
type
})
const mountNode = document.createElement('div') // 创建node节点
document.body.appendChild(mountNode) // 将node节点添加到body中
messageInstance.mount(mountNode) // 将创建的实例挂载到node节点上
setTimeout(() => {
messageInstance.unmount() // 卸载vue实例
document.body.removeChild(mountNode) // 移除node节点
}, timeout)
}

export default createMessage

vue中支持markdown渲染

markdown-it

安装markdown-it

1
2
3
4
// js环境
npm install markdown-it --save
// typescript环境
npm i --save-dev @types/markdown-it

使用

1
2
3
4
5
6
7
8
9
10
<template>
<div v-html="currentHTML"></div> // v-htm语法直接渲染转换好的html
</template>

<script lang="ts">
import MarkdownIt from 'markdown-it' // 引入
const md = new MarkdownIt() // 实例化
const contetntest = '### 123, **hhhh**, kkkk, ```jdfjadjlf```' // 要转换的markdown文本
const content = md.render(contetntest) // render()方法将markdown文本转换成html文本
</script>