Vue学习笔记(Vue2)

目录

一、vue核心

1、vue简介

1.1、vue是什么

一套用于构建用户界面的渐进式JavaScript框架。

渐进式:vue可以自底向上逐层应用;简单应用:只需一个轻量小巧的核心库;复杂应用:可以引入各式各样的的vue插件。

1.2、谁开发的

华人 尤雨溪;
vue2(攻壳机动队) vue3(海贼王)
在这里插入图片描述

1.3、vue的特点
  • 采用组件化模式,提高代码复用率、且让代码更好的维护
    在这里插入图片描述
  • 声明式编码(原生js为命令式编码),使无需直接操作DOM,提高开发效率
  • 使用虚拟DOM+优秀的Diff算法,尽量复用DOM节点
    在这里插入图片描述
1.4、vue官网

vue官网:.https://cn.vuejs.org/v2/guide/
在这里插入图片描述

1.5、搭建vue开发环境
  • 引入vue.js文件
  • 安装浏览器vue插件:vue.js devyools
  • Vue.config.productionTip = false;//设置为 false 以阻止 vue 在启动时生成生产提示。

2、初识Vue:

2.1、第一个Vue程序:
<!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>Document</title>
    <script type="text/javascript" src="vue.js"></script>
</head>

<body>
    <!-- 准备一个容器 -->
    <!-- 插值 -->
    <div id="root">hello {
   
   {name}}</div>
    <script>
        Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。

        // 创建Vue实例
        const x = new Vue({
      
      
            el: '#root', //el:挂载点,相当于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
            data: {
      
       //data用于存储数据,数据供el所指定的容器去使用。值我们暂时先写成一个对象
                name: 'vue',
                age: 8
            }
        });
    </script>
</body>

</html>
2.2、初识Vue总结:
  • 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
  • root容器的代码依然符合html规范,只不过混入了一些特殊的Vue语法
  • root容器里的代码被称为Vue模板
  • 容器和vue实例必须一一对应,不能一对多,也不能多对一
  • 真实开发中只有一个Vue实例,并且会配合组件一起使用
  • { {xxx}}中的xxx要写js表达式,且xxx可以自动读取到data中的所有数据
  • 一旦data中的数据发生改变,那么模板中用到的地方也会自动更新

3、模板语法

3.1、插值语法

写法:{ {js表达式}}
功能:用于解析标签体内容

<div id="root">hello {
   
   {name}}</div>
new Vue({
    
    
            el: '#root',
            data: {
    
    
                name: 'vue'
            }
        });
3.2、指令语法

写法:v-bind:href="xxx"或简写为 :href=“xxx”
功能:用于解析标签(包括标签属性、标签体内容、绑定事件…)

<div id="root">
        <a v-bind:href="url">百度一下</a>
    </div>
new Vue({
    
    
            el: '#root',
            data: {
    
     
                url: 'http://www.baidu.com'
            }
        });

4、数据绑定

4.1、单向数据绑定

单向数据绑定(v-bind,简写 :):数据只能从data流向页面

<div id="root">
        单向数据绑定: <input type="text" v-bind:value="name">
    </div>
  Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: 'zhangsan'
            }
        })
4.2、双向数据绑定

双向数据绑定(v-model):数据不仅可以从data流向页面,还可以从页面流向data

双向绑定一般都应用在表单类元素上(如input、select等)

v-model:value 可以简写为 v-model,因为v-model默认手机就是value值

<div id="root">
         双向数据绑定: <input type="text" v-model:value="name">
    </div>
  Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: 'zhangsan'
            }
        })

穿插:el和data的两种写法

1、el的两种写法

(1)new Vue的时候配置el属性
(2)先创建Vue实例,随后再通过vm.$mount(’#root’)指定el值

2、data的两种写法

(1)对象式

data:{
    
    }

(2)函数式

data:function(){
    
    
	return {
    
    
		
	}
}

以后学组件的时候,data必须使用函数式,否则会报错
一个重要原则:由Vue管理的函数,一定不要写箭头函数,写了箭头函数,this指向就不再是Vue实例了

5、MVVM模型

5.1、MVVM模型

M:模型(Model),对应data中的数据
V:视图(View):对应模板
VM:视图模型(ViewModel):对应Vue实例对象
在这里插入图片描述
data(M)中多有属性最后都出现在了vm身上
vm身上所有属性及Vue原型上所有属性,在vue模板(V)中都可以直接使用

5.2、Object.defineProperty方法

功能:给对象添加属性
语法:

Object.defineProperty(对象名,新属性名,{
    
    value:新属性值[,其他配置]})

其他配置项例如:

let person = {
    
    
            name: 'zhangsan',
            sex: 'nan'
        }
        Object.defineProperty(person, 'age', {
    
    
             value: 18,
             enumerable: true, //控制属性是否可以枚举,默认值是false
             writable: true, //控制属性是否可以被修改,默认值是false
             configurable: true //控制属性是否可以被删除,默认值是false
        })
        console.log(person);

借助Object.defineProperty方法让对象和数据产生关联:

let number = 14;
        let person = {
    
    
            name: 'zhangsan',
            sex: 'nan'
        }
        Object.defineProperty(person, 'age', {
    
    
            //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
            get() {
    
    
                console.log('有人读取age属性了');
                return number;
            },

            //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
            set(value) {
    
    
                console.log('有人修改了age属性,且值为' + value);
                number = value;
            }
        })
        console.log(person);
5.3、理解数据代理

什么是数据代理:通过一个对象代理对另一个对象中属性的操作(读写)
例如一个简单的数据代理:

 let obj1 = {
    
    
            x: 100
        }
        let obj2 = {
    
    
            y: 200
        }
        Object.defineProperty(obj2, 'x', {
    
    
            get() {
    
    
                return obj1.x;
            },
            set(value) {
    
    
                obj1.x = value
            }
        })

这样obj2就可以读写obj1的x属性,这就是简单的数据代理
在这里插入图片描述

5.4、vue中的数据代理

在vue中,通过vm对象来代理data对象中的属性的操作(读写)
模板中直接写属性名就可以读写操作,这就用到了数据代理,如下有:

 <div id="root">
        <h2>学校:{
   
   {name}}</h2>
        <h2>地址:{
   
   {address}}</h2>
    </div>
Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: 'beida',
                address: 'beijing'
            }
        })

模板中用的是name,而不是_data.name(即data.name),这里就用到了数据代理,vue对象代理了_data对象;通过vm.name就可以读写_data中的name。
在这里插入图片描述
这里vm.name和vm._data.name的值相同,修改vm.name的值,vm._data.name也会改变。
在这里插入图片描述
基本原理:
就是上一小节讲到的数据代理,通过Object.defineProperty()方法把data对象中所有属性添加到vm,为每一个添加到vm上的属性,都指定一个getter/setter,getter/setter内部去操作(读写)data中对应的数据

vue中数据代理的好处:
更加方便的操作data中的数据

6、事件处理

6.1、事件处理的基本使用
  • 使用v-on:xxx@xxx绑定事件,其中xxx是事件名
  • 事件的回调需要配置在methods对象中,最终会在vm上;
  • methods中配置的函数不要用箭头函数,否则this指向就不是vm了
  • methods中配置的函数,都被Vue所管理的函数,this的指向是vm 或组件实例对象
  • @click="demo"@click="demo($event)"效果一样,但后者可以传参
 <div id="root">
        <h3>欢迎来到{
   
   {name}}学习</h3>
        <br>
        <button v-on:click="showInfo1">点我提示信息1</button>
        <button v-on:click="showInfo2(666,$event)">点我提示信息2</button>
    </div>
 Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: 'atshanggui'
            },
            methods: {
    
    
                showInfo1(event) {
    
    
                    console.log(event.target);
                    console.log(this);
                    alert('同学你好')
                },
                showInfo2(a, event) {
    
    
                    console.log(event.target);
                    console.log(this);
                    alert(a + '同学你好')
                }
            }
        })
6.2、事件修饰符

之前可以通过e.preventDefault();阻止标签的默认行为,如下:

	<div id="root">
        <a href="http://www.baidu.com" @click="showInfo">百度一下</a>
    </div>
Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
            },
            methods: {
    
    
                showInfo(e) {
    
    
                    e.preventDefault(); //阻止事件默认行为,这里阻止a标签的默认跳转
                    alert("百度一下你就知道")
                }
            }
        })

在vue中可以通过事件修饰符来阻止默认行为:

	<div id="root">
        <a href="http://www.baidu.com" @click.prevent="showInfo">百度一下</a>
    </div>

js代码部分的e.preventDefault()就可以去掉,也可以阻止默认行为。这就是事件修饰符的作用之一。


Vue中的事件修饰符有六个,前三个比较常用:

  • (1)prevent:阻止默认事件
    上面已讲
  • (2)stop:阻止事件冒泡

事件冒泡:

<style>
        .demo1 {
      
      
            height: 100px;
            background-color: pink;
        }
    </style>
 	<div id="root">
        <div class="demo1" @click="showInfo1">
            <button @click="showInfo1">点我提示信息</button>
        </div>
    </div>
 Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
            },
            methods: {
    
    
                showInfo1(e) {
    
    
                    alert("你好同学")
                }
            }
        })

这样点击button就会弹框两次,这就是事件冒泡,可以通过e.stopPropagation()阻止,也可以用事件修饰符stop(加在内嵌容器上)

 	<div id="root">
        <div class="demo1" @click="showInfo1">
            <button @click.stop="showInfo1">点我提示信息</button>
        </div>

    </div>
  • (3)once:事件只触发一次

事件只触发一次:

 <div id="root">
        <button @click.once="showInfo1">点我提示信息</button>
    </div>
  • (4)capture:使用事件的捕获模式

使用事件的捕获模式:
事件先捕获后冒泡,页面一般在冒泡阶段处理事件,比如下面这段代码点击div2就是先打印2,再打印1:

<style>
 .box1 {
      
      
            padding: 5px;
            background-color: pink;
        }
        .box2 {
      
      
            padding: 5px;
            height: 50px;
            background-color: red;
        }
</style>
	<div id="root">
 		<div class="box1" @click="show(1)">
            div1
            <div class="box2" @click="show(2)">div2</div>
        </div>
    </div>
 const vm = new Vue({
    
    
            el: "#root",
            methods: {
    
    
                show(x) {
    
    
                    console.log(x);
                }
            }
        })

在这里插入图片描述
在这里插入图片描述
这时就可以用capture使用事件的捕获模式,先打印1,再打印2(加在外层容器上):

	<div id="root">
 		<div class="box1" @click.capture="show(1)">
            div1
            <div class="box2" @click="show(2)">div2</div>
        </div>
    </div>

在这里插入图片描述

  • (5)self:只有event.target是当前操作的元素时才触发事件
  • (6)passive:事件的默认行为立即执行,无需等待事件回调执行完毕

(5)和(6)用的少,就不细说了
再就是事件修饰符可以连续写,例如:

@click.stop.prevent="xxx"

就是先阻止冒泡事件,再阻止默认行为。

6.3、键盘事件

(1)常用的两个键盘触发事件:

  • keydown:按下按键不用抬起来就触发事件
  • keyup:按下按键抬起来才触发事件(常用)

(2)Vue中按键别名

 <div id="root">
        <input type="text" placeholder="按下回车提示输入" @keyup.enter="showInfo">
    </div>
    <script>
        Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
      
      
            el: "#root",
            methods: {
      
      
                showInfo(e) {
      
      
                    console.log(e.target.value);
                }
            }
        })
    </script>

上面代码中通过Enter键按下后抬起触发事件,enter为vue中Enter的别名

常用的按键别名有:

  • 回车=>enter
  • 删除=>delete(捕获“删除”和“退格”键)
  • 退出=>esc
  • 空格=>space
  • 换行=>tab(特殊,必须配合keydown使用)
  • 上=>up
  • 下=>down
  • 左=>left
  • 右=>right

Vue未提供别名的按键,可以使用按键原始的key值去绑定,但要注意驼峰命名法的原始key值要转化为短横线命名,例如大小写转换键KebabCase就要写成kebab-case。

系统修饰键(用法特殊):ctrl、alt、shift、meta,用法如下:

  • 配合keyup使用:按下修饰键的同时再按下其他键,随着释放其他键,事件才被触发
  • 配合kedown使用:正常触发事件
  • 如果只想要修饰键+一个其他键 才能触发事件可以连续写,例如下面这行代码就是只有按下ctrl+y才能触发事件:
@keyup.ctrl.y="xxx"

Vue.config.keyCodes.自定义键名 =键码,可以自定义按键别名。

在事件触发函数中获取键名和键码:

showInfo(e) {
    
    
    console.log(e.key, e.keyCode);
}

7、计算属性与监视

7.1、计算属性

(1)定义:要用的属性不存在,要通过已有属性计算出来
(2)原理:底层借助了Object.defineproperty方法提供的getterr和setter
(3)get函数什么时候执行?
     ①初次读取时会执行一次
     ②当依赖的数据发生改变时会被再次调用
(4)优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试更方便
(5)备注:
     ①计算属性最终会出现在vm上,直接调用即可
     ②如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生变化

例如:

 <div id="root">
        姓:<input type="text" v-model="firstName"><br><br>
        名:<input type="text" v-model="lastName"><br><br>
        全名:{
   
   {fullName}}
    </div>
 Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                firstName: '张',
                lastName: '三'
            },
            //计算属性
            computed: {
    
    
                fullName: {
    
    
                    // get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值
                    get() {
    
    
                        return this.firstName + '-' + this.lastName;
                    },
                    //多数情况只读不改,所以只读情况set就不用写
                    set(value) {
    
    
                        const arr = value.split('-');
                        this.firstName = arr[0];
                        this.lastName = arr[1];
                    }
                }
            }
        })

多数情况只读不改,所以只读情况set就不用写,就可以直接写一个函数:

 fullName() {
    
    
     return this.firstName + '-' + this.lastName;
}

这个函数的功能就是get函数的功能,最后还是会把fullName配置到vm对象下,所以模板中写的是fullName,而不是fullName(),写fullName()就相当于调用get(),是错误的。

7.2、监视属性

(1)监视属性(watch)可以监视数据的变化,data和computed中的数据都可以被监视。
例如:

    <div id="root">
        <h2>今天天气很{
   
   {info}}</h2>
        <button @click="changeWeather">切换天气</button>
    </div>
Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                isHot: true
            },
            methods: {
    
    
                changeWeather() {
    
    
                    this.isHot = !this.isHot;
                }
            },
            computed: {
    
    
                info() {
    
    
                    return this.isHot ? '炎热' : '凉爽';
                }
            },
            watch: {
    
    
                isHot: {
    
    
                	deep:true, //开启深度监测
                    immediate: true, //初始化时让handler调用一下
                    handler(newValue, oldValue) {
    
    
                        console.log('isHot被修改了', newValue, oldValue);
                    }
                }
            }
        })

在watch中监视isHot,第一个属性immediate默认false,设置为true就会初始化时让handler调用一下;第二个handler方法,当数据isHot发生改变时自动调用,里面的参数第一个是改变后的值,第二个是改变前的值。

监视属性(watch)即有:

  • 当被监视的属性变化时,回调函数自动调用,进行相关的操作
  • 监视的属性必须存在,才能进行监视
  • 监视的两种写法:除了上面的在new Vue时写入watch配置这种方法外,还有 通过vm.$watch,例如下面代码
	   vm.$watch("isHot", {
    
    
            immediate: true, //初始化时让handler调用一下
            handler(newValue, oldValue) {
    
    
                console.log('isHot被修改了', newValue, oldValue);
            }
        });

(2)深度监视
Vue中的监视属性watch默认不监视对象内部值的改变(只监视一层),比如:

 const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                number: {
    
    
                    a: 1,
                    b: 2
                }
            }
        }),
        watch: {
    
    
                number: {
    
    
                    handler(newValue, oldValue) {
    
    
                        console.log('a被修改了', newValue, oldValue);
                    }
                }
            }

上面代码中,watch只能监视number的值的改变(该值是一个地址),里面的a或b发生改变了监视不到,watch改成:

 'number.a': {
    
    
                    handler(newValue, oldValue) {
    
    
                        console.log('a被修改了', newValue, oldValue);
                    }
}

就可以监视到里面的a的变化,但是如果要同时监视a和b,甚至有跟多的数时,这时就不能这么写了。

于是,配置deep:true可以检测对象内部值的变化(监视多层)

number: {
    
    
     deep: true,
     handler(newValue, oldValue) {
    
    
         console.log('bumber被修改了', newValue, oldValue);
     }
 }

注:

  • Vue自身可以监视对象内部值的改变,但Vue提供的watch默认不可以
  • 使用watch时根据数据的具体结构,决定是否采取深度监视

(3)监视属性的简写
前面学习计算属性computed的时候,只有get函数不写set函数时可以简写该属性;在这监视属性watch也一样,只写handler函数不写deep,immediate时也可以简写。
例如上面写过的如果只写hander就可以简写为:

	 watch: {
    
    
                // 简写:
                isHot(newValue, oldValue) {
    
    
                    console.log('isHot被修改了', newValue, oldValue);
                }
            }

当然还有通过vm.$watch来监视时也可以简写:

		vm.$watch("isHot", function(newValue, oldValue) {
    
    
            console.log('isHot被修改了', newValue, oldValue);
        });

同样为了不改变函数的this指向,也能写成箭头函数;即一般在Vue内管理的函数都不要写箭头函数。

7.3、计算属性和监视属性的区别

(1)computed能完成的功能,watch都可以完成
(2)watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作
(3)都能完成的尽量用computed,因为computed是声明式的,watch是命令式的

下面这个姓名案例两种属性都可以实现:

	<div id="root">
        姓:<input type="text" v-model="firstName"><br><br>
        <!--  -->
        名:<input type="text" v-model="lastName"><br><br>
        <!--  -->
        全名:{
   
   {fullName}}
    </div>

计算属性实现:

	Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                firstName: '张',
                lastName: '三'
            },
            computed: {
    
    
                fullName() {
    
    
                    return this.firstName + '-' + this.lastName;
                }
            }
        })

监视属性实现:

		const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                firstName: '张',
                lastName: '三',
                fullName: "张三"
            },
            watch: {
    
    
                firstName(newValue, oldValue) {
    
    
                    this.fullName = newValue + '-' + this.lastName;
                },
                lastName(newValue, oldValue) {
    
    
                    this.fullName = this.firstName + '-' + newValue;
                }
            }
        })

但是异步任务用计算属性就很难实现,用监视属性就很好实现,比如加一个计时器,让全名的姓延迟一秒更新:

		const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                firstName: '张',
                lastName: '三',
                fullName: "张三"
            },
            watch: {
    
    
                firstName(newValue, oldValue) {
    
    
                    setTimeout(() => {
    
    
                        this.fullName = newValue + '-' + this.lastName;
                    }, 1000)
                },
                lastName(newValue, oldValue) {
    
    
                    this.fullName = this.firstName + '-' + newValue;
                }
            }
        })

两个重要的小原则:

  • 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象。
  • 所有不被Vue所管理的函数(如定时器的回调函数,ajax的回调函数等,Promise的回调函数),最好写成箭头函数,这样this的指向才是vm 或组件实例对象。

目标都一个,就是让this指向是vm。

8、class与style绑定

8.1、理解

(1)在应用界面中,某个(些)元素的样式是变化的
(2)class/style绑定就是专门用来实现动态样式效果的技术

8.2、绑定class

(1)绑定class样式–字符串写法,适用于:样式的类名不确定,需要动态指定
(2)绑定class样式–数组写法,适用于:要绑定的样式个数不确定、名字也不确定
(3)绑定class样式–对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用
例如:

<style>
        .basic {
      
      
            width: 300px;
            height: 60px;
            margin: 10px;
            border: 1px solid black;
        }
        .happy {
      
      
            background-color: pink;
            border: 1px solid black;
        } 
        .sad {
      
      
            background-color: rgb(70, 67, 68);
            border: 1px solid black;
        }
        .wu {
      
      
            background-color: rgb(236, 3, 42);
            border: 1px solid black;
        }
        .at1 {
      
      
            background-color: green;
            border: 4px solid black;
        }
        .at2 {
      
      
            background-color: green;
            border: 4px dashed black;
        }
        .at3 {
      
      
            background-color: green;
            border: 4px solid blue;
        }
    </style>
    <div id="root">
        <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
        <div class="basic" :class="classStr">{
   
   {name}}</div>
        <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
        <div class="basic" :class="classArr">{
   
   {name}}</div>
        <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
        <div class="basic" :class="classObj">{
   
   {name}}</div>
    </div>
Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: "test",
                classStr: "happy",
                classArr: ['at1', 'at2', 'at3'],
                classObj: {
    
    
                    at1: true,
                    at2: false
                }
            }
        })
8.3、style绑定

(1)绑定style样式–对象写法 ,属性名改为小驼峰命名
(2)绑定style样式–数组写法(不常用),数组其中是对象
例如:

	<style>
        .basic {
      
      
            width: 300px;
            height: 60px;
            margin: 10px;
            border: 1px solid black;
        }
    </style>
    <div id="root">
        <!-- 绑定style样式--对象写法 -->
        <div class="basic" :style="styleObj">{
   
   {name}}</div>
        <!-- 绑定style样式--数组写法(不常用) -->
        <div class="basic" :style="styleArr">{
   
   {name}}</div>
    </div>
	const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: "test",
                styleObj: {
    
    
                    fontSize: '40px',
                    color: "red",
                    backgroudColor: "yellow"
                },
                styleArr: [{
    
    
                        fontSize: '40px',
                        color: "red"
                    }, {
    
    
                        backgroudColor: "yellow"
                    }

                ]
            }
        })

9、条件渲染

(1)v-if

<h3 v-if="false">你好,{
   
   {name}}</h3>

写法:

  • v-if=“表达式”
  • v-else-if=“表达式”
  • v-else
    适用于:切换频率较低的场景
    特点:不展示的DOM元素直接被删除
    注意:v-if可以和v-else-if、v-else一起使用,但要求结构不能被打断

< template>配合v-if使用:

		<template v-if="true">
            <h3>你好,{
   
   {name}}</h3>
            <h3>你好,{
   
   {name}}</h3>
            <h3>你好,{
   
   {name}}</h3>
        </template>

在这里插入图片描述
< template>不会渲染到DOM结构中,且只有和v-if配合使用才可以起到整套删除整套显示的效果。

(2)v-show

<h3 v-show="false">你好,{
   
   {name}}</h3>

写法:v-show=“表达式”
适用于:切换频率较高的场景
特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉了

10、列表渲染

10.1、基本列表

v-for指令:

  • 用于展示列表数据
  • 语法:
v-for="(item,index) in xxx" :key="xxx"

key的原理和用法下面会说。

  • 可遍历数组、对象、字符串(用得少)、指定次数(用得少)

例如遍历数组:

	<div id="root">
        <ul>
            <li v-for="(p,index) in person" :key="">{
   
   {p.name}}-{
   
   {p.age}}</li>
        </ul>
    </div>
const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                person: [{
    
    
                    id: 001,
                    name: "张三",
                    age: 18
                }, {
    
    
                    id: 002,
                    name: "李四",
                    age: 28
                }, {
    
    
                    id: 003,
                    name: "王五",
                    age: 13
                }, ]
            }
        })

在这里插入图片描述

p就是数组值,index就是索引

如果遍历的是对象,前者就是值,后者就是键

10.2、key的原理

我们在上面遍历数组的代码基础上,增加一个需求:点击一个按钮在张三的上面增加一行老刘的信息,并在每一行后面增加一个input框,用这个需求来分析key的原理:

	<div id="root">
        <button @click.once="add">添加</button>
        <ul>
            <li v-for="(p,index) in person" :key="index">{
   
   {p.name}}-{
   
   {p.age}}
                <input type="text">
            </li>
        </ul>
    </div>
 const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                person: [{
    
    
                    id: 001,
                    name: "张三",
                    age: 18
                }, {
    
    
                    id: 002,
                    name: "李四",
                    age: 28
                }, {
    
    
                    id: 003,
                    name: "王五",
                    age: 13
                }, ]
            },
            methods: {
    
    
                add() {
    
    
                    const p = {
    
    
                        id: '004',
                        name: '老刘',
                        age: 40
                    }
                    //向数组前加元素
                    this.person.unshift(p)
                }
            }
        })

上面代码在没点击按钮前并在input中输入一些信息后为:
在这里插入图片描述
此时点击按钮后:
在这里插入图片描述
就出现了一个很严重的问题,input框信息错位

这里就是把key用index作为值出现的问题,另外还有一个隐蔽的问题就是效率太低。

出现这两个问题的原因就要分析key的原理和Vue的虚拟DOM的diff对比算法
下面几点都要重点了解,面试常问:

①虚拟DOM中key的作用:

  • key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】
  • 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则 见下面②。

②对比规则:

  • 若 旧虚拟DOM中找到了与新虚拟DOM相同的key:若虚拟DOM中的内容没有变化,直接使用之前的真实DOM;若内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
  • 若 旧虚拟DOM中未找到与新虚拟DOM相同的key:创建新的真实DOM,随后渲染到页面

③用index作为key可能会出现的问题:

  • 若对数据进行 逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新==》界面效果没有问题,但效率低
  • 如果结构中还包含输入类的DOM(如上面的input):还会产生错误的DOM更新==》界面有问题

③开发中如何选择key?

  • 最好使用每条数据的唯一标识作为key值,比如id、手机号、身份证号、学号等唯一值
  • 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key值是没有问题的
10.3、列表过滤

下面用两个例子(分别是用监视和计算属性)来实现列表过滤:

例子一(监视):

    <div id="root">
        <input type="text" placeholder="请输入名字" v-model="keyword">
        <ul>
            <li v-for="p in person1" :key="p.id">{
   
   {p.name}}-{
   
   {p.age}}-{
   
   {p.sex}}</li>
        </ul>
    </div>
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                keyword: "",
                person: [{
    
    
                    id: '001',
                    name: "马冬梅",
                    age: 18,
                    sex: '女'
                }, {
    
    
                    id: '002',
                    name: "周冬雨",
                    age: 28,
                    sex: '女'
                }, {
    
    
                    id: '003',
                    name: "周杰伦",
                    age: 13,
                    sex: '男'
                }, {
    
    
                    id: '004',
                    name: "温兆伦",
                    age: 45,
                    sex: '男'
                }, ],
                person1: []
            },
            watch: {
    
    
                keyword: {
    
    
                	//初始状态页面没有数据显示,即初始状态person1为空,此时让handler先执行一遍,让person1得到数据
                    immediate: true,
                    handler(val) {
    
    
						//filter过滤器 遍历旧数组中的元素后,返回过滤后的新数组
                        this.person1 = this.person.filter((p) => {
    
    
                        	//indexOf 前一个字符串中是否包含有括号里的字符串,不包含返回-1
                            return p.name.indexOf(val) !== -1
                        })
                    }
                }
            }
        })

例子二(计算属性):

    <div id="root">
        <input type="text" placeholder="请输入名字" v-model="keyword">
        <ul>
            <li v-for="p in person1" :key="p.id">{
   
   {p.name}}-{
   
   {p.age}}-{
   
   {p.sex}}</li>
        </ul>
    </div>
	 const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                keyword: "",
                person: [{
    
    
                    id: '001',
                    name: "马冬梅",
                    age: 18,
                    sex: '女'
                }, {
    
    
                    id: '002',
                    name: "周冬雨",
                    age: 28,
                    sex: '女'
                }, {
    
    
                    id: '003',
                    name: "周杰伦",
                    age: 13,
                    sex: '男'
                }, {
    
    
                    id: '004',
                    name: "温兆伦",
                    age: 45,
                    sex: '男'
                }, ]
            },
            computed: {
    
    
                person1() {
    
    
                    return this.person.filter((p) => {
    
    
                        return p.name.indexOf(this.keyword) !== -1
                    })
                }
            }

        })
10.4、列表排序

在上面用计算属性的列表过滤的基础上增加一个排序功能:

    <div id="root">
        <input type="text" placeholder="请输入名字" v-model="keyword">
        <button @click="sortType=2">年龄升序</button>
        <button @click="sortType=1">年龄降序</button>
        <button @click="sortType=0">原顺序</button>
        <ul>
            <li v-for="p in person1" :key="p.id">{
   
   {p.name}}-{
   
   {p.age}}-{
   
   {p.sex}}</li>
        </ul>
    </div>
 const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                keyword: "",
                sortType: 0, //0原顺序,1降序,2升序
                person: [{
    
    
                    id: '001',
                    name: "马冬梅",
                    age: 18,
                    sex: '女'
                }, {
    
    
                    id: '002',
                    name: "周冬雨",
                    age: 28,
                    sex: '女'
                }, {
    
    
                    id: '003',
                    name: "周杰伦",
                    age: 13,
                    sex: '男'
                }, {
    
    
                    id: '004',
                    name: "温兆伦",
                    age: 45,
                    sex: '男'
                }, ]
            },
            computed: {
    
    
                person1() {
    
    
                    const arr = this.person.filter((p) => {
    
    
                            return p.name.indexOf(this.keyword) !== -1
                        })
                        //判断是否排序
                    if (this.sortType) {
    
    
                        arr.sort((a, b) => {
    
    
                            return this.sortType === 1 ? b.age - a.age : a.age - b.age
                        })
                    }
                    return arr
                }
            }

        })

穿插:Vue监测数据的原理

1、更新时的一个问题
    <div id="root">
        <ul>
            <button @click="mdm">更新马冬梅的信息</button>
            <li v-for="p in person" :key="p.id">{
   
   {p.name}}-{
   
   {p.age}}</li>
        </ul>
    </div>
	const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                person: [{
    
    
                    id: '001',
                    name: "马冬梅",
                    age: 18,
                    sex: '女'
                }, {
    
    
                    id: '002',
                    name: "周冬雨",
                    age: 28,
                    sex: '女'
                }, {
    
    
                    id: '003',
                    name: "周杰伦",
                    age: 13,
                    sex: '男'
                }, {
    
    
                    id: '004',
                    name: "温兆伦",
                    age: 45,
                    sex: '男'
                }, ]
            },
            methods: {
    
    
                mdm() {
    
    
                    this.person[0].name = '马老师'
                    this.person[0].age = 50
                    this.person[0].sex = '男'
                }
            }
        })

上述代码实现更新马冬梅的信息:
在这里插入图片描述
点击按钮后成功更新:
在这里插入图片描述
但是将methods中的方法改为直接修改整个对象元素:

 		methods: {
    
    
                mdm() {
    
    
                        this.person[0] = {
    
    
                            id: '001',
                            name: "马老师",
                            age: 50,
                            sex: "男"
                        }
                }
            }

这样点击按钮,信息就没有更新,其实Vue无法监测到直接给数组元素赋的值,下面说说Vue监测数据的原理。

2、监测数据的原理

①Vue会监测data中所有层次的数据,就是即使对象中的对象的属性也能递归监测

②如何监测对象中的数据
通过setter实现监测,且要在new Vue时就传入要监测的数据
(1)对象中后追加的属性,Vue默认不做响应式处理
(2)如需要给后添加的属性做响应式,请使用如下API:Vue.set(target,propertyName/index,value)或vm.$set(target,propertyName/index,value)

③如何监测数组中的数据
通过包裹数组更新的方法实现,本质就是做了两件事:
(1)调用原生对应的方法对数组进行更新
(2)重写解析模板,进行更新页面

④在Vue修改数组中的某一个元素不能直接赋值,即不能直接带下标赋值,Vue监测不到,就像上面的问题一样,一定要用如下方法:
(1)使用数组的一些API:push()/pop()/shift()/unshift()/splice()/sort()/reverse()
(2)Vue.set()或vm.$set()

当然可以直接改变整个数组。

特别注意:Vue.set()和vm.$set()不能给vm或vm的根数据对象 添加属性

下面一个例子来实践上面的总结:

    <div id="root">
        <br><button @click="student.age++">年龄+1</button>
        <br><button @click="addsex">添加性别属性 男</button>
        <br><button @click="student.sex=''">修改性别</button>
        <br><button @click="addfriend">在列表首位添加一个朋友</button>
        <br><button @click="setfriend">修改第一个朋友的名字为张三</button>
        <br><button @click="addhobby">添加一个爱好</button>
        <br><button @click="sethobby">修改第一个爱好为:开车</button>
        <br>
        <h3>姓名:{
   
   {student.name}}</h3>
        <h3>年龄:{
   
   {student.age}}</h3>
        <h3 v-if="student.sex">性别:{
   
   {student.sex}}</h3>
        <h3>爱好:</h3>
        <ul>
            <li v-for="(h,index) in student.hobby" :key="index">{
   
   {h}}</li>
        </ul>
        <h3>朋友们:</h3>
        <ul>
            <li v-for="(f,index) in student.friend" :key="index">{
   
   {f.name}}---{
   
   {f.age}}</li>
        </ul>
    </div>
        Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                student: {
    
    
                    name: 'tom',
                    age: 18,
                    hobby: ['抽烟', '喝酒', '烫头'],
                    friend: [{
    
    
                        name: 'jerry',
                        age: 35
                    }, {
    
    
                        name: 'tony',
                        age: 36
                    }]
                }
            },
            methods: {
    
    
                addsex() {
    
    
                    Vue.set(this.student, 'sex', "男")
                },
                addfriend() {
    
    
                    this.student.friend.unshift({
    
    
                        name: 'Bob',
                        age: 31
                    })
                },
                setfriend() {
    
    
                    // 数组里面的对象有set方法,可以直接修改就会响应式的到页面
                    this.student.friend[0].name = "张三"
                },
                addhobby() {
    
    
                    this.student.hobby.push('学习')
                },
                sethobby() {
    
    
                    // this.student.hobby[0]='开车' 在Vue修改数组中的某一个元素不能直接赋值,即不能直接带下标赋值,Vue监测不到
                    Vue.set(this.student.hobby, 0, "开车")
                    // 或this.student.hobby.splice(0, 1, '开车')
                }
            }
        })

初始页面:
在这里插入图片描述
每个点击一遍后的页面:
在这里插入图片描述

11、收集表单数据

11.1、收集表单数据的例子:

收集如下页面的表单:
在这里插入图片描述
代码实现页面和收集:

    <div id="root">
        <form @submit.prevent='demo'>
            <br>账号:<input type="text" v-model.trim="bag.account"> <br><br> 
            密码:<input type="password" v-model="bag.password"> <br><br> 
            年龄:<input type="number" v-model.number="bag.age"> <br><br>
            性别: 男<input type="radio" name="sex" v-model="bag.sex" value="man"><input type="radio" name="sex" v-model="bag.sex" value="woman"><br><br>
            爱好:学习<input type="checkbox" v-model="bag.hobby" value="study">
            睡觉<input type="checkbox" v-model="bag.hobby" value="sleep">
            吃饭<input type="checkbox" v-model="bag.hobby" value="eat"><br>
            <br>所属校区:
            <select v-model="bag.city">
                <option value="">请选择校区</option>
                <option value="beijing">北京</option>
                <option value="shanghai">上海</option>
                <option value="shenzhen">深圳</option>
                <option value="wuhan">武汉</option>
            </select><br><br>
            其他信息:
            <textarea v-model.lazy="bag.other"></textarea> <br><br>
            <input type="checkbox" v-model="bag.agree">阅读并接受协议 <a href="">《用户协议》</a><br><br>
            <button>提交</button>
        </form>
    </div>
        Vue.config.productionTip = false; //设置为 false 以阻止 vue 在启动时生成生产提示。
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                bag: {
    
    
                    account: "",
                    password: "",
                    age: '',
                    sex: "woman",
                    hobby: [],
                    city: '',
                    other: '',
                    agree: ''
                }
            },
            methods: {
    
    
                demo() {
    
    
                    console.log(JSON.stringify(this.bag));
                }
            },
        })

在这里插入图片描述

11.2、上述表单总结

①若< input type=“text” >,则v-model收集的是value值,用户输入的就是value值

②若< input type=“radio” >则v-model收集的是value值,且要给标签配置value值

③若< input type=“checkbox” >

  • 没有配置input的value属性,那么收集的就是checked(勾选或未勾选,是布尔值)
  • 配置input的value属性:
    (1)v-model的初始值是非数组,那么收集的我就是checked(同样勾选或未勾选,布尔值)
    (2)v-model的初始值是数组,那么收集的就是value组成的数组

④v-model的三个修饰符(上述代码有用到):

  • lazy:失去焦点再收集数据
  • number:输入字符串转为有效的数字
  • trim:输入首尾空格过滤

12、过滤器

①定义:对要显示的数据进行特定格式化后再显示(使用于一些简单逻辑的处理)
②语法:
(1)注册过滤器: 全局过滤器 Vue.filter(name, callback)或局部过滤器 new Vue(filters:{})
(2)使用过滤器:插值语法 { {xxx | 过滤器名}} 或 v-bind:属性 = “xxx | 过滤器名”

备注:
(1)过滤器也可以接收额外参数,多个过滤器也可以串联
(2)并没有改变原本的数据,是产生新的对应数据

例子:

    <div id="root">
        <h2>显示格式化后的时间</h2>
        <!-- 计算属性实现 -->
        <h2>现在是{
   
   {fmtTime}}</h2>
        <!-- methods 实现 -->
        <h2>现在是{
   
   {getfmtTime()}}</h2>
        <!-- 过滤器实现 -->
        <h2>现在是{
   
   {time | timeFormater}}</h2>
        <h3></h3>
    </div>
        // 全局过滤器
        Vue.filter("mySlice", function(value) {
    
    
            return value.slice(0, 4)
        })
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                time: Date.now()
            },
            computed: {
    
    
                fmtTime() {
    
    
                    return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss')
                }
            },
            methods: {
    
    
                getfmtTime() {
    
    
                    return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss')
                }
            },
            filters: {
    
    
                timeFormater(value, str) {
    
    
                    return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
                }
            }
        })

13、内置指令与自定义指令

13.1、前面学过的内置指令

在这里插入图片描述

13.2、还有内置指令:v-text / v-html / v-cloak / v-once / v-pre

①v-text

    <div id="root">
        <div v-text="name"></div>
    </div>
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: "zhangsan"
            }
        })

作用:向其所在节点中渲染文本内容
与插值语法相比的区别:v-text会替换掉节点中的内容。{ {xx}}则不会

②v-html

    <div id="root">
        <div v-html="name"></div>
    </div>
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                name: "<h2>zhangsan</h2>"
            }
        })

作用:与v-text相比,支持html解析。向指定节点中渲染包含html结构的内容
v-html安全问题:
(1)在网站上动态渲染任意HTML都是非常危险的,容易导致XSS攻击(例如盗取cookie)
(2)一定要在可信的内容上使用v-html,永不要用在用户提交的内容上

③v-cloak

<!-- 样式 -->
 <style>
  [v-cloak]{
      
      
  	display:none;
  }
</style>

<!-- body -->
<div id="root">
    <div v-cloak>{
   
   {xxx}}</div>
</div>

v-cloak指令(没有值):
(1)本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性
(2)使用css配合v-cloak可以解决网速慢时页面展示出“{ {xx}}”的问题

④v-once
v-once(没有值):
(1)v-once所在节点初次动态渲染后,就视为静态内容了
(2)以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能

⑤v-pre
v-pre(没有值):
(1)跳过其所在节点的编译过程
(2)可以利用其跳过:没有指令语法、没有使用插值语法的节点,会加快编译

13.3、自定义指令

①定义语法
(1)局部指令:

//对象式
new Vue({
    
    
	directives:{
    
    指令名:配置对象}
})

//函数式
new Vue({
    
    
	directives:{
    
    指令名。回调函数}
})

(2)全局指令(和全局过滤器语法相似):

Vue.directive(指令名,配置对象)

Vue.directive(指令名,回调函数)

②配置对象常用的三个回调:
(1)bind:指令与元素成功绑定时调用
(2)inserted:指令所在元素被插入页面时调用
(3)update:指令所在模板结构被重新解析时调用

③备注:
(1)指令定义时不加v-,但使用时要加v-
(2)指令名如果是多个单词,要是用kebab-case命名方式,不要使用camelCase命名

④例子
(1) 函数式自定义指令

	<!-- 需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍 -->
    <div id="root">
        <h2>n的值为<span v-text="n"></span></h2>
        <h2>放大后n的值为<span v-big="n"></span></h2>
        <button @click="n++">点击n++</button>
    </div>
const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                n: 1
            },
            directives: {
    
    
                //big函数何时会被调用?
                // 1、指令与元素成功绑定时(一上来)
                // 2、指令所在的模板被重新解析时
                big(element, binding) {
    
    
                    element.innerText = binding.value * 10;
                }
            }
        })

(2)对象式自定义指令

<!-- 需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其他所绑定的input元素默认获取焦点 -->
    <div id="root">
        <input type="text" v-bind:value="n"><br>
        <input type="text" v-fbind:value="n"><br>
    </div>
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                n: 1
            },
            directives: {
    
    
                fbind: {
    
    
                    // 指令与元素成功绑定时(一上来)
                    bind(element, binding) {
    
    
                        console.log(this); //此处this是window,下面同
                        element.value = binding.value
                    },
                    // 指令所在元素被插入页面时
                    inserted(element, binding) {
    
    
                        element.focus()
                    },
                    // 指令所在的模板被重新解析时
                    update(element, binding) {
    
    
                        element.value = binding.value
                    }
                }
            }
        })

(3)全局指令

    <!-- 需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其他所绑定的input元素默认获取焦点 -->
    <div id="root">
        <!-- 用全局自定义指令 -->
        <input type="text" v-ffbind:value="n">
    </div>
		//全局指令
 		Vue.directive('ffbind', {
    
    
            // 指令与元素成功绑定时(一上来)
            bind(element, binding) {
    
    
                element.value = binding.value
            },
            // 指令所在元素被插入页面时
            inserted(element, binding) {
    
    
                element.focus()
            },
            // 指令所在的模板被重新解析时
            update(element, binding) {
    
    
                element.value = binding.value
            }
        })
        const vm = new Vue({
    
    
            el: "#root",
            data: {
    
    
                n: 1
            }
        })

14、Vue实例生命周期

14.1、引出生命周期

生命周期又名生命周期回调函数、生命周期函数、生命周期钩子,是Vue在关键时刻帮我们调用的一些特殊名称的函数。

生命周期函数的名字不可更改,但函数的具体内容是根据需求编写的

生命周期函数中的this指向是vm 或组件实例对象

14.2、生命周期各个流程(挂载流程、更新流程、销毁流程)

在这里插入图片描述
上面的生命周期流程中有创建、挂载、更新、销毁四个阶段,这四个阶段前后都对应两个生命周期函数,一共八个生命周期函数(还有三个其他生命周期函数到路由的时候在介绍),下面将上图流程细化讲解。

①创建阶段以及前后对应的两个函数:
(这里的创建不是之vue实例的创建,而是数据监测和数据代理是否初始化了)
在这里插入图片描述

创建阶段,也就是初始化数据监测和数据代理阶段,前后两个函数为:

之前的beforeCreate:此时无法通过vm访问到data中的数据、methods中的方法
之后的created:此时可以通过vm访问到data中的数据、methods中的方法


(创建阶段结束后到挂载阶段开始之前,Vue开始解析模板,生成虚拟DOM(内存中),页面还不能显示解析好的内容)
在这里插入图片描述

②挂载阶段以及前后对应的两个函数:
(挂载阶段所做的是将上面解析好的模板而生成的虚拟DOM转为真实DOM插入页面
在这里插入图片描述
挂载前后的两个函数:
之前的beforeMount:此时(1)页面呈现的是未经Vue编译的DOM结构 (2)所有DOM的操作均不奏效
之后的mounted:此时(1)页面中呈现的是经过Vue编译的DOM (2)对DOM的操作均有效(尽可能避免)。至此初始化过程结束,一般在这个函数内进行:开启定时器、发送网络请求、订阅消息、绑定自定义事件等初始化操作

③更新阶段以及前后对应的两个函数:
(更新阶段就是 根据新数据,生成新的虚拟DOM,随后与旧的虚拟DOM进行比较,最终完成页面更新,即:完成了Model -> View的更新)
在这里插入图片描述

更新前后的两个函数:
之前的beforeUpdate:此时数据是新的,但页面时旧的,即:页面尚未和数据保持同步
之后的updated:此时数据是新的,页面也是新的,即页面和数据保持同步

④销毁阶段以及前后对应的两个函数:
在这里插入图片描述
销毁前后的两个函数:
之前的beforeDestroy:vm中所有的e:data、methods、指令等等都处于可用状态,但马上要执行销毁过程。一般在此阶段:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作
之后的destroyed:无

总结一波:
第一点:四个阶段,八个生命周期函数。
在这里插入图片描述
第二点:其中挂载之后的mounted函数和销毁之前的beforeDestroy函数是常用的,重要。
(1)mounted:发送ajax请求、开启定时器、订阅消息、绑定自定义事件等初始化操作
(2)beforeDestroy:一般在此阶段:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作

第三点:关于销毁Vue实例:
(1)销毁后借助Vue开发者工具看不到任何信息
(2)销毁后自定义事件会失效,但原生DOM事件依然有效
(3)一般不会再beforeDestroy操作数据,因为几遍操作数据了也不会再触发更新流程了

二、Vue组件化编程

1、模块、组件、模块化、组件化

1.1、模块与模块化

①模块
(1)理解: 向外提供特定功能的 js 程序, 一般就是一个 js 文件
(2)为什么: js 文件很多很复杂
(3)作用: 复用 js, 简化 js 的编写, 提高 js 运行效率

②模块化
当应用中的 js 都以模块来编写的, 那这个应用就是一个模块化的应用。

1.2、组件与组件化

①组件
理解: 用来实现局部(特定)功能效果的代码集合(html/css/js/image……)
为什么: 一个界面的功能很复杂

②组件化
当应用中的功能都是多组件的方式来编写的, 那这个应用就是一个组件化的应用。

2、非单文件组件

2.1、组件的基本使用

下面是一个创建组件的实例

    <div id="root">
        <!-- 第三步:编写组件标签 -->
        <he></he>
        <hr>
        <xuexiao></xuexiao>
        <xuesheng></xuesheng>
        <hr>

    </div>
    <div id="root1">
        <he></he>
    </div>
        // 第一步:创建school组件
        const school = Vue.extend({
    
    
            template: `
                <div>
                    <h2>学校名称:{
     
     {schoolName}}</h2>
                    <h2>学校地址:{
     
     {address}}</h2>
                </div>
            `,
            data() {
    
    
                return {
    
    
                    schoolName: '武汉大学',
                    address: '武汉'
                }
            }
        });
        // 第一步:创建student组件
        const student = Vue.extend({
    
    
            template: `
                <div>
                    <h2>学生姓名:{
     
     {studentName}}</h2>
                    <h2>学生年龄:{
     
     {age}}</h2>
                </div>
            `,
            data() {
    
    
                return {
    
    
                    studentName: '张三',
                    age: 18
                }
            }
        });

        const hello = Vue.extend({
    
    
            template: `
                <div>
                    <h2>hello,{
     
     {name}}</h2>
                </div>
            `,
            data() {
    
    
                return {
    
    
                    name: 'Tom'
                }
            }
        });
        // 第二步:注册全局组件
        Vue.component('he', hello)
            // 创建vm
        new Vue({
    
    
            el: "#root",
            // 第二步:注册组件(局部注册)
            components: {
    
    
                xuexiao: school,
                xuesheng: student
            }
        });
        new Vue({
    
    
            el: "#root1"
        })

上面实例中,可以看出
①使用组件的三大步骤:
(1)定义组件(创建组件)
(2)注册组件
(3)使用组件(写组件标签)

细说三大步骤:
1、如何定义一个组件:
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;
区别如下:
(1)el不要写,因为 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
(2)data必须写成函数,因为 避免组件被复用时,数据存在引用关系。
备注:使用template可以配置组件结构。

2、如何注册组件
(1)局部注册:靠new Vue的时候传入components选项
(2)全局注册:靠Vue.component(‘组件名’,组件)

3、编写组件标签:< school>< /school>

2.2、几个注意点

①关于组件名
(1)一个单词组成:第一种写法(首字母小写):school;第二种写法(首字母大写):School
(2)多个单词组成:第一种写法(kebab-case命名):my-school;第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
(1)组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
(2)可以在组件中使用name配置项指定组件在开发者工具中呈现的名字。

②关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,会导致后续组件不能渲染。

③创建组件的一个简写方式:
const school = Vue.extend(options) 可简写为:const school = options

2.3、组件的嵌套

用下面这个例子来体现组件的嵌套:

    <div id="root">

    </div>
        // 第一步:创建student组件
        const student = Vue.extend({
    
    
            template: `
                <div>
                    <h2>学生姓名:{
     
     {studentName}}</h2>
                    <h2>学生年龄:{
     
     {age}}</h2>
                </div>
            `,
            data() {
    
    
                return {
    
    
                    studentName: '张三',
                    age: 18
                }
            }
        });
        // 第一步:创建school组件
        const school = Vue.extend({
    
    
            template: `
                <div>
                    <h2>学校名称:{
     
     {schoolName}}</h2>
                    <h2>学校地址:{
     
     {address}}</h2>
                    <xuesheng></xuesheng>
                </div>
            `,
            components: {
    
    
                xuesheng: student
            },
            data() {
    
    
                return {
    
    
                    schoolName: '武汉大学',
                    address: '武汉'
                }
            }
        });
		// 第一步:创建hello组件
        const hello = Vue.extend({
    
    
            template: `
                <div>
                    <h2>hello,{
     
     {name}}</h2>
                </div>
            `,
            data() {
    
    
                return {
    
    
                    name: 'Tom'
                }
            }
        });
		// 第一步:创建app组件
        const app = Vue.extend({
    
    
            template: `
                <div>
                    <he></he>
                    <xuexiao></xuexiao>
                </div>
            `,
            components: {
    
    
                xuexiao: school,
                he: hello
            }
        })
        //vm实例
        new Vue({
    
    
            el: "#root",
            template: `<app></app>`,
            // 第二步:注册组件(局部注册)
            components: {
    
    
                app
            }
        })
2.4、关于Vue.Component

(1)school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。

(2)我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。

(3)特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!

(4)关于this指向:
组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。

(5)VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。Vue的实例对象,以后简称vm。

2.5、一个重要的内置关系

一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype

要想清楚了解这个内置关系就要先了解JavaScript高级部分的原型链相关的知识,下面先简单复习一下原型链的知识:
在这里插入图片描述
在这里插入图片描述
了解原型链后再分析Vue与VueComponent的关系
在这里插入图片描述
上图中的黄色的线本应该指向Object的原型对象的,Vue中将其改为指向Vue的原型对象。就有了VueComponent.prototype.__proto__ === Vue.prototype

为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

3、单文件组件

了解了上面的非单文件组件后,单文件组件就容易理解了。单组件让每一个组件都自成一个文件,后缀为.vue,里面包含三部分:<template></template>、<style></style>、<script></script>,分别在这三个标签里写结构、样式、逻辑。

这些多个组件文件需要组合起来这就涉及到js模块化的知识了,下面简单复习一下:
①模块功能主要由两个命令(暴露和引入)构成:export和import
export命令用于规定模块的对外接口
import命令用于输入其他模块提供的功能

②暴露数据语法汇总:
(1)统一暴露
在这里插入图片描述

(2)分别暴露:
在这里插入图片描述

(3)默认暴露(default)(vue中常用)
在这里插入图片描述

③引入模块数据语法汇总
(1)通用的导入方式
在这里插入图片描述
(2)解构赋值形式
在这里插入图片描述
(3)简便形式(只针对默认暴露)
在这里插入图片描述

简单复习一下后,将上节的非单文件组件例子写成单文件组件,当然这就要写多个文件,如下:
在这里插入图片描述
首先是School组件和Student组件:

<template>
	<div class="demo">
		<h2>学校名称:{
   
   {name}}</h2>
		<h2>学校地址:{
   
   {address}}</h2>
		<button @click="showName">点我提示学校名</button>	
	</div>
</template>

<script>
	 export default {
      
      
		name:'School',
		data(){
      
      
			return {
      
      
				name:'尚硅谷',
				address:'北京昌平'
			}
		},
		methods: {
      
      
			showName(){
      
      
				alert(this.name)
			}
		},
	}
</script>

<style>
	.demo{
      
      
		background-color: orange;
	}
</style>
<template>
	<div>
		<h2>学生姓名:{
   
   {name}}</h2>
		<h2>学生年龄:{
   
   {age}}</h2>
	</div>
</template>

<script>
	 export default {
      
      
		name:'Student',
		data(){
      
      
			return {
      
      
				name:'张三',
				age:18
			}
		}
	}
</script>

再就是App.vue,用来汇总所有组件:

<template>
	<div>
		<School></School>
		<Student></Student>
	</div>
</template>
<script>
	//引入组件
	import School from './School.vue'
	import Student from './Student.vue'

	export default {
      
      
		name:'App',
		components:{
      
      
			School,
			Student
		}
	}
</script>

接着写main.js,里面创建vue实例

import App from './App.vue'

new Vue({
    
    
	el:'#root',
	template:`<App></App>`,
	components:{
    
    App},
})

最后就是整个页面的结构,也就是index.html:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>练习一下单文件组件的语法</title>
	</head>
	<body>
		<!-- 准备一个容器 -->
		<div id="root"></div>
		<!-- <script type="text/javascript" src="../js/vue.js"></script> -->
		<!-- <script type="text/javascript" src="./main.js"></script> -->
	</body>
</html>

上面虽然写完了,但是浏览器无法支持vue、es6模块化的语法,页面暂时无法显示,控制台会报错,等下一章学脚手架再解决。

三、Vue脚手架

1、初始化脚手架

1.1、说明

①Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)。
②最新的版本是 4.x。
⑤文档: https://cli.vuejs.org/zh/。

1.2、具体步骤

第一步(仅第一次执行):全局安装@vue/cli。
命令行输入:npm install -g @vue/cli

第二步:切换到你要创建项目的目录,然后使用命令创建项目
命令行输入:vue create 项目名

第三步:启动项目
命令行输入:npm run serve

备注:
如出现下载缓慢请配置 npm 淘宝镜像:
npm config set registry https://registry.npm.taobao.org

1.3、分析脚手架结构

①结构目录
在这里插入图片描述
先说说倒数的几个文件:
.gitignore:设置哪些文件不想接受git的管理
babel.config.js:babel的控制文件
package-lock.json:包的版本控制文件
package.json:包的说明书

再说说src文件夹中的文件:
(1)入口文件main.js:

/* 
  该文件是整个项目的入口文件 
*/

// 引入Vue
import Vue from 'vue'
// 引入App组件,它是所有组件的父组件
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false
    // 创建Vue实例对象
new Vue({
    
    
    // 这行代码下后面会解释,功能:将App组件放入容器中(注册App组件)
    render: h => h(App),
    // 挂载的第二种写法
}).$mount('#app')

(2)App.vue文件:管理其他组件

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

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

<style>
#app {
      
      
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

(3)在就是assets文件夹:用来存放静态资源
(4)还有components:用来存放除App.vue的其他组件

再就是public文件夹:
(1)favicon.ico:网站的页签图标
(2)index.html:整个应用的界面

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <!-- 针对IE浏览器的一个特殊配置,含义让IE以最高级别渲染页面 -->
    <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>
    <!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
    <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>
    <!-- built files will be auto injected -->
</body>

</html>

最后将上一章的单文件组件放入脚手架中就能实现页面:
在这里插入图片描述
将School和Student两个组件移到components文件夹中,App组件移到src中,(注意修改路径),最后控制台输入npm run serve,运行,复制链接去网页就可以了。
在这里插入图片描述
在这里插入图片描述

1.4、render函数

脚手架中的main.js代码里有下面这一行代码:
在这里插入图片描述
关于不同版本的Vue:

	1.vue.js与vue.runtime.xxx.js的区别:
			(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
			(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
			
	2.因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用
	render函数接收到的createElement函数去指定具体内容。
1.5、修改默认配置

Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置,
请执行:vue inspect > output.js

个性化修改脚手架就需要去官网到配置参考.
在这里插入图片描述
首先给自己的脚手架中新建一个名为vue.config.js的文件,这是一个可选的配置文件:
在这里插入图片描述
这个文件应该导出一个包含了选项的对象:

// vue.config.js

/**
 * @type {import('@vue/cli-service').ProjectOptions}
 */
module.exports = {
    
    
  // 选项...
}

将这些代码复制到新建的名为vue.config.js的文件中:
在这里插入图片描述
需要修改什么配置项就将相关的代码复制到新建文件中的对象里去,这里就以关掉语法检查为例:
在这里插入图片描述

在这里插入图片描述
将lintOnSave改为false后写入到对象中作为配置项:
在这里插入图片描述
这就成功关闭了语法检查。
后续还需要修改别的配置项再说。

2、ref属性、props配置项

2.1、ref属性

ref属性是Vue给标签的赋予的一个属性:
(1)被用来给元素或子组件注册引用信息(id的替代者)
(2)应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
(3) 使用方式:
打标识:<h1 ref="xxx">.....</h1><School ref="xxx"></School>
获取:this.$refs.xxx

2.2、props配置项
  1. 功能:让组件接收外部传过来的数据

  2. 传递数据:<Demo name="xxx"/>

  3. 接收数据:

第一种方式(只接收):props:['name']

props:['name','address']

第二种方式(限制类型):props:{name:String}

props:{
    
    
        name:String,
        address:String
    }

第三种方式(限制类型、限制必要性、指定默认值):

    props:{
    
    
        name:{
    
    
            type:String,//类型
            required:true//必要性
        },
        address:{
    
    
            type:String,
            default:'武汉'//默认值

        }
    }

备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
在这里插入图片描述

3、mixins(混入)配置项

功能:可以把多个组件共用的配置提取成一个混入对象

使用方式:
第一步定义混合:

   {
       data(){....},
       methods:{....}
       ....
   }

第二步使用混入(在需要用混入的组件中):

全局混入:Vue.mixin(xxx)
​局部混入:mixins:['xxx']

4、插件

功能:用于增强Vue

本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。

定义插件:

    对象.install = function (Vue, options) {
    
    
        // 1. 添加全局过滤器
        Vue.filter(....)
    
        // 2. 添加全局指令
        Vue.directive(....)
    
        // 3. 配置全局混入(合)
        Vue.mixin(....)
    
        // 4. 添加实例方法
        Vue.prototype.$myMethod = function () {
    
    ...}
        Vue.prototype.$myProperty = xxxx
    }

使用插件:Vue.use()

下面将之前学过的过滤器、自定义指令、混入都放入插件中:

export default {
    
    
    install(Vue) {
    
    
        // 全局过滤器
        Vue.filter("mySlice", function(value) {
    
    
            return value.slice(0, 4)
        });
        // 全局自定义指令
        Vue.directive('ffbind', {
    
    
            // 指令与元素成功绑定时(一上来)
            bind(element, binding) {
    
    
                element.value = binding.value
            },
            // 指令所在元素被插入页面时
            inserted(element, binding) {
    
    
                element.focus()
            },
            // 指令所在的模板被重新解析时
            update(element, binding) {
    
    
                element.value = binding.value
            }
        });
        // 混入
        Vue.mixin({
    
    
                data() {
    
    
                    return {
    
    
                        x: 100,
                        y: 200
                    }
                },
            })
            // 给Vue原型上添加一个方法(vm和vc就都可以用)
        Vue.prototype.hello = () => {
    
    
            alert('nihao');
        }
    }
}

然后再main.js中引入和使用:

// 引入插件
import plugins from './plugins'
// 使用插件
Vue.use(plugins);

这样组件中就可以用插件中的东西了:

<template>
    <div class="demo">
        <h1>{
   
   {msg}}</h1>
        <!-- 使用插件中的过滤器 -->
        <h2>学校名称:{
   
   {name | mySlice}}</h2>
        <h2>学校地址:{
   
   {address}}</h2>
        <!-- 使用插件中的自定义指令和混入 -->
        <input type="text" v-ffbind="x+y">
        <!-- 使用插件中的方法 -->
        <button @click="hello">点我测试hello方法</button>
    </div>
</template>

<script>
export default {
      
      
    name:'School',
    data() {
      
      
        return {
      
      
            msg:'学校信息',
            name:'北大1234',
            address:'北京'
        }
    }
}
</script>

<style>
    .demo{
      
      
        background-color: pink;
    }
</style>

穿插 scoped样式

  1. 作用:让样式在局部生效,防止冲突。
  2. 写法:<style scoped>

5、通用组件化编程流程(用一个Todo-list 案例来完成)

5.1、组件化编码流程
  1. 实现静态组件:抽取组件,使用组件实现静态页面效果
  2. 展示动态数据:(①数据的类型、名称是什么?②数据保存在哪个组件?)
  3. 交互-----从绑定事件监听开始
5.2、Todo-list 案例

①页面效果展示
在这里插入图片描述
②src文件目录下的结构
在这里插入图片描述
③具体文件代码:
main.js:

// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'

// 关闭Vue的生产提示
Vue.config.productionTip = false;

// 创建vm
new Vue({
    
    
    el: '#app',
    render: h => h(App)
})

App.vue:

<template>
  <div id="root">
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader :addTodo="addTodo"></MyHeader>
      <MyList :todos='todos' :handleCheck="handleCheck" :handleDelete="handleDelete"></MyList>
      <MyFooter :todos='todos' :allDelete='allDelete' :checkAllTodo='checkAllTodo'></MyFooter>
    </div>
  </div>
</div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'
    export default {
      
      
        name:'App',
        components:{
      
      
            MyHeader,
            MyList,
            MyFooter
        },
        data() {
      
      
        return {
      
      
            todos:[
                {
      
      id:'001',title:'20天学完Vue2',done:false},
                {
      
      id:'002',title:'期末各科实验报告完成',done:false},
                {
      
      id:'003',title:'吃喝玩乐',done:true},
            ]
        }
    },
    methods:{
      
      
        // 添加一个todo
        addTodo(todoObj){
      
      
            this.todos.unshift(todoObj)
        },
        // 勾选or取消勾选一个todo
        handleCheck(id){
      
      
            this.todos.forEach((todo)=>{
      
      
                if(todo.id===id){
      
      
                    todo.done=!todo.done
                }
            })
        },
        // 删除一个todo
        handleDelete(id){
      
      
            // 方法一:用splice
            /* this.todos.forEach((todo,index)=>{
                if(todo.id===id){
                    this.todos.splice(index,1)
                }
            }) */
            // 方法二:用filter
            this.todos =this.todos.filter(todo => todo.id!==id)
        },

        // 删除已完成todo
        allDelete(){
      
      
            this.todos=this.todos.filter((todo)=>!todo.done)
        },
        // 全选或取消全选
        checkAllTodo(done){
      
      
            this.todos.forEach((todo)=>{
      
      
                todo.done =done
            })
        }
    }
    }
</script>

<style>
        /*base*/
    .btn {
      
      
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        border-radius: 4px;
    }

    .btn-danger {
      
      
        color: #fff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
      
      
        color: #fff;
        background-color: #bd362f;
    }

    .btn:focus {
      
      
         outline: none;
    }

    .todo-container {
      
      
        width: 600px;
        margin: 0 auto;
    }
    .todo-container .todo-wrap {
      
      
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>

MyHeader.vue:

<template>
  <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>

<script>
import {
      
      nanoid} from 'nanoid'
export default {
      
      
    name:'MyHeader',
    data() {
      
      
        return {
      
      
            title:''
        }
    },
    methods:{
      
      
        add(e){
      
      
            // 校验数据
            if(!this.title.trim()) return alert('输入不能为空!')
            // 将用户输入包装成一个todo对象
            const todoObj={
      
      id:nanoid(),title:this.title,done:false}
            // 通知App组件去添加一个todo对象
            // 子组件向父组件传输数据可以在父组件中定义一个方法,再将方法通过props配置项传给子组件,子组件通过方法来传给父组件
            this.addTodo(todoObj)
            // 清空输入
            this.title=''
        }
    },
    props:['addTodo']
}
</script>

<style scoped>
    /*header*/
    .todo-header input {
      
      
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
    }

    .todo-header input:focus {
      
      
        outline: none;
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
    }
</style>

MyList.vue:

<template>
  <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" 
            :todo="todoObj" 
            :handleCheck="handleCheck"
            :handleDelete="handleDelete"
        ></MyItem>
      </ul>
</template>

<script>
import MyItem from './MyItem.vue'
export default {
      
      
    name:'MyList',
    components:{
      
      MyItem},
    // 父组件向子组件传输数据直接用props配置项
    props:['todos','handleCheck','handleDelete']
}
</script>

<style scoped>
    /*main*/
    .todo-main {
      
      
        margin-left: 0px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0px;
    }

    .todo-empty {
      
      
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>

MyItem.vue:

<template>
  <li>
          <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
           <!--  如下代码也能实现上面勾选得到值的功能,但是不推荐这样写,
            因为Vue是不允许修改props中的数据的,这里就修改了props
            这里修改props没有报错是因为修改的是数组中元素的值,Vue监测不到,就相当于浅监测,只有地址改变了才监测的到
            但是总之虽然没有报错,还是不建议这样写 -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{
   
   {todo.title}}</span>
          </label>
          <button class="btn btn-danger" @click="handleOption(todo.id)">删除</button>
        </li>
</template>

<script>
export default {
      
      
    name:'MyItem',
    // 声明接收todo对象
    props:['todo','handleCheck','handleDelete'],
    methods:{
      
      
        handleOption(id){
      
      
            if(confirm('确定要删除吗?')) {
      
      
                this.handleDelete(id)
            }
    }
    }
    
}
</script>

<style scoped>
/*item*/
    li {
      
      
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
      
      
        float: left;
        cursor: pointer;
    }

    li label li input {
      
      
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
      
      
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
      
      
      content: initial;
    }

    li:last-child {
      
      
     border-bottom: none;
    }

    li:hover{
      
      
        background-color: #ddd;
    }

    li:hover button{
      
      
        display: block;
    }
</style>

MyFooter.vue:

<template>  
    <div class="todo-footer" v-show="todos.length">
        <label>
        <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
        <span>已完成{
   
   {TrueTodos}}</span> / 全部{
   
   {todos.length}}
        </span>
        <button class="btn btn-danger" @click="optionAll">清除已完成任务</button>
    </div>
</template>

<script>
export default {
      
      
    name:'MyFooter' ,
    props:['todos','allDelete','checkAllTodo'],
    methods:{
      
      
        optionAll(){
      
      
            if(confirm('确定要删除吗?')) {
      
      
                this.allDelete()
            }
        }
    },
    computed:{
      
      
        TrueTodos(){
      
      
            //方法一:filter
            // return this.todos.filter(todo=>todo.done).length
            // 方法二:reduce 第一个参数为回调函数 第二个参数为统计值的初始值
            // 数组长度为多少,第一个参数回调函数就被调用多少
            // 回调函数由两个参数,第一个参数pre第一次是统计值的初始值,后面还有回调的话,pre就是前一次的返回值
            // 第二个参数是每次数组中的元素
            return this.todos.reduce((pre,curent)=>pre+(curent.done?1:0),0)

        },
        isAll:{
      
      
            get(){
      
      
                return this.todos.length===this.TrueTodos && this.todos.length>0
            },
            set(value){
      
      
                this.checkAllTodo(value)
            }
        }
    }
}
</script>

<style scoped>
   /*footer*/
    .todo-footer {
      
      
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
    }

    .todo-footer label {
      
      
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
      
      
        position: relative;
        top: -1px;
        vertical-align: middle;
        margin-right: 5px;
    }

    .todo-footer button {
      
      
        float: right;
        margin-top: 5px;
    }
</style>

一定要自己敲一下。
后续学到新内容后还有要完善的会提及并完善。

④Todo-list 案例总结一波
(1)组件化编码流程:

​	第一步,拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

​	第二步,实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

​			1).一个组件在用:放在组件自身即可。

​			2). 一些组件在用:放在他们共同的父组件上(<span style="color:red">状态提升</span>)。

    ​第三步,实现交互:从绑定事件开始。

(2)props适用于:

​	(1).父组件 ==> 子组件 通信

​	(2).子组件 ==> 父组件 通信(要求父先给子一个函数)

(3)使用v-model时要切记:
v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

(4) props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

5.3、webStorage 本地存储

① 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)

②浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。

③ 相关API:

  1. xxxxxStorage.setItem('key', 'value');
    该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。

  2. xxxxxStorage.getItem('person');
    该方法接受一个键名作为参数,返回键名对应的值。

  3. xxxxxStorage.removeItem('key');
    该方法接受一个键名作为参数,并把该键名从存储中删除。

  4. xxxxxStorage.clear()
    该方法会清空存储中的所有数据。

④ 备注:

  1. SessionStorage存储的内容会随着浏览器窗口关闭而消失。
  2. LocalStorage存储的内容,需要手动清除才会消失。
  3. xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null。
  4. JSON.parse(null)的结果依然是null。

⑤将上面的Todo-list 案例改为本地存储,即使刷新页面,记录的内容依旧在

只需要给App.vue配置一个监听属性,将变化后的todos通过localStorage.setItem存储到本地,然后初始化的todos改为通过localStorage.getItem从本地获取,这里就不再把上面的代码全写了,只写需要改变的代码:
添加一个watch配置项:

     // 监视todos是否发生改变,改变后将其存储到本地
    watch:{
    
    
        todos:{
    
    
            // 开启深度监视,监视到数组中对象元素中的done的变化
            deep:true,
            handler(value){
    
    
                localStorage.setItem('todos', JSON.stringify(value))
            }
        }
    }

将data改为:

	data() {
    
    
            return {
    
    
            // 本地没有数据时,数组就为null了,后面的length属性就会报错,所以后面加一个||[]
            todos:JSON.parse(localStorage.getItem('todos'))||[]
            }
        },

6、Vue 中的自定义事件

①一种组件间通信的方式,适用于:子组件 ===> 父组件
② 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。

③绑定自定义事件:

  1. 第一种方式,在父组件中:<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>

  2. 第二种方式,在父组件中:

    <Demo ref="demo"/>
    ......
    mounted(){
          
          
       this.$refs.xxx.$on('atguigu',this.test)
    }
    
  3. 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。

④ 触发自定义事件:this.$emit('atguigu',数据)

⑤解绑自定义事件this.$off('atguigu')

⑥组件上也可以绑定原生DOM事件,需要使用native修饰符。

⑦ 注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,否则this指向会出问题!

⑧总结:子组件触发事件,传递数据;父组件给子组件绑定事件,写回调函数

7、全局事件总线(GlobslEventBus)

①一种组件间通信的方式,适用于任意组件间通信。

②安装全局事件总线:

   new Vue({
    
    
   	......
   	beforeCreate() {
    
    
   		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
   	},
       ......
   }) 

③使用事件总线:

  1. 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
        methods(){
    
    
        demo(data){
    
    ......}
      }
      ......
      mounted() {
    
    
        this.$bus.$on('xxxx',this.demo)
  1. 提供数据:this.$bus.$emit('xxxx',数据)

④最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。

⑤总结:发送数据的组件触发事件,传递数据;接收数据的组件给$bus绑定事件,写回调函数

⑥将之前todos案例中MyItem给App提供数据要通过MyList 改为 直接传输数据:
提供数据的MyItem改为(style省略了):

<template>
  <li>
          <label>
          <!-- $bus.$emit('handleCheck',todo.id触发事件,提供数据 -->
            <input type="checkbox" :checked="todo.done" @change="$bus.$emit('handleCheck',todo.id)"/>
            <span>{
   
   {todo.title}}</span>
          </label>
          <button class="btn btn-danger" @click="handleOption(todo.id)">删除</button>
        </li>
</template>

<script>
export default {
      
      
    name:'MyItem',
    props:['todo'],
    methods:{
      
      
        handleOption(id){
      
      
            if(confirm('确定要删除吗?')) {
      
      
            // this.$bus.$emit('handleDelete',id)触发事件,提供数据
                this.$bus.$emit('handleDelete',id)
            }
        }
    }
    
}
</script>

接收数据的App改为(style省略了):

<template>
  <div id="root">
  <div class="todo-container">
    <div class="todo-wrap">
      <MyHeader @addTodo="addTodo"></MyHeader>
      <MyList :todos='todos' ></MyList>
      <MyFooter :todos='todos' @allDelete='allDelete' @checkAllTodo='checkAllTodo'></MyFooter>
    </div>
  </div>
</div>
</template>

<script>
    import MyHeader from './components/MyHeader.vue'
    import MyList from './components/MyList.vue'
    import MyFooter from './components/MyFooter.vue'
    export default {
      
      
        name:'App',
        components:{
      
      
            MyHeader,
            MyList,
            MyFooter
        },
        data() {
      
      
            return {
      
      
            todos:JSON.parse(localStorage.getItem('todos'))||[]
            }
        },
        methods:{
      
      
            addTodo(todoObj){
      
      
                this.todos.unshift(todoObj)
            },
            handleCheck(id){
      
      
                this.todos.forEach((todo)=>{
      
      
                    if(todo.id===id){
      
      
                        todo.done=!todo.done
                    }
                })
            },
            handleDelete(id){
      
      
                this.todos =this.todos.filter(todo => todo.id!==id)
            },
            allDelete(){
      
      
                this.todos=this.todos.filter((todo)=>!todo.done)
            },
            checkAllTodo(done){
      
      
                this.todos.forEach((todo)=>{
      
      
                    todo.done =done
                })
            }
        },
        watch:{
      
      
            todos:{
      
      
                deep:true,
                handler(value){
      
      
                    localStorage.setItem('todos', JSON.stringify(value))
                }
            }
        },
        // 给$bus绑定事件,写回调函数
        mounted(){
      
      
            this.$bus.$on('handleCheck',this.handleCheck)
            this.$bus.$on('handleDelete',this.handleDelete)
        },
        // 用完最好销毁
        beforeDestroy(){
      
      
            this.$bus.$off('handleCheck')
            this.$bus.$off('handleDelete')
        }
    }
</script>

当然入口文件main.js也需要安装全局事件总线:

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false;
new Vue({
    
    
    el: '#app',
    render: h => h(App),
    beforeCreate() {
    
    
        Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
    }
})

这样就不需要MyList中转数据了,相关的代码也可以删除了。

8、消息订阅与发布(pubsub)

①一种组件间通信的方式,适用于任意组件间通信

②使用步骤:
(1)安装消息订阅与发布的库,有许多这种库,这里安装pubsub-js,安装命令:npm i pubsub-js
(2)在组件中引用:import pubsub from ‘pubsub-js’
(3)接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调写在A组件自身:

   methods(){
    
    
       //第一个是消息名,一般用不到,第二个是传的数据
        demo(msgName,data){
    
    ......}
      }
      ......
   mounted() {
    
    
        this.pubId = pubsub.subscribe('xxx',this.demo) //订阅消息
      }

当然也可以直接写成箭头函数:

   mounted() {
    
    
        this.pid = pubsub.subscribe('xxx',(msgName,data)=>{
    
    ....}) //订阅消息

(4)提供数据:B组件发布消息,pubsub.publish('xxx',数据)
(5)最好在beforeDestroy钩子中,用PubSub.unsubscribe(pubId)去取消订阅。

③在todos案例中MyItem给App提供数据 改为 消息的订阅与发布(改一个 删除一个todo):
在App中首先得引入pubsub库:

    import pubsub from 'pubsub-js'

methods中的handleDelete也得传两个参数,第一个是消息的名字用不到,可以写一个下划线:

 handleDelete(_,id){
    
    
                this.todos =this.todos.filter(todo => todo.id!==id)
}

接下来就可以订阅消息和用完销毁:

       mounted(){
    
    
           this.pubId=pubsub.subscribe('handleDelete',this.handleDelete)
        },
        // 用完最好销毁
        beforeDestroy(){
    
    
            pubsub.unsubscribe(this.pubId)
        }

再就是MyItem中,先引入pubsub库:

import pubsub from 'pubsub-js'

再就是发布消息:

		handleOption(id){
    
    
            if(confirm('确定要删除吗?')) {
    
    
                pubsub.publish('handleDelete',id)
            }
        }

穿插:todos案例增加一个编辑功能和$nextTick()

这里给todos案例增加一个编辑功能
在这里插入图片描述
在MyItem中(style就都省略了):

<template>
  <li>
          <label>
            <input type="checkbox" :checked="todo.done" @change="$bus.$emit('handleCheck',todo.id)"/>
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span v-show="!todo.isEdit">{
   
   {todo.title}}</span>
            <!-- 增加一个input框 失去焦点时,触发handleBlur-->
            <input 
                type="text" 
                :value="todo.title" 
                v-show="todo.isEdit"
                @blur="handleBlur(todo,$event)"
                ref="inputTitle"
                >
          </label>
          <button class="btn btn-danger" @click="handleOption(todo.id)">删除</button>
          <!--增加一个编辑按钮 点击后触发handleEdit(todo) -->
          <button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>
        </li>
</template>

<script>
import pubsub from 'pubsub-js'
export default {
      
      
    name:'MyItem',
    props:['todo'],
    methods:{
      
      
        handleOption(id){
      
      
            if(confirm('确定要删除吗?')) {
      
      
                pubsub.publish('handleDelete',id)
            }
        },
        // handleEdit(todo)触发后,添加一个isEdit属性,判断编辑与否
        handleEdit(todo){
      
      
            // 不是第一次点击编辑就不用再添加isEdit,直接改为true
            if(todo.hasOwnProperty('isEdit')){
      
      
                todo.isEdit=true
            }else{
      
      
                this.$set(todo,'isEdit',true)
            }
            // 点击编辑后紧随其后让input框获取焦点
            // $nextTick在下一次DOM更新结束后执行器指定的回调
            // 这里没有$nextTick的话,DOM还没有更新,就执行了代码,却找不到DOM
            this.$nextTick(function(){
      
      
                this.$refs.inputTitle.focus()
            })
        },
        // 通过全局事件总线将修改后的数据传递到App组件处理
        handleBlur(todo,e){
      
      
            todo.isEdit=false;
            if(!e.target.value.trim()) return alert('输入不能为空!');
            this.$bus.$emit('updateTodo',todo.id,e.target.value);
        }
    }
    
}
</script>

上面的 $nextTick:
一个生命周期钩子
在下一次DOM更新结束后执行器指定的回调,没有$nextTick的话,DOM还没有更新,就执行了代码,却找不到DOM

在App中mounted钩子中加一个:

this.$bus.$on('updateTodo',this.updateTodo);

在App中methods增加一个事件触发回调updateTodo:

 // 编辑
   updateTodo(id,value){
    
    
         this.todos.forEach((todo)=>{
    
    
             if(todo.id===id){
    
    
                 todo.title=value
             }
         })
     },

在App中beforeDestroy钩子中加一个销毁程序:

this.$bus.$off('updateTodo');

这样就实现编辑功能了,但是视频中有一个刷新页面input框还存在的bug,这里个人是在在App中mounted钩子中加了一个遍历todos,解析页面前把isEdit改为false:

mounted(){
    
    
        // 解决刷新页面input框还存在的bug
        this.todos.forEach((todo)=>{
    
    
            if(todo.hasOwnProperty('isEdit')){
    
    
                todo.isEdit=false
            }
        })

9、过渡与动画

9.1 css3过渡与动画复习

①过渡
(1)过渡动画:从一个状态 渐渐的过渡到另一个状态,现在经常和:hover一起搭配使用

(2)语法:
transition:要过渡的属性 花费时间 运动曲线 何时开始;
多个属性都要加过渡的话这样写:
transition:要过渡的属性1 花费时间 运动曲线 何时开始,要过渡的属性1 花费时间 运动曲线 何时开始,...;

其中四个为

  • 属性:想要变化的css属性,宽度高度,背景颜色,内外边距等都可以,如果想要所有的属性都变化过渡,写一个all就可以
  • 花费时间:单位是 s(必须写单位)比如:0.5s
  • 运动曲线(可以省略):默认ease()
    运动曲线如下五个:
    在这里插入图片描述
  • 何时开始(可以省略):单位是秒(必须写单位),可以设置延迟触发时间,默认是0s

过渡口诀:谁(属性)做过渡给谁加

②动画
(1)动画:可通过设置多个节点来精确控制一个或一组动画,常用来实现复杂的动画效果。
相比过渡,动画可以实现更多变化,更多控制,连续自动播放等效果。

(2)动画的基本使用
第一步:先定义动画
用keyframes定义动画(类似定义类选择器)

@keyframes 动画名称{
    
    
	0%{
    
    
		width:100px;
	}
	100%{
    
    
		width:200px;
	}
}

0%是动画的开始,100%是动画的完成,这样的规则就是动画序列。
在@keyframes中规定某项css样式,就能有当前样式逐渐改为新样式的动画效果。
0%和100%也可以用from和to代替。

第二步:使用(调用)动画
animation-name:动画名称
animation-duration:持续时间来调用
在这里插入图片描述
调用时除了这两个必须的属性要写,还可以写许多其他属性:
在这里插入图片描述
简写:
animation:动画名称 持续时间 运动曲线 何时开始 播放次数 是否反方向 动画其实或者结束状态

9.2、vue中封装动画和过渡

① 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。

② 图示:在这里插入图片描述
③ 写法:
1.准备好样式:
元素进入的样式:
(1)v-enter:进入的起点
(2)v-enter-active:进入过程中
(3) v-enter-to:进入的终点
元素离开的样式:
(1) v-leave:离开的起点
(2) v-leave-active:离开过程中
(3) v-leave-to:离开的终点

2.使用<transition>包裹要过度的元素,并配置name属性:

   <transition name="hello">
   	<h1 v-show="isShow">你好啊!</h1>
   </transition>

在这里插入图片描述
3. 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

9.3、todos案例增加动画效果

操作都在MyItem中:
第一步:在style中定义一个简单的动画,并且写元素进入和出去的样式

    .todo-enter-active{
        animation: todo 1s;

    }
    .todo-leave-active{
        animation: todo 0.5s linear reverse;
    }
    @keyframes todo {
        from{
            transform: translateX(100%);
        }
        to{
            transform: translateX(0px);
        }
    }

第二步:在整个<li></li>外包裹一层<transition>

<transition name="todo" appear>
	<li>中的内容
</transition>

这样就可以实现一个简单的动画效果了。

四、Vue中的AJAX

1、 解决开发环境 Ajax 跨域问题

1.1、复习node.js
// 引入express框架
const express = require('express');
// 创建网站服务器
const app = express();
app.use((request, response, next) => {
    
    
    console.log("有人请求了服务器1");
    next();
})
app.get('/students', (request, response) => {
    
    
    const students = [
        {
    
     id: '001', name: 'tom', age: 18 },
        {
    
     id: '002', name: 'jerry', age: 38 },
        {
    
     id: '003', name: 'tony', age: 15 },
    ]
    response.send(students)
})

// 监测端口
app.listen(5000);
console.log('服务器1开启成功,请求学生信息地址为:http://localhost:5000/students');

通过写一个请求学生信息(server1.js)的服务后端,来复习一下node,汽车信息的服务端(server2.js)就把数据改一下即可。
更多node相关的内容可以移步到我做的node笔记。

1.2、Vue解决跨域问题

学过ajax的肯定都知道两种解决跨域问题的方法(更多ajax相关的内容可以移步到我做的ajax笔记):
(1)通过JSONP,JSONP(JSON with Padding)是一个非官方的跨域解决方案,只支持get请求
(2)CORS(Cross-Origin-Resource-Sharing),跨域资源共享。CORS是官方的跨域解决方案

而在Vue中借助vue-cli开启一个代理服务器就可以解决跨域问题。

例如在App.vue中要访问1.1中的server1服务器:

<template>
    <div class="todo-wrap">
      <button @click="getStudents">获取学生信息</button>
    </div>
</template>

<script>
import axios from 'axios'
    export default {
    
    
        name:'App',
        data() {
    
    
            return {
    
    
            
            }
        },
        methods:{
    
    
            getStudents(){
    
    
                axios.get('http://localhost:5000/students').then(
                    response=>{
    
    
                        console.log('请求成功了',response.data);
                    },
                    error=>{
    
    
                        console.log("请求失败了",error.message);
                    }
                )
            }
        }
    }
</script>

直接这样写就会报错(跨域问题):
在这里插入图片描述
vue中就通过开启一个代理服务器,来解决跨域问题,原理图如下:
在这里插入图片描述
那么在vue中如何开启代理服务器:
(1)第一种:
去vue官网–>生态系统–>Vue CLI–>配置参考–>vue.config.js中devServer.proxy
在这里插入图片描述
配置到自己的vue.config.js中(配置完记得重启脚手架):
在这里插入图片描述
改成需要请求的服务器地址。
再将App.vue中ajax请求的地址改成代理服务器的地址(8080端口)
这样就解决跨域问题了。
在这里插入图片描述
第一种方法优缺点:
优点:配置简单,请求资源时直接发给前端(8080)即可。
缺点:不能配置多个代理,不能灵活的控制请求是否走代理。

工作方式:
若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

(2)第二种:
上面哪一种方法一次只能配置一次代理,并且不能灵活控制到底走不走代理。
同样:去vue官网–>生态系统–>Vue CLI–>配置参考–>vue.config.js中devServer.proxy,在第一种下面:
在这里插入图片描述
'/api':请求前缀,匹配所有以 '/api’开头的请求路径
ws: true,:用于支持websocket
changeOrigin: true:默认值为true;设置为true时,服务器收到的请求头中的host为:localhost:5000;设置为false时,服务器收到的请求头中的host为:localhost:8080

下面就在App.vue中向1.1节的server1.js和server2.js中分别请求学生信息和汽车信息:

首先在vue.config.js配置好代理服务器:
在这里插入图片描述
通过pathRewrite: { '^/server1': '' },将请求前缀去掉

在App.vue中:

<template>
    <div class="todo-wrap">
      <button @click="getStudents">获取学生信息</button>
      <button @click="getCars">获取车辆信息</button>
    </div>
</template>

<script>
import axios from 'axios'
    export default {
      
      
        name:'App',
        data() {
      
      
            return {
      
      
            
            }
        },
        methods:{
      
      
            getStudents(){
      
      
                axios.get('http://localhost:8080/server1/students').then(
                    response=>{
      
      
                        console.log('请求成功了',response.data);
                    },
                    error=>{
      
      
                        console.log("请求失败了",error.message);
                    }
                )
            },
            getCars(){
      
      
                axios.get('http://localhost:8080/server2/cars').then(
                    response=>{
      
      
                        console.log('请求成功了',response.data);
                    },
                    error=>{
      
      
                        console.log("请求失败了",error.message);
                    }
                )
            }
        }
    }
</script>

请求的url就得加上请求前缀:
'http://localhost:8080/server1/students'
'http://localhost:8080/server2/cars'

第二种方法优缺点:
优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
缺点:配置略微繁琐,请求资源时必须加前缀。

2、 github 用户搜索案例

main.j文件:

// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'

// 关闭Vue的生产提示
Vue.config.productionTip = false;

// 创建vm
new Vue({
    
    
    el: '#app',
    render: h => h(App),
    beforeCreate() {
    
    
        Vue.prototype.$bus = this
    },
})

App.js文件:

<template>
    <div class="container">
        <Category title="美食" :listData="foods">
            <img src="https://seopic.699pic.com/photo/50074/8934.jpg_wh1200.jpg" alt="">
        </Category>
        <Category title="游戏" :listData="games">
            
        </Category>
        <Category title="电影" :listData="films"> 
        </Category>
    </div>
</template>

<script>
    import Category from './components/Category'
    export default {
      
      
        name:'App',
        data() {
      
      
            return {
      
      
                foods: ['火锅', '烧烤', '小龙虾'], 
                games: ['红色警戒', '穿越火线', '劲舞团'], 
                films: ['《教父》', '《拆弹专家》', '《你好,李焕英》']
            }
        },
        components:{
      
      
            Category
        }
    }
</script>
<style lang="css">
    .container{
      
      
        display: flex;
        justify-content: space-around;
    }
</style>

Search.vue组件:

<template>
    <section class="jumbotron">
        <h3 class="jumbotron-heading">Search Github Users</h3>
        <div>
            <input type="text" placeholder="enter the name you search" v-model="keyWord"/>&nbsp;
            <button @click="searchUsers">Search</button>
        </div>
    </section>
</template>

<script>
import axios from 'axios'
export default {
      
      
    name:'Search',
    data(){
      
      
        return {
      
      
            keyWord:''
        }
    },
    methods:{
      
      
        searchUsers(){
      
      
            // 请求前更新List数据
            this.$bus.$emit('updateListData',{
      
      
                        isFirst:false,
                        isLoading:true,
                        errMsg:'',
                        users:[]
                    })
            axios.get(`https://api.github.com/search/users?q=${ 
        this.keyWord}`).then(
                response=>{
      
      
                    // 请求成功后更新List的数据
                    this.$bus.$emit('updateListData',{
      
      
                        isLoading:false,
                        errMsg:'',
                        users:response.data.items
                    })
                },
                error=>{
      
      
                    console.log(error.message);
                    // 请求失败后更新List的数据
                    this.$bus.$emit('updateListData',{
      
      
                        isLoading:false,
                        errMsg:error.message,
                        users:[]
                    })
                }
            );
            
        }
    }
}
</script>

List.vue组件:

<template>
  <div class="row">
    <div class="card" v-for="user in info.users" :key="user.id">
        <a :href="user.html_url" target="_blank">
        <img :src="user.avatar_url" style='width: 100px'/>
        </a>
        <p class="card-text">{
   
   {user.login}}</p>
    </div>
    <h1 v-show="info.isFirst">欢迎使用!</h1>
    <h1 v-show="info.isLoading">加载中...</h1>
    <h1 v-show="info.errMsg">{
   
   {info.errMsg}}</h1>
</div>
</template>

<script>
export default {
      
      
    name:'List',
    data() {
      
      
        return {
      
      
            info:{
      
      
                isFirst:true,
                isLoading:false,
                errMsg:'',
                users:[]
            }
        }
    },
    mounted(){
      
      
        this.$bus.$on('updateListData',dataObj=>{
      
      
            this.info={
      
      ...this.info,...dataObj}
        })
    }
}
</script>

<style scoped>
    .album {
      
      
        min-height: 50rem; /* Can be removed; just added for demo purposes */
        padding-top: 3rem;
        padding-bottom: 3rem;
        background-color: #f7f7f7;
    }

    .card {
      
      
        float: left;
        width: 33.333%;
        padding: .75rem;
        margin-bottom: 2rem;
        border: 1px solid #efefef;
        text-align: center;
    }

    .card > img {
      
      
        margin-bottom: .75rem;
        border-radius: 100px;
    }

    .card-text {
      
      
        font-size: 85%;
    }
</style>

3、 vue 项目中常用的 2 个 Ajax 库

3.1、axios(官方推荐)

上面都是用的axios,了解更多可移步我之前写的ajax相关的笔记。

3.2、vue-resource(不再维护,不推荐)

了解即可:
第一步:安装npm i vue-resource
第二步:在main.js中引入:import vueResource from 'vue-resource'
第三步:在main.js中使用Vue.use(vueResource)
第四步:用的时候和axios一样,也是promise格式。把axios.get(url).then(response=>{},error=>{})改为this.$http.get(url).then(response=>{},error=>{})即可

4、 slot 插槽

插槽的作用:让父组件可以向子组件指定位置插入html结构。

插槽也是一种组件间通信的方式,适用于父组件===>子组件

插槽有三种分类:默认插槽、具名插槽、作用域插槽

分三节介绍下三种插槽的使用:

4.1、默认插槽

app.vue父组件中:

		<Category>
           <div>html结构1</div>
        </Category>

Category.vue子组件中:

        <template>
            <div>
               <!-- 定义插槽 -->
               <slot>插槽默认内容...</slot>
            </div>
        </template>

<slot>插槽默认内容...</slot>定义插槽

子组件中的<slot>插槽默认内容...</slot>就会被父组件中<div>html结构1</div>替换,如果父组件中没有<div>html结构1</div>就会使用<slot>插槽默认内容...</slot>中·的默认内容插槽默认内容...

4.2、具名插槽

app.vue父组件中:

        <Category>
            <template slot="center">
              <div>html结构1</div>
            </template>

            <template v-slot:footer>
               <div>html结构2</div>
            </template>
        </Category>

Category.vue子组件中:

        <template>
            <div>
               <!-- 定义插槽 -->
               <slot name="center">插槽默认内容...</slot>
               <slot name="footer">插槽默认内容...</slot>
            </div>
        </template>

把子组件中定义多个插槽,这就要给每个插槽取个名字,用name="xxx"属性给每个插槽取名字。

在父组件中给需要用到插槽的结构用slot="xxxx"v-slot:footer(这个只能用在template模板,前者所有结构都可以用)来确定将结构放在哪个插槽内。

4.3、作用域插槽

①理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。
例如,在父组件App.vue中:

<template>
    <div class="container">
        
        <Category title="游戏" >
            <!-- scope="gamesObj"接收插槽中的数据,封装在一个名字为gamesObj对象里 -->
            <!-- 可简化为scope={games} -->
            <template scope="gamesObj">
                <ul>
                    <li v-for="(item,index) in gamesObj.games" :key="index">{
   
   {item}}</li>
                </ul>
            </template>
        </Category>

        <Category title="游戏" >
            <template scope="gamesObj">
                <ol>
                    <li v-for="(item,index) in gamesObj.games" :key="index">{
   
   {item}}</li>
                </ol>
            </template>
        </Category>

        <Category title="游戏" >
            <template scope="gamesObj">
                <h4 v-for="(item,index) in gamesObj.games" :key="index">{
   
   {item}}</h4>
            </template>
        </Category>
    </div>
</template>

<script>
    import Category from './components/Category'
    export default {
      
      
        name:'App',
        
        components:{
      
      
            Category
        }
    }
</script>
<style lang="css">
    .container{
      
      
        display: flex;
        justify-content: space-around;
    }
</style>

在子组件Category.vue中:

<template>
  <div class="category"> 
      <h3>{
   
   {title}}分类</h3>
      <!-- 谁往插槽里插入结构,这个games数据就传给谁 并且可以传入多个值,例如<slot :games="games" msg="hello">-->
      <slot :games="games">我是一些默认的内容</slot>
  </div>
</template>

<script>
export default {
      
      
    name:'Category',
    props:['title'],
    data() {
      
      
            return {
      
      
                games: ['红色警戒', '穿越火线', '劲舞团','超级玛丽']
            }
        },
}
</script>

<style>
    .category{
      
      
        background-color: skyblue;
        width: 200px;
        height: 300px;
    }
    h3{
      
      
        text-align: center;
        background-color: pink;
    }
    img{
      
      
        width: 100%;
    }
</style>

这个案例就用到了作用域插槽,games数据在Category子组件中,但使用数据所遍历出来的结构由App父组件决定。

通过<slot :games="games">我是一些默认的内容</slot>将Category子组件中的games数据传出去,并且可以传入多个值,例如如果有msg数据,就可以这样写<slot :games="games" msg="hello">

App父组件中在编写Category子组件标签时,就可以在里面写入<template scope="gamesObj">结构</template>(结构必须由template包裹),scope="gamesObj"接收插槽中的数据,封装在一个名字为gamesObj对象里。当然scope="gamesObj"的最新写法为slot-scope="gamesObj"

五、vuex

1、 理解 vuex

①概念:专门在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是组件间一种通信的方式,且适用于任意组件间通信。

②什么时候使用Vuex
(1)多个组件依赖于同一状态
(2)来自不同组件的行为需要变更同一状态
总结来说就是组件间需要数据共享时使用Vuex。

③在这里写一个求和的实例,为下一节使用:
在这里插入图片描述
main.js:

// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 引入vuex
import Vuex from 'vuex'

// 关闭Vue的生产提示
Vue.config.productionTip = false;

// 使用vuex
Vue.use(Vuex)
// 创建vm
new Vue({
    
    
    el: '#app',
    render: h => h(App),
    beforeCreate() {
    
    
        Vue.prototype.$bus = this
    },
})

App.vue:

<template>
  <Count></Count>
</template>

<script>
import Count from './components/Count.vue'
export default {
      
      
    name:'App',
    components:{
      
      
        Count
    }
}
</script>

Count.vue:

<template>
  <div>
      <h3>当前求和为:{
   
   {sum}}</h3>
      <select v-model.number="n">
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
      </select>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
      <button @click="incrementOdd">当前求和为奇数再加</button>
      <button @click="incrementWait">等一等再加</button>
  </div>
</template>

<script>
export default {
      
      
    name:'Count',
    data() {
      
      
        return {
      
      
            n:1,
            sum:0
        }
    },
    methods:{
      
      
        increment(){
      
      
            this.sum+=this.n
        },
        decrement(){
      
      
            this.sum-=this.n
        },
        incrementOdd(){
      
      
            if(this.sum%2==1){
      
      
                this.sum+=this.n
            }
        },
        incrementWait(){
      
      
            setTimeout(()=>{
      
      
                this.sum+=this.n
            },1000)
        },
    }
}
</script>
<style>
    button{
      
      
        margin-left: 5px;
    }
</style>

2、 vuex 核心概念和 API

2.1、Vuex的工作原理图:

在这里插入图片描述
对原理图的初理解(最开始的了解,看一看了对后面的详细使用有很大帮助):
在这里插入图片描述

2.2、搭建Vuex环境:

①下载vuex插件,命令:npm i vuex

②在src目录下创建store/index.js:
在这里插入图片描述
并在index.js中写:

// 引入Vue核心库
import Vue from 'Vue'

// 引入Vuex
import Vuex from 'vuex'

// 应用Vuex插件
Vue.use(Vuex)

// 装备vuex中的三个对象
// 1.准备actions对象——响应组件中用户的动作
const actions = {
    
    }

// 2.准备mutations对象——修改state中的数据
const mutations = {
    
    }

// 3.准备state对象——保存具体的数据
const state = {
    
    }

// 创建并暴露store
export default new Vuex.Store({
    
    
    actions, //actions:actions的简写
    mutations,
    state
})

③在入口文件main.js中创建vm时传入store配置项

// 引入Vue
import Vue from 'vue'

// 引入App
import App from './App.vue'

//引入store
import store from './store'

// 关闭Vue的生产提示
Vue.config.productionTip = false;

// 创建vm
new Vue({
    
    
    el: '#app',
    render: h => h(App),
    store //store配置项,store:store的简写
})

Vue实例vm中和VueComponent实例vc上就都有store了:
在这里插入图片描述

2.3、vuex的基本使用

①Vuex中的两个API
(1)dispatch(‘action中的方法名’,数据)
组件向Actions对话

(2)commit(‘mutations中的方法名’,数据)
Actions向Mutations对话

②Vuex中的三个对象:
(1)actions

该对象包含多个响应用户动作的回调函数。

通过commit()来触发mutations中函数的调用,间接更新state

在组件中使用$store.dispatch('对应的action回调名')触发actions中的回调

可以包含异步代码(定时器,ajax等等)

示例代码:

const actions = {
    
    
 zzz(context,value){
    
    
 	context.commit('xxx',value)
 }
}

(2)mutations

该对象中包含多个直接更新state的方法

在 action 中使用:commit(‘对应的 mutations 方法名’) 触发能调用 mutations 中的方法

mutations 中方法的特点:不能写异步代码、只能单纯的操作 state

示例代码:

const mutations = {
    
    
	yyy(state,value){
    
    
	 	//更新state的某个属性
	}
}

(3)state

vuex的管理状态对象,唯一的。

示例代码:

const state = {
    
    
	xxx: initvalue
}

③vuex的基本使用

将上面的求和的示例来改为vuex版来了解vuex的基本使用:

Count.vue中的数据sum放到$store.state中

Count.vue文件:

<template>
  <div>
      <!-- 通过$store.state.sum读取到sum -->
      <h3>当前求和为:{
   
   {$store.state.sum}}</h3>
      <select v-model.number="n">
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
      </select>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
      <button @click="incrementOdd">当前求和为奇数再加</button>
      <button @click="incrementWait">等一等再加</button>
  </div>
</template>

<script>
export default {
      
      
    name:'Count',
    data() {
      
      
        return {
      
      
            n:1,
        }
    },
    methods:{
      
      
        increment(){
      
      
            // 没有逻辑和异步操作时可直接通过commit与Mutations对话
            this.$store.commit('JIA',this.n)
        },
        decrement(){
      
      
            // 没有逻辑和异步操作时可直接通过commit与Mutations对话
            this.$store.commit('JIAN',this.n)
        },
        incrementOdd(){
      
      
            this.$store.dispatch('jiaOdd',this.n)
        },
        incrementWait(){
      
      
            this.$store.dispatch('jiawait',this.n)
        },
    }
}
</script>

<style>
    button{
      
      
        margin-left: 5px;
    }
</style>

store/index.js文件:

// 引入Vue核心库
import Vue from 'vue'

// 引入Vuex
import Vuex from 'vuex'

// 应用Vuex插件
Vue.use(Vuex)

// 装备vuex中的三个对象
// 1.准备actions对象——响应组件中用户的动作
const actions = {
    
    
    // 第一个参数context就是一个简写版的store
    // 第二个参数value就是组件中传来的值
    jiaOdd(context, value) {
    
    
        if (context.state.sum % 2 == 1) {
    
    
            context.commit('JIA', value)
        }

    },
    jiawait(context, value) {
    
    
        setTimeout(() => {
    
    
            context.commit('JIA', value)
        }, 1000)
    }
}

// 2.准备mutations对象——修改state中的数据
const mutations = {
    
    
    // 第一个参数是vue封装get、set后的state
    // 第二个参数value是actions传来的值
    JIA(state, value) {
    
    
        console.log('mutations的JIA被调用了', state, value);
        state.sum += value
    },
    JIAN(state, value) {
    
    
        state.sum -= value
    }
}

// 3.准备state对象——保存具体的数据
const state = {
    
    
    sum: 0
}

// 创建并暴露store
export default new Vuex.Store({
    
    
    actions, //actions:actions的简写
    mutations,
    state
})

需要了解:
(1)初始化数据、配置actions、配置mutations
(2)组件中读取vuex中的数据:$store.state.sum
(3)没有逻辑和异步操作时可直接通过commit与Mutations对话this.$store.commit('JIA',this.n)
(4)有逻辑和异步操作时,先通过dispatch与actions对话this.$store.dispatch('jiaOdd',this.n),在再actions中写逻辑和异步操作,再通过commit与Mutations对话context.$store.commit('JIA',this.n)
这里的context是actions中函数的第一个参数,通过和2.2节的store的图对比,这是一个简写版的store,里面有几个store中的参数和方法,如下图:
在这里插入图片描述

2.4、getters配置项

从前面学习后可以了解到,store中有三个必写的配置项:actions,mutations,state
在这里插入图片描述
这节的getters也是一个配置项,只不过不是必须要配置的。

将上述的求和示例增加一个放大十倍的功能,来使用一下getters这个配置:在这里插入图片描述
先写好getters

// 准备getters--用于将state中的数据进行加工但不破坏
const getters = {
    
    
    bigSum(state) {
    
    
        return state.sum * 10
    }
}

配置到store中:

export default new Vuex.Store({
    
    
    actions, //actions:actions的简写
    mutations,
    state,
    getters
})

在组件中添加<h2>放大十倍:{ {$store.getters.bigSum}}</h2>即可。

getters和state的关系就像vue实例(组件)中的computed和data的关系

2.5、四个map方法的使用

mapState方法:用于帮助我们映射state中的数据为计算属性

computed: {
    
    
    //借助mapState生成计算属性:sum、school、subject(对象写法)
     ...mapState({
    
    sum:'sum',school:'school',subject:'subject'}),
         
    //借助mapState生成计算属性:sum、school、subject(数组写法)
    ...mapState(['sum','school','subject']),
},

mapGetters方法:用于帮助我们映射getters中的数据为计算属性

computed: {
    
    
    //借助mapGetters生成计算属性:bigSum(对象写法)
    ...mapGetters({
    
    bigSum:'bigSum'}),

    //借助mapGetters生成计算属性:bigSum(数组写法)
    ...mapGetters(['bigSum'])
},

mapActions方法:用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

methods:{
    
    
    //靠mapActions生成:incrementOdd、incrementWait(对象形式)
    ...mapActions({
    
    incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

    //靠mapActions生成:incrementOdd、incrementWait(数组形式)
    ...mapActions(['jiaOdd','jiaWait'])
}

mapMutations方法:用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数

methods:{
    
    
    //靠mapActions生成:increment、decrement(对象形式)
    ...mapMutations({
    
    increment:'JIA',decrement:'JIAN'}),
    
    //靠mapMutations生成:JIA、JIAN(对象形式)
    ...mapMutations(['JIA','JIAN']),
}

备注:
(1)记得引入import {mapActions, mapGetters, mapMutations, mapState} from 'vuex'
(2)数组的写法要求模板中的名字和调用的名字一致
(3)mapActions与mapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象。

将前面的求和示例改为map方法:
(1)数据用mapState和mapGetters映射为计算属性:在这里插入图片描述
模板中就可以直接简写了:
在这里插入图片描述
(2)将methods中的方法改为用mapActions和mapMutations来与actions和mutations对话:

 methods:{
    
    
        ...mapMutations({
    
    increment:'JIA',decrement:'JIAN'}),
        ...mapActions({
    
    incrementOdd:'jiaOdd',incrementWait:'jiawait'})
    },

模板中记得传递参数:

在这里插入图片描述

2.6、多组件共享数据

经过上面通过一个组件与vuex直接对话访问数据后,下面再加一个组件实现多个组件共享vuex中的数据:

将前面的求和案例添加Person一个组件,vuex中有sum和personList两个数据,下面Count组件和Person组件都可以访问到sum和personList数据
在这里插入图片描述
增加的Person组件代码如下:

<template>
  <div>
      <h1>人员列表</h1>
      <input type="text" placeholder="请输入名字" v-model="name">
      <button @click="add">添加</button>
      <ul>
          <li v-for="person in personList" :key='person.id'>{
   
   {person.name}}</li>
      </ul>
      <h3 style="color:red">Count组件中求和为:{
   
   {sum}}</h3>
  </div>
</template>

<script>
import {
      
      mapState} from 'vuex'
import {
      
      nanoid} from 'nanoid'
export default {
      
      
    name:'Person',
    data(){
      
      
        return {
      
      name:''}
    },
    methods:{
      
      
        add(){
      
      
            const personObj ={
      
      
                id:nanoid(),name:this.name
            }
            this.name=''
            this.$store.commit('ADD_PERSON',personObj)
        }
    },
    computed:{
      
      
        ...mapState({
      
      sum:'sum',personList:'personList'})
    }
}
</script>

Count组件增加的代码:
div中

<h3 style="color:red">Person组件的总人数是:{
   
   {personList.length}}</h3>

computed中

//访问到store中的数据
...mapState({
    
    sum:'sum',personList:'personList'}),

store/index.js增加的代码:
mutations中

ADD_PERSON(state, value) {
    
    
        state.personList.unshift(value)
    }

state中

    personList: [
        {
    
     id: '001', name: '张三' }
2.7、模块化+命名空间

①理解:
将index.js中的actions、mutations、state、getters中的各种方法、各种数据按一定要求分类

②目的:
让代码更好维护,让多种数据分类更加明确

③开启命名空间前的index.js
在这里插入图片描述
在这里插入图片描述
开启后的index.js:

// 将store.js分为两类:
// 1、countAbout
const countAbout={
    
    
    namespace:true,//开启命名空间
    actions:{
    
    
        jiaOdd(context, value) {
    
    
            if (context.state.sum % 2 == 1) {
    
    
                context.commit('JIA', value)
            }
    
        },
        jiawait(context, value) {
    
    
            setTimeout(() => {
    
    
                context.commit('JIA', value)
            }, 1000)
        }
    },
    mutations:{
    
    
        JIA(state, value) {
    
    
            console.log('mutations的JIA被调用了', state, value);
            state.sum += value
        },
        JIAN(state, value) {
    
    
            state.sum -= value
        },
    },
    state:{
    
    
        sum: 0,
    },
    getters:{
    
    
        bigSum(state) {
    
    
            return state.sum * 10
        }
    }
}
// 2、personAbout
const personAbout={
    
    
    namespace:true,//开启命名空间
    actions:{
    
    },
    mutations:{
    
    
        ADD_PERSON(state, value) {
    
    
            state.personList.unshift(value)
        }
    },
    state:{
    
    
        personList: [
            {
    
     id: '001', name: '张三' }
        ]
    },
    getters:{
    
    }
}


// 创建并暴露store
export default new Vuex.Store({
    
    
    modules:{
    
    
        countAbout,
        personAbout
    }
})

④开启命名空间后,组件中读取state数据:

//方式一:自己直接读取
this.$store.state.personAbout.list
//方式二:借助mapState读取:
...mapState('countAbout',['sum','school','subject']),

⑤开启命名空间后,组件中读取getters数据:

//方式一:自己直接读取
this.$store.getters['personAbout/firstPersonName']
//方式二:借助mapGetters读取:
...mapGetters('countAbout',['bigSum'])

⑥开启命名空间后,组件中调用dispatch

//方式一:自己直接dispatch
this.$store.dispatch('personAbout/addPersonWang',person)
//方式二:借助mapActions:
...mapActions('countAbout',{
    
    incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

⑦开启命名空间后,组件中调用commit

//方式一:自己直接commit
this.$store.commit('personAbout/ADD_PERSON',person)
//方式二:借助mapMutations:
...mapMutations('countAbout',{
    
    increment:'JIA',decrement:'JIAN'}),

通过④⑤⑥⑦将求和案例的需要改的地方改一下,这里就不写了。

⑧开启模块化+命名空间,分好的countAbout、personAbout就可以写在各个文件中了,然后通过暴露和引入完成:
在这里插入图片描述
然后在index.js中引入即可:
在这里插入图片描述

六、vue-router

1、 相关理解

1.1、路由的理解

①路由
一个路由(route)就是一组映射关系(key-value),多个路由需要路由器(router)进行管理。
key 为路径, value 可能是 function 或 component

②前端路由
理解:value 是 component,用于展示页面内容。
工作过程:当浏览器的路径改变时, 对应的组件就会显示。

③后端路由
理解:value 是 function, 用于处理客户端提交的请求。
工作过程:服务器接收到一个请求时, 根据请求路径找到匹配的函数 来处理请求, 返回响应数据。

1.2、vue-router的理解

vue 的一个插件库,专门用来实现 SPA 应用

1.3、对 SPA 应用的理解

①单页 Web 应用(single page web application,SPA)。

②整个应用只有一个完整的页面。

③点击页面中的导航链接不会刷新页面,只会做页面的局部更新。

④数据需要通过 ajax 请求获取。

2、 基本路由

2.1、基本使用

①安装vue-router,命令:npm i vue-router

②应用插件:Vue.use(VueRouter)

③编写router配置项:

//引入VueRouter
import VueRouter from 'vue-router'
//引入Luyou 组件
import About from '../components/About'
import Home from '../components/Home'

//创建router实例对象,去管理一组一组的路由规则
const router = new VueRouter({
    
    
	routes:[
		{
    
    
			path:'/about',
			component:About
		},
		{
    
    
			path:'/home',
			component:Home
		}
	]
})

//暴露router
export default router

④实现切换(active-class可配置高亮样式)

<router-link active-class="active" to="/about">About</router-link>

⑤指定展示位置

<router-view></router-view>
2.2、几个注意点

①路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹。
②通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
③ 每个组件都有自己的$route属性,里面存储着自己的路由信息。
④整个应用只有一个router,可以通过组件的$router属性获取到。

2.3、路由案例

通过路由实现如下页面效果:
在这里插入图片描述
目录结构:
在这里插入图片描述
路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹
router/index.js(创建路由器):

// 该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
// 引入组件
import About from '../pages/About.vue'
import Home from '../pages/Home.vue'
export default new VueRouter({
    
    
    routes: [{
    
    
            path: '/about',
            component: About
        },
        {
    
    
            path: '/home',
            component: Home
        }
    ]
})

然后在main.js中:

// 引入Vue
import Vue from 'vue'

// 引入App
import App from './App.vue'

// 引入VueRouter
import VueRouter from 'vue-router'

// 引入路由器
import router from './router/index'

// 关闭Vue的生产提示
Vue.config.productionTip = false;

Vue.use(VueRouter)
    // 创建vm
new Vue({
    
    
    el: '#app',
    render: h => h(App),
    router: router
})

在App.js中:

<template>
   <div>
    <div class="row">
      <div class="col-xs-offset-2 col-xs-8">
        <Banner></Banner>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-2 col-xs-offset-2">
        <div class="list-group">
          <!-- 原始HTML中使用a标签实现页面的跳转 路由中用router-link,active-class可配置高亮样式-->
          <router-link class="list-group-item" active-class="active" to="/about">About</router-link>
          <router-link class="list-group-item" active-class="active" to="/home">Home</router-link>
        </div>
      </div>
      <div class="col-xs-6">
        <div class="panel">
          <div class="panel-body">
            <router-view></router-view>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// 路由组件存放在pages文件夹,引入时注意路径
import About from './pages/About.vue'
import Home from './pages/Home.vue'
import Banner from './components/Banner.vue'
export default {
    
    
    name:'App',
    components:{
    
    
      About,
      Home,
      Banner 
    }
}
</script>

其他组件就不再写了,应该简简单单不成问题。

3、 嵌套(多级)路由

3.1、使用规则

①配置路由规则,使用children配置项:

routes:[
	{
    
    
		path:'/about',
		component:About,
	},
	{
    
    
		path:'/home',
		component:Home,
		children:[ //通过children配置子级路由
			{
    
    
				path:'news', //此处一定不要写:/news
				component:News
			},
			{
    
    
				path:'message',//此处一定不要写:/message
				component:Message
			}
		]
	}
]

②跳转(要写完整路径):

<router-link to="/home/news">News</router-link>
3.2、将上面的路由案例加一个多级路由

在Home中加一个路由:
在这里插入图片描述

首先写好二级组件New.vue和Message.vue:
在这里插入图片描述

在router/index.js中配置多级路由规则:

// 该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
// 引入组件
import About from '../pages/About.vue'
import Home from '../pages/Home.vue'
import New from '../pages/New.vue'
import Message from '../pages/Message.vue'
export default new VueRouter({
    
    
    routes: [{
    
    
            path: '/about',
            component: About
        },
        {
    
    
            path: '/home',
            component: Home,
            children: [{
    
    
                    path: 'new',
                    component: New
                },
                {
    
    
                    path: 'message',
                    component: Message
                }
            ]
        }
    ]
})

需要在Home.vue中实现切换:

<template>
<div>
  <h2>我是Home的内容</h2>
    <ul class="nav nav-tabs">
      <li>
        <router-link class="list-group-item" active-class="active" to="/home/new">News</router-link>
      </li>
      <li>
        <router-link class="list-group-item" active-class="active" to="/home/message">Message</router-link>
      </li>
    </ul>
    <!-- 指定展示区 -->
    <router-view></router-view>
</div>
</template>
<script>
export default {
      
      
    name:'Home'
}
</script>

其实和前面的一级路由差不多,但要注意下面几点:

一定要注意:
(1)配置路由规则,一级路由routes配置项,path中要写‘/’,而使用二级路由children配置项时,path中一定不要写‘/’
(2)<router-link to="/home/news">一定要写完整路径,不能只是<router-link to="/news">

4、 路由传参

4.1、路由的query参数

将上一级路由的参数传给下一级,可以使用query传递。
例如
在这里插入图片描述
将上一级的
在这里插入图片描述
数据传递给下一级中的
在这里插入图片描述
就可以使用query传参。

如何使用:
①传递参数:

<!-- 1.跳转路由并携带query参数,to的字符串写法 -->
<router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{
   
   {m.title}}</router-link>
</router-link>

<!-- 2.跳转路由并携带query参数,to的对象写法 -->
<router-link :to="{
                path:'/home/message/detail',
                query:{
                    id:m.id,
                    title:m.title
                }
            }">
{
   
   {m.title}}
</router-link>

②接收参数:在接收的组件中用{ {$route.query.xxx}}接收

  <ul>
      <li>消息编号:{
   
   {$route.query.id}}</li>
      <li>消息标题:{
   
   {$route.query.title}}</li>
  </ul>
4.2、命名路由

①作用:可以简化路由的跳转。

②使用方法
(1)给路由命名:

{
    
    
	path:'/demo',
	component:Demo,
	children:[
		{
    
    
			path:'test',
			component:Test,
			children:[
				{
    
    
                    name:'hello', //给路由命名
					path:'welcome',
					component:Hello,
				}
			]
		}
	]
}

(2)命名后然后就可以简化跳转:

<!--简化前,需要写完整的路径 -->
<router-link to="/demo/test/welcome">跳转</router-link>

<!--简化后,直接通过名字跳转 -->
<router-link :to="{name:'hello'}">跳转</router-link>

<!--简化写法配合传递参数 -->
<router-link 
	:to="{
		name:'hello',
		query:{
		   id:666,
            title:'你好'
		}
	}"
>跳转</router-link>
4.3、路由的params参数

和4.1节的query参数一样,也是一种传参方法,将上面的例子由query改为params,对比学习

如何使用:
首先的在router/index.js中配置路由,声明params参数(query就不需要声明):

{
    
    
	path:'/home',
	component:Home,
	children:[
		{
    
    
			path:'news',
			component:News
		},
		{
    
    
			component:Message,
			children:[
				{
    
    
					// 命名路由
					name:'xiangqing',
					path:'detail/:id/:title', //使用占位符声明接收params参数
					component:Detail
				}
			]
		}
	]
}

①传递参数:

<!-- 1.跳转路由并携带params参数,to的字符串写法 -->
<router-link :to="`/home/message/detail/${m.id}/${m.title}`">{
   
   {m.title}}</router-link>

            <!-- 2.跳转路由并携带params参数,to的对象写法 -->
 <router-link :to="{
//用对象写法时,这里必须用路由命名,不能用path:'/home/message/detail',可能是匹配不到带有占位符的path了(query参数中就随意)
    name:'xiangqing',
    params:{
    	id:m.id,
        title:m.title
    }
 }">
{
   
   {m.title}}
</router-link>

②接收参数:在接收的组件中用{ {$route.query.xxx}}接收

  <ul>
      <li>消息编号:{
   
   {$route.params.id}}</li>
      <li>消息标题:{
   
   {$route.params.title}}</li>
  </ul>

特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!

4.4、路由的props配置

路由的props配置和组件的props配置相似,可以比较学习。

路由的props配置作用:让路由组件更方便的收到参数

路由的props配置 配置在路由下,有三种配置方法:

{
    
    
	name:'xiangqing',
	path:'detail/:id',
	component:Detail,

	//第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
	// props:{a:900}

	//第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
	// props:true
	
	//第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
	props(route){
    
    
		return {
    
    
			id:route.query.id,
			title:route.query.title
		}
	}
}

然后在接收参数的组件中配置一个props:
props:['id','title']就可以接收参数了

模板中就可以简写了:

  <ul>
      <li>消息编号:{
   
   {id}}</li>
      <li>消息标题:{
   
   {title}}</li>
  </ul>
4.5、<router-link>的replace属性

① 作用:控制路由跳转时操作浏览器历史记录的模式
②浏览器的历史记录有两种写入方式:分别为pushreplacepush是追加历史记录,replace是替换当前记录。路由跳转时候默认为push

③如何开启replace模式:<router-link replace .......>News</router-link>

5、 编程式路由导航

①作用:不借助<router-link>实现路由跳转,让路由跳转更加灵活。
<router-link>最后都是转化为<a>;用编程式路由导航,就可以用其他各种标签实现路由跳转,例如按钮等等。

使用方法:通过$router中的API实现
例如:
在这里插入图片描述
上图的九个路由导航都是通过按钮实现,这里就用到了编程式导航
代码:
在Message.vue中的<li>里增加两个按钮,并绑定点击事件:

 <button @click="pushShow(m)">push查看</button>
<button @click="replaceShow(m)">replace查看</button>
    methods:{
    
    
        pushShow(m){
    
    
            this.$router.push({
    
    
                name:'xiangqing',
                params:{
    
    
                    id:m.id,
                    title:m.title
                }
            })
        },
        replaceShow(m){
    
    
            this.$router.replace({
    
    
                name:'xiangqing',
                params:{
    
    
                    id:m.id,
                    title:m.title
                }
            })
        }
    }

在Banner.vue中增加三个按钮,并绑定事件:

    <button @click="back">后退</button>
    <button @click="forward">前进</button>
    <button @click="gogo">测试go</button>
methods:{
    
    
      back(){
    
    
        this.$router.back();
      },
      forward(){
    
    
        this.$router.forward();
      },
      gogo(){
    
    
        this.$router.go(-2);
      },
    }

就实现了页面的效果

总结编程式导航用到的$router中的相关API:

//$router的API
//push
this.$router.push({
    
    
	name:'xiangqing',
		params:{
    
    
			id:xxx,
			title:xxx
		}
})
//replace
this.$router.replace({
    
    
	name:'xiangqing',
		params:{
    
    
			id:xxx,
			title:xxx
		}
})
//forward
this.$router.forward() //前进
//back
this.$router.back() //后退
//go
this.$router.go() //可前进也可后退,例如参数是-2,就回退两下,是2就前进两下

6、缓存路由组件

我们在New组件中增加三个输入框,然后我们输入一些内容(下面第一张图),接着切换到其他路由去,比如点Message,然后再点回来,会发现输入的内容都清空了(下面第二张图)
在这里插入图片描述
在这里插入图片描述
这是因为前面2.2节讲过,通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。

我们就可以给<router-view></router-view>添加:

<keep-alive include="New"> 
    <router-view></router-view>
</keep-alive>

让不展示的路由组件保持挂载,不被销毁。(作用)

注意:
1、上面的include="New"中写的是组件名
2、如果要让多个组件都保持挂载include中可以先在前面加:再写成数组,再在数组中写要挂载的组件名::include=['New','组件名2','组件名3'...] 3、如果不写include```,那就全部都挂载

7、两个新的生命周期钩子

① 作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。
② 具体名字:

  1. activated路由组件被激活时触发。
  2. deactivated路由组件失活时触发。

例如在一个组件中的生命周期钩子mounted中添加一个定时器,并且通过include切换会挂载,那么切换组件时,定时器还会继续工作,定时器不会走生命周期钩子beforeDestroy进行销毁,影响了性能。此时就可以把定时器写在 activated中,销毁定时器写在deactivated中,就可以实现。

8、路由守卫

作用:对路由进行权限控制
分类:全局守卫、独享守卫、组件内守卫

8.1、全局前置路由守卫

全局前置路由守卫:初始化时执行、每次路由切换前执行

用到router中的beforeEach:
router.beforeEach((to, from, next) => {})
在这里插入图片描述
里面是一个回调函数,参数有to,from,next
to和from是对象:表示来自哪,去往哪
在这里插入图片描述
在这里插入图片描述
next是一个方法,表示放行。

在本地存储加一个school:atguigu,通过前置路由守卫来判断是否放行

//全局前置守卫:初始化时执行、每次路由切换前执行
router.beforeEach((to,from,next)=>{
    
    
	console.log('beforeEach',to,from)
	if(to.meta.isAuth){
    
     //判断当前路由是否需要进行权限控制
		if(localStorage.getItem('school') === 'atguigu'){
    
     //权限控制的具体规则
			next() //放行
		}else{
    
    
			alert('暂无权限查看')
			// next({name:'guanyu'})
		}
	}else{
    
    
		next() //放行
	}
})

to.meta.isAuth这个要在路由中配置meta,如果要进行权限控制,就写一个true的值。如下:
在这里插入图片描述

8.2、全局后置路由守卫

全局后置路由守卫:初始化时执行、每次路由切换后执行

用到router中的afterEach:
router.afterEach((to, from) => {})
和前置路由守卫差不多,只是路由以及切换了,就没有next,不需要放行。
用来给切换路由后添加一个标题是否方便(不过前置路由也可以实现,麻烦一点):

//全局后置守卫:初始化时执行、每次路由切换后执行
router.afterEach((to,from)=>{
    
    
	console.log('afterEach',to,from)
	if(to.meta.title){
    
     
		document.title = to.meta.title //修改网页的title
	}else{
    
    
		document.title = 'vue_test'
	}
})

当然,要给每个路由给一个title,和前置路由的to.meta.isAuth一样要在路由中配置的meta中添加标题。例如
在这里插入图片描述

8.3、独享路由守卫

对单个路由进行控制,就可以用独享路由守卫,在路由内写beforeEnter,全局和前置路由用法一样,只是对写的路由生效。

独享路由守卫没有后置路由守卫

下面将new路由进行控制,注释之前的全局前置路由守卫后,只需要在new路由中写上beforeEnter相关内容即可:
在这里插入图片描述

8.4、组件内路由守卫

在组件中写路由控制,用法和前面的路由守卫也类似,这里只是写在组件中

//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
    
    
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
    
    
}

个人测试了一下在组件中两个新的生命周期钩子activateddeactivated和两个组件路由守卫beforRouteEnterbeforRouteLeave之间执行的先后顺序:
在New.vue中测试
在这里插入图片描述
当切入到New和离开New时,控制台打印如下:

在这里插入图片描述
也就是说先后顺序为:beforRouteEnteractivatedbeforRouteLeavedeactivated
按常理也能理解,先允许进来才被激活,离开了才失活。

8.5、两种工作模式:hash模式和history模式

路由器默认是hash模式,可以更改为history模式,通过配置项mode修改:在这里插入图片描述

① 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。

② hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。

③ hash模式:

(1) 地址中永远带着#号,不美观 。
(2)若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
(3) 兼容性较好。

④ history模式:

(1) 地址干净,美观 。
(2) 兼容性和hash模式相比略差。
(3) 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。

⑤项目打包,将写好的项目通过打包变成原始的html、css、js等静态资源。输入命令npm run build,就会生成dist文件夹,打包资源就都在里面了。

七、vue UI组件库

1、 移动端常用 UI 组件库

① Vant https://youzan.github.io/vant
② Cube UI https://didi.github.io/cube-ui
③ Mint UI http://mint-ui.github.io

2、 PC 端常用 UI 组件库

①Element UI https://element.eleme.cn
②IView UI https://www.iviewui.com

3、Element UI的使用

①全部引入

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

在这里插入图片描述
就可以使用想要的组件了,例如在App.vue中直接复制Button按钮相关的代码就可以使用:在这里插入图片描述
全部引入代码提及太大了,下面介绍按需引入

②按需引入
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component:
npm install babel-plugin-component -D

然后,将 babel.config.js修改为:

module.exports = {
    
    
    presets: [
        '@vue/cli-plugin-babel/preset',
        /* ********************** */
        ["@babel/preset-env", {
    
     "modules": false }]
    ],
    /* ********************** */
    plugins: [
        [
            "component",
            {
    
    
                "libraryName": "element-ui",
                "styleLibraryName": "theme-chalk"
            }
        ]
    ]
}

/* ********************** */下方都是修改内容,@babel/preset-env这个和官网中的不一样,需要用这个,要不然会报错。官网中应该是更新不及时。

接下来按需引入,比如我这里用到这些组件:
在这里插入图片描述
就要在main.js中按需引入import { Button, Row, Carousel, CarouselItem } from 'element-ui';,这个引入就是将组件标签名有短横线命名法改为了驼峰命名法。

最后在main.js中注册组件就可以了:

Vue.component(Button.name, Button);
Vue.component(Row.name, Row);
Vue.component(Carousel.name, Carousel);
Vue.component(CarouselItem.name, CarouselItem);

也可以自己取名字,将Button.name改为自己想要的名字也可以,标签中跟着改就可以了。

更多用法去官网了解即可。



写在最后:
经过二十多天的学习,对vue2的知识也算系统的过了一遍,留下了这一篇笔记,方便以后自己查漏补缺,也可以给有需要的读者学习。

由于是个人笔记,多多少少可能会有一些错误,若有发现,欢迎指
正。

本篇笔记是结合B站的尚硅谷张天禹老师的vue视频学习而写成的,看到本篇笔记的可前往b站进行学习,效果更佳。

后续还会接着学习vue3,写vue3的笔记~

猜你喜欢

转载自blog.csdn.net/weixin_55935744/article/details/121119943
今日推荐