Vue2进阶笔记
- 一、基础知识
 - 
- 1.1 computed计算属性
 - 1.2 watch监视属性
 - 1.3 动态绑定样式
 - 1.4 列表循环渲染 key的探讨
 - 1.5 列表过滤
 - 1.6 数据监视
 - 1.7 表单收集
 - 1.8 过滤器
 - 1.9 生命周期函数
 - 1.10 nextTick
 - 1.11 动画与过渡
 - 1.12 脚手架配置跨域代理
 
 - 二、组件化开发
 - 
- 2.1 演替与定义
 - 2.2 使用与注册
 - 2.3 VueComponent
 - 2.4 vue-cli脚手架
 - 2.5 main.js中的render
 - 2.6 ref属性
 - 2.7 props配置
 - 2.8 mixin混入
 - 2.9 插件
 - 2.10 scoped样式
 
 - 三、组件通信
 - 
- 3.1 父传子
 - 3.2 子传父
 - 3.3 全局事件总线
 - 3.4 消息订阅与发布(pubsub)
 - 3.5 插槽
 
 - 四、Vuex
 - 
- 4.1 概念
 - 4.2 环境搭建
 - 4.3 基本使用
 - 4.4 getters的使用
 - 4.5 四个map方法
 - 4.6 模块化
 
 - 五、vue-router
 - 
- 5.1 基本使用
 - 5.2 多级路由
 - 5.3 路由query参数
 - 5.4 路由params传参
 - 5.5 路由命名
 - 5.6 路由props配置
 - 5.7 链接跳转replace属性
 - 5.8 编程式路由导航
 - 5.9 缓存路由组件
 - 5.10 路由组件生命周期函数
 - 5.11 路由守卫
 - 5.12 hash与history工作模式
 - 5.13 meta元数据
 
 
Vue2进阶笔记,受益于尚硅谷天禹老师课程,在此表示感谢,全文共三万余字。
一、基础知识
tips:
 1、所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
 2、所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数、Promise的回调函数等),最好写成箭头函数这样this的指向才是vm或组件实例对象。
1.1 computed计算属性
在computed中,可以定义一些属性,即计算属性。计算属性具有缓存功能,相比较methods效率更高。
 计算属性本质是方法,只是在使用这些计算属性的时候,把他们的名称直接当作属性来使用,并不会把计算属性当作方法去调用,不需要加小括号()调用。
 计算属性的求值结果会被缓存起来,方便下次直接使用(多次调用只要内部数据不改变就不会重新求值,改变了也只会计算一次,虽然有多个地方引用此属性)。getter方法内部无论如何都要return出去一个值。
    <body>
        <div id="app">
            <h1>计算属性:computed的getter/setter</h1> <br/>
            姓:<input type="text" v-model="firstName"> <br/>
        	名:<input type="text" v-model="lastName"> <br/>
        	全名:<input type="text" v-model="fullName">>
        </div>
        <script>
            var app = new Vue({
                el:"#app",
                data:{
                firstName:"zhang",
                lastName:"san",
                },
                computed: {
                  /* 完全写法,可读可修改
                    fullName:{
                        get:function(){
                             return this.firstName + "-" + this.lastName
                        },
                        set:function(value){
                            var list = value.split(' ');
                            this.firstName=list[0]
                            this.lastName=list[1]
                        }
                    }
                  */
                  // 只读不修改,只有getter没有setter,简写
                  fullName(){
                  return this.firstName + "-" + this.lastName
                  }
               },
            });
        </script>
    </body>
注意:计算属性可以传参,但是不能直接传参。
但是在实际开发中我们会更多地使用计算属性,因为计算属性会进行缓存,多次使用时,计算属性只会调用一次。
 <p>班级:{{ getClassName(item.faceClass) }}</p>
// ...
// 正确写法 返回的是一个函数
computed: {
    getClassName() {
      return function (classId) {
        const element = this.classList;
        for (let index = 0; index < element.length; index++) {
          if (element[index].classId == classId) {
            return element[index].className;
          }
        }
      };
    },
  },
如果直接传参则会提示错误:>TypeError: songerName is not a function
// 错误写法
computed: {
    songName(classId) {
       const element = this.classList;
        for (let index = 0; index < element.length; index++) {
          if (element[index].classId == classId) {
            return element[index].className;
          }
        }
    }
}
1.2 watch监视属性
Watch概述
     一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。
 深度监视:
 (1). Vue中的watch默认不监测对象内部值的改变(一层)。
 (2). 配置deep:true可以监测对象内部值改变(多层)。
 备注:
 (1). Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以!
 (2). 使用watch时根据数据的具体结构,决定是否采用深度监视。
简单的监听
<body>
<div id="app">
    <input type="text" v-model="num">
</div>
<script src="vue.js"></script>
<script>
   const vm =  new Vue({
        el: '#app',
        data: {
            num: ''
        },
        // 创建vue实例时就知道需要监视那个数据项,用这种方法
        watch: {
        	// 不考虑immediate和deep只有hander一个配置项时,可以简写
            num(newVal, oldVal) {
            // 监听 num 属性的数据变化
    		// 作用 : 只要 num 的值发生变化,这个方法就会被调用
    		// 第一个参数 : 新值
    		// 第二个参数 : 旧值,之前的值
                console.log('oldVal:',oldVal)
                console.log('newVal:',newVal)
            }		
        },
        // 正常写法 根据用户行为,方才知道那个数据项需要监视,用这种方法
        vm.$watch('num',{
          // 每个属性值发生变化就会调用这个函数
                handler(newVal, oldVal) {
                    console.log('oldVal:', oldVal)
                    console.log('newVal:', newVal)
                },
                // 立即处理 进入页面就触发
                immediate: true,
                // 深度监听 监视多级结构中所有属性的变化
                deep: true
          }
       })
		// 不考虑immediate和deep只有hander一个配置项时,可以简写
		 vm.$watch('num',function(newVal, oldVal) {
                    console.log('oldVal:', oldVal)
                    console.log('newVal:', newVal)
         })
</script>
</body>
immediate(立即处理 进入页面就触发)
 deep(深度监听)
     对象和数组都是引用类型,引用类型变量存的是地址,地址没有变,所以不会触发watch。这时我们需要进行深度监听,就需要加上一个属性 deep,值为 true。
<body>
<div id="app">
    <input type="button" value="更改名字" @click="change">
</div>
<script src="vue.js"></script>
<script>
    new Vue({
        el: '#app',
        data: {
            food: {
                id: 1,
                name: '冰激凌'
            }
        },
        methods: {
            change() {
                this.food.name = '棒棒糖'
            }
        },
        watch: {
        	// 完整写法
        	// 第一种方式:监听整个对象,每个属性值的变化都会执行handler
        	// 注意:属性值发生变化后,handler执行后获取的 newVal 值和 oldVal 值是一样的
            food: {
                // 每个属性值发生变化就会调用这个函数
                handler(newVal, oldVal) {
                    console.log('oldVal:', oldVal)
                    console.log('newVal:', newVal)
                },
                // 立即处理 进入页面就触发
                immediate: true,
                // 深度监听 监视多级结构中所有属性的变化
                deep: true
            },
            // 第二种方式:监听对象的某个属性,被监听的属性值发生变化就会执行函数
            // 函数执行后,获取的 newVal 值和 oldVal 值不一样
            'food.name'(newVal, oldVal) {
                console.log('oldVal:', oldVal)   // 冰激凌
                console.log('newVal:', newVal)   // 棒棒糖
            }
        }
    })
</script>
</body>
Watch和computed的区别
1、 computed支持缓存,只有依赖数据发生改变,才会重新进行计算;而watch不支持缓存,数据变,直接会触发相应的操作。
 2、computed不支持异步,当computed内有异步操作时无效,无法监听数据的变化,而watch支持异步。
 3、computed属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值;而watch监听的函数接收两个参数,第一个参数是最新的值,第二个参数是输入之前的值。
 4、如果一个属性是由其它属性计算而来的,这个属性依赖其它属性,多对一或者一对一,一般用computed;而当一个属性发生变化时,需要执行对应的操作,一对多,一般用watch。
1.3 动态绑定样式
<!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>绑定样式</title>
    <!-- 引入Vue -->
    <script type="text/javascript" src="../js/vue.js"></script>
    <style>
        .basic{
            width: 400px;
            height: 200px;
            border: 2px solid cornflowerblue;
        }
        .normal{
            background-color: lightsteelblue;
        }
        .happy{
            background-color: cornflowerblue;
        }
        .sad{
            background-color: chartreuse;
        }
        .test1{
            font-size: 20px;
            text-align: center;
        }
        .test2{
            border-radius: 10px;
        }
        .test3{
            font-style: inherit;
            background-color: red;
        }
    </style>
</head>
<body>
    <!-- 准备好一个容器 -->
    <div id="root" >
        <!-- 绑定class样式,字符串写法。适用于:样式的类名不确定,需要动态绑定 -->
        <div class="basic" :class="mood" @click="changeMood">{{name}}></div><br><br>
        <!-- 绑定class样式,数组写法。适用于:要绑定的个数不确定,名字也不确定 -->
        <div class="basic" :class="arr" >{{name}}></div><br><br>
        <!-- 绑定class样式,对象写法。适用于:要绑定的个数不确定,名字也不确定 -->
        <div class="basic" :class="classObj" >{{name}}></div><br><br>
        <!-- 绑定style样式,对象写法。 -->
        <div class="basic" :style="styleObj" >{{name}}></div><br><br>
		<!-- 绑定style样式,数组写法,使用较少。 -->
        <div class="basic" :style="styleArr" >{{name}}></div><br><br>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip =  false // 阻止 vue 在启动时生成生产提示
    new Vue({
        el:"#root",
        data:{
            name:"才疏学浅",
            mood:'normal',
            arr:['test1','test2','test3'],
            classObj:{
                test1:false,
                test2:false,
                test3:false
            },
            styleObj:{
            	// 这里的属性名要遵循驼峰命名规范 font-size --> fontSize
                fontSize: '40px',
                color: 'blue'
            },
            styleArr:[
	            {
	                fontSize: '40px',
	                color: 'blue'
	            },
	            {
	            	backgroundColor: 'red'
	            }
            ]
        },
        methods: {
            changeMood(){
                const arr = ['normal','happy','sad']
                this.mood = arr[Math.floor(Math.random()*3)]
            }
        },
    })
</script>
</html>
1.4 列表循环渲染 key的探讨
当数组数据有可能顺序被破坏时,采用数组index作为key值可能导致:输入框内容错乱、效率过低的情况。当不写key时,vue会自动将数组的index索引值作为key值进行deff虚拟DOM对比算法。
 
采用数组自带id属性,则不会出现这种情况。
 
面试题:react、vue中的key有什么作用?(key的内部原理)
 1、虚拟DOM中key的作用:
 key是虚拟DOM对象的标识,当状态中的数据发生变化时,Vue会根据【新数据】生成【新的虚拟DON】,随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
2、对比规则:
 (1). 旧虚拟DOM中找到了与新虚拟DOM相同的key:
 若虚拟DOM中内容没变,直接使用之前的真实DOM。
 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
 (2). 旧虚拟DOM中未找到与新虚拟DOM相同的key:
 创建新的真实DOM,随后渲染到到页面。
3、用index作为key可能会引发的问题:
 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 – > 界面效果没问题,但效率低。
 如果结构中还包含输入类的DOM:会产生错误DOM更新 – > 界面有问题。
4、开发中如何选择key? :
 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
1.5 列表过滤

 监视(侦听)属性实现列表过滤:
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!--引入Vue-->
  <script type="text/javascript" src="../js/vue.js"></script>
  <title></title>
</head>
<body>
  <!--准备好一个容器-->
  <div id="root">
    <input type="text" placeholder="请输入名字" v-model="keyWord">
    <ul>
      <li v-for="p in filPersons" :key="p.id">
        {{p.name}}-{{p.age}}--{{p.sex}}
      </li>
    </ul>
  </div>
  <script type="text/javascript">
    Vue.config.productionTip = false;
    new Vue({
      el: '#root',
      data: {
        keyWord: '',
        persons: [
          { id: '001', name: '马冬梅', age: 19, sex: '女' },
          { id: '002', name: '周冬雨', age: 20, sex: '女' },
          { id: '003', name: '周杰伦', age: 21, sex: '男' },
          { id: '003', name: '温兆伦', age: 22, sex: '男' }
        ],
        filPersons: []
      },
      watch: {
        keyWord: {
          immediate: true,
          handler(val) {
            this.filPersons = this.persons.filter((p) => {
              return p.name.indexOf(val) !== -1
            })
          }
        }
      }
    })
  </script>
</body>
</html>
用计算属性实现列表过滤:
computed:{
        filPersons(){
          return this.persons.filter((p)=>{
            return p.name.indexOf(this.keyWord)!==-1
          })
        }
      }
用计算属性实现过滤,同时需要列表排序:
computed:{
        filPersons(){
          const arr= this.persons.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
        }
      }
1.6 数据监视
Vue会监视data中所有层次的数据。通过setter实现监视,且在new Vue时就传入要监测的数据。
对象中后追加的属性,Vue默认不做响应式处理。如需给后添加的属性做响应式,请使用如下API:
Vue.set(target,prpertyName/index,value)
vm.$set(target,prpertyName/index,value)
通过包裹数组更新元素的方式实现监测数组中的数据,本质是:1. 调用原生对应的方法对数组进行更新。2. 重新解析模板,进而更新页面。
在Vue修改数组中的某个元素时一定要使用如下方法:
 1、使用API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
 2、使用 Vue.set或者vm.$set(this.$set)
注意:Vue.set和vm.$set不能给vm或vm的根数据对象添加属性。
 
1.7 表单收集
v-model默认收集的是表单的value值,这里有几个需要主要的点。
若<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
若<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值。
若<input type="checkbox"/>
  1、没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,布尔值)
  2、配置了input的value属性
      v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,布尔值)
      v-model的初始值是数组,那么收集的就是value组成的数组。
备注:v-model的三个修饰符:
 1、lazy:失去焦点后再收集数据。
 2、number:输入字符串转为有效的数字。
 3、trim:输入首尾空格过滤。
1.8 过滤器
定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
  // 语法:
  注册过滤器:Vue.filter(name,callback)或new Vue{filters:{}}
  使用过滤器:{{xxx | 过滤器名}} 或 v-bind:属性 = "xxx | 过滤器名"
备注:
 过滤器也可以接收额外参数,多个过滤器能够串联。
 并没有改变原本的数据,是产生新的对应的数据。
<!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>过滤器</title>
    <!-- 引入Vue -->
    <script type="text/javascript" src="../js/vue.js"></script>
    <script type="text/javascript" src="../js/dayjs.min.js"></script>
</head>
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
        <h2>显示格式化后的时间</h2>
        <!-- 计算属性实现 -->
        <h3>(计算属性实现)现在是{{fmtTime}}</h3>
        <!-- methods实现 -->
        <h3>(methods实现)现在是{{getfmtTime()}}</h3>
        <!-- 过滤器实现 -->
        <h3>(过滤器实现)现在是{{time | timeFormater}}</h3>
        <!-- 过滤器传参 -->
        <h3>(过滤器传参)现在的日期是{{time | timeFormater('YYYY年_MM月_DD日')}}</h3>
         <!-- 多个过滤器传参 -->
         <h3>(多个过滤器传参)今年是{{time | timeFormater('YYYY年_MM月_DD日') | mySlice}}</h3>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip =  false // 阻止 vue 在启动时生成生产提示
    // 全局过滤器
    Vue.filter('mySlice',function(value){
        return value.slice(0,5)
    })
    new Vue({
        el:'#root',
        data:{
            name:'才疏学浅的小缘同学',
            time: 1647417712099 
        },
        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='YYYY年-MM月-DD日 HH:mm:ss'){
                return dayjs(value).format(str)
            },
        }
    })
</script>
</html>
1.9 生命周期函数
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期的函数,这给了用户在不同阶段添加自己的代码的机会。
- beforeCreate(创建前)
 - created (创建后)
 - beforeMount (载入前)
 - mounted (载入后)
 - beforeUpdate (更新前)
 - updated (更新后)
 - beforeDestroy( 销毁前)
 - destroyed (销毁后)
 
Vue生命周期函数就是vue实例在某一个时间点会自动执行的函数
当Vue对象创建之前触发的函数(beforeCreate)
Vue对象创建完成触发的函数(Created)
当Vue对象开始挂载数据的时候触发的函数(beforeMount)
当Vue对象挂载数据的完成的时候触发的函数(Mounted)
当Vue对象中的data数据发生改变之前触发的函数 (beforeUpdate)
当Vue对象中的data数据发生改变完成触发的函数(Updated)
当Vue对象销毁之前触发的函数 (beforeDestroy)
当Vue对象销毁完成触发的函数(Destroy)

1.10 nextTick
- 语法:
this.$nextTick(回调函数) - 作用:在下一次DOM更新结束后执行其指定的回调。
 - 什么时候用:当改变数据后,要基于更新后的新的DOM进行某些操作时,要在nextTick所指定的回调函数中执行。
 
1.11 动画与过渡

指定过渡动画的步骤:
- 在目标元素外面包裹<transition name=“xxx”>
 - 为name名指定CSS样式
指定过渡样式:transition(css3的属性)
指定隐藏时的样式:opacity/或者其他(width等等) - 如果有多个元素需要过渡,则需要使用:
<<transition-group>,并且每个元素都要指定key值。 
<transition name="xxx">
	<h1>你好</h1>
</transition>	
- 其他的第三方库/animate.style,官网有文档,自行学习。
 
过渡的相关类名:
xxx-enter-active:指定显示的 transition
xxx-leave-active:指定隐藏的 transition
xxx-enter/xxx-leave-to:指定隐藏时的样式
<style>
    /*入场动画规则*/
    @keyframes boxenter {
        0% {
            transform: translateX(-100px);
        }
        100% {
            transform: translateX(0);
        }
    }
    /*出场动画规则*/
    @keyframes boxleave {
        0% {
            transform: translateX(0);
        }
        100% {
            transform: translateX(-100px);
        }
    }
    /*这里应用入场动画规则*/
    .v-enter-active {
        animation: boxenter 3s;
    }
    /*这里应用出场场动画规则*/
    .v-leave-active {
        animation: boxleave 3s;
    }
</style>
<body>
    <div id="app">
        <button @click="change">切换</button>
        <!-- 使用transition标签包装需要动画的元素 -->
        <transition>
            <mycomponent v-if="isShow"></mycomponent>
        </transition>
    </div>
    ...
</body>
<style>
	/* demo1 */
	/* 显示/隐藏的过渡效果 */
	.xxx-enter-active,.xxx-leave-active{
		transition: opacity 1s;
	}
	/* 隐藏时的样式 */
	.xxx-enter,.xxx-leave-to{
		opacity: 0;
	}
	/* demo2 */
	/* 显示的过渡效果 */
	.yyy-enter-active{
		transition: all 1s;
		/* transform: translateX(20px); */
	}
	/* 隐藏时的过渡效果 */
	.yyy-leave-active{
		transition: all 3s;
	}
	/* 隐藏时的样式 */
	.yyy-enter,.yyy-leave-active{
		opacity: 0;
		transform: translateX(20px);
	}
</style>
<body>
<div id="demo1">
	<button @click="isShow = !isShow">Toggle</button>
	<transition name="xxx">
		<p v-show="isShow">hello</p>
	</transition>
</div>
<div id="demo2">
	<button @click="isShow = !isShow">Toggle</button>
	<transition name="yyy">
		<p v-show="isShow">hello</p>
	</transition>
</div>
<script src="../js/vue.js" type="text/javascript" charset="utf-8"></script>
<script>
	new Vue({
		el:"#demo1",
		data:{
			isShow:true
		}
	})
	new Vue({
		el:"#demo2",
		data:{
			isShow:true
		}
	})
</script>
</body>
1.12 脚手架配置跨域代理
方法一:
 在vue.config.js中添加如下配置:
devServer:{
	proxy:'http://localhost:服务器端口'
}
说明:
- 优点:配置简单,请求资源时直接发给前端(8080)即可。
 - 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
 - 工作方式:若按照上述配置代理,当请求了前端不存在的资源(public目录下资源)时,那么该请求会转发给服务器(优先匹配前端资源)
 
方法二:
 编写vue.config.js配置具体代理规则:
module.exports = {
  devServer: {
    proxy: {
      "/api": {   // 匹配所有以'/api'开头的请求路径
        target: "http://localhost:5000",    // 代理目标的基础路径
        changeOrigin: true,     // 用于控制请求头中的host值
        pathRewrite: { "^/api": "" },
      },
      "/other": {         // 可以配置多个代理路径
        target: "http://localhost:5001",
        changeOrigin: true,
        pathRewrite: { "^/other": "" },
      },
    },
  },
};
/**
 *      changeOrigin设置为true时,服务器收到的请求头中的host为: localhost:5000
 *      changeOrigin设置为false时,服务器收到的请求头中的host为: localhost:8080
 *      changeorigin默认值为true
 */
说明:
- 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
 - 缺点:配置略微繁琐,请求资源时必须加前缀。
 
二、组件化开发
2.1 演替与定义
组件的定义:实现应用中局部功能代码和资源的集合。

 
 
2.2 使用与注册
组件化编码流程:
- 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
 - 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
 
(1)一个组件在用:放在组件自身即可。
(2)一些组件在用:放在他们共同的父组件上(状态提升)。- 实现交互:从绑定事件开始。
 
组件的使用分为:非单文件组件、单文件组件。区别是否将单个html文件解耦为多个vue文件。
标准化开发中,我们会创建一个 app 组件去管理(领导)下属的所有组件,最顶级的vm管理app。(一人之下万人之上)

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>几个注意点</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
	<!-- 
		几个注意点:
			1.关于组件名:
						一个单词组成:
									第一种写法(首字母小写):school
									第二种写法(首字母大写):School
						多个单词组成:
									第一种写法(kebab-case命名):my-school
									第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
						备注:
								(1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
								(2).可以使用name配置项指定组件在开发者工具中呈现的名字。
			2.关于组件标签:
						第一种写法:<school></school>
						第二种写法:<school/>
						备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
			3.一个简写方式:
						const school = Vue.extend(options) 可简写为:const school = options
	-->
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>{{msg}}</h1>
			<school></school>
		</div>
	</body>
	<script type="text/javascript">
		Vue.config.productionTip = false
		//定义组件
		const s = Vue.extend({
			name:'atguigu',
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			}
		})
		new Vue({
			el:'#root',
			data:{
				msg:'欢迎学习Vue!'
			},
			components:{
				school:s
			}
		})
	</script>
</html>
2.3 VueComponent
组件本质其实是一个名为VueComponent的构造函数,且不是由程序员来定义的,是由Vue.extend生成的。
- 当我们写出组件名作为标签时,Vue解析会自动帮我们创建该组件名的对象实例并且执行
new VueComponent(options)。 - 每次调用
Vue.extend返回的都是一个新的VueComponent学过后端的应该很清楚。 - 在组件中的this所指向的是 
VueComponent类似于java的动态绑定。 

 组件实例对象(vc)可以访问到Vue原型上的属性、方法。
 
2.4 vue-cli脚手架
脚手架文件结构
 更正:App.vue 文件名默认是可以更改的,只不过不推荐。
 
脚手架文件目录
// 当引入第三方公共css库,使用import导入存在报错(import会严格检查),可将css库放在public目录下,在index.html主页面通过link引入。
├── node_modules 
├── public   
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源  
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   └── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
└── package-lock.json: 包版本控制文件
报错信息:error Component name “School” should always be multi-word vue/multi-word-component-names
报错原因:自己在给组件命名时没有使用推荐的大驼峰或者’-'拼接单词,所以编译的时候报错,实际上是语法检测的问题
vue.config.js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  transpileDependencies: true,
  // 关闭语法检测
  lintOnSave: false,
});
为什么要写那么多版本的vue包,因为模板解析器占体积太大了,大概占整个Vue源码的1/3。
开发时没问题,但是在上线生产时, 这个模板解析器太大且没有必要打包,为了精简与优雅,尤雨溪给我们提供了不同阶段使用不同版本的Vue。
关于不同版本的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函数去指定具体内容
2.5 main.js中的render
为什么会用render不用template, 因为 默认引入的Vue =>
import Vue from 'vue'是残缺版的,完整版在vue/dist/vue这个里面包含模板解析器。
默认脚手架生成的main.js入口文件
import Vue from "vue";
import App from "./App.vue";
// 创建vm
new Vue({
  // 完成这样的功能:将App组件挂载到容器中。类似于 template:‘<App></App>’ 的作用,但这里不能使用template。
  render: (h) => h(App)
}).$mount("#app");
// 关闭Vue生产提示
// Vue.config.productionTip = false
实际上等同于下列写法:
import Vue from "vue";
import App from "./App.vue";
new Vue({
  el: "#app",
  // createElement是一个形参
  render(createElement) {
    return createElement(App);
  },
});
// Vue.config.productionTip = false
2.6 ref属性
ref是Vue提供的操作DOM的属性,相比于js中给标签添加id,在通过document.getElementById()获取DOM,可以直接获取子组件标签的实例对象。作用如下:
- 被用来给元素或子组件注册引用信息(id的替代者)。
 - 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)。
 - 使用方式:
打标识:<h1 ref="xxx">.....</h1>或<School ref="xxx"></School>
获取:this.$refs.xxx 
<template>
  <div>
    <h1 ref="title">nihao</h1>
    <School ref="school"></School>
    <Student></Student>
    <button @click="showDom">点我提示DOM</button>
  </div>
</template>
<script>
import School from './components/School.vue';
import Student from './components/Student.vue';
export default {
  components: {
    School, Student
  },
  methods: {
    showDom() {
      console.log(this.$refs);
    }
  }
}
</script>
<style>
</style>

2.7 props配置
props功能是让组件接收外部传过来的数据。props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告。注意:由于props的渲染等级高于data,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
props适用于:
- 父组件 ==> 子组件 通信
 - 子组件 ==> 父组件 通信 (要求父先给子一个函数)
 
注意:
- 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
 - props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
 
父组件App
<template>
  <div>
    <Student name="zs" sex="男" :age="19"></Student>
  </div>
</template>
子组件Student
<template>
    <div>
        <h2>{{ msg }}</h2>
        <h1>学生名称:{{ name }}</h1>
        <h1>学生性别:{{ sex }}</h1>
        <h1>学生年龄:{{ age }}</h1>
    </div>
</template>
<script>
export default {
	// 1、简单声明接收
    props: ['name', 'sex', 'age'],
	// 2、接收的同时对数据类型进行限制
	props:{
		name:String,
		age:Number,
		sex:String
	}
	// 3、接收的同时对数据类型限制+默认值指定+必要性限制
	props:{
		name:{
			type:String,	// name的类型必须是String
			required:true	// name值是必要的
		},
		age:{
			type:Number,	
			default:99	
		},
		name:{
			type:String,	
			required:true	
		}
	}
    data() {
        return {
            msg: 'hello,world'
        }
    }
}
</script>
<style>
</style>
2.8 mixin混入
mixin用于抽取公共的配置项为混入对象。通过import xxx from '...'导入 与 mixins:[xxx,...]将抽取出来的配置与自身配置进行整合。
 当配置出现冲突时,生命周期函数来者不拒,混合在前自身在后。除生命周期函数之外的配置以自身配置为主,覆盖掉混合配置。
 EX:Student.vue与School.vue中都有公共的showName()方法,可以抽取为mixin.js
// mixin.js
export const mixin = {
  // 	除了methods,还可以配置data、 mounted等等诸多配置。
  methods: {
    showName() {
      alert(this.name);
    },
  }
};
School.vue
<template>
    <div>
        <h1 @click="showName">学校名称:{{ name }}</h1>
        <h1>学校地址:{{ address }}</h1>
    </div>
</template>
<script>
// 1、引入
import { mixin } from '../mixin'
export default {
    data() {
        return {
            name: '希望小学',
            address: '北京北京'
        }
    },
    // 2、通过mixins配置
    mixins: [mixin]
}
</script>
<style>
</style>
Student.vue
<template>
    <div>
        <h2>{{ msg }}</h2>
        <h1 @click="showName">学生名称:{{ name }}</h1>
        <h1>学生性别:{{ sex }}</h1>
        <h1>学生年龄:{{ age }}</h1>
    </div>
</template>
<script>
import {mixin} from '../mixin'
export default {
    props: ['name', 'sex', 'age'],
    data() {
        return {
            msg: 'hello,world'
        }
    },
    mixins:[mixin]
}
</script>
<style>
</style>
上述引用方式属于局部引用,下列配置属于全局引用,作用在main.js,全局混入会给vm下的所有vc添加响应的配置。
 main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// 全局引用
import {mixin} from './mixin'
Vue.mixin(mixin)
new Vue({
  render: (h) => h(App),
}).$mount("#app");
2.9 插件
功能:用于增强Vue
 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。install(Vue,x,y,...)  与 main.js中先import导入,后Vue.use(plugins,1,2,...)。
plugins.js
export default {
  install(Vue) {
    // 定义全局过滤器
    Vue.filter("mySlice", function (value) {
      return value.slice(0, 4);
    });
    // 定义全局指令
    Vue.directive("fbind", {
      // ...
    });
    // 定义全局混入
    Vue.mixin({
      //...
    });
    // 给Vue原型上添加一个hello方法(vm和vc就都能用了)
    Vue.prototype.hello = () => {
      alert("hello");
    };
	 // 给Vue原型上添加一个属性(vm和vc就都能用了)
	 Vue.prototype.x = 100
  },
};
main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// 全局引用插件
import plugins from './plugins'
Vue.use(plugins)
new Vue({
  render: (h) => h(App),
}).$mount("#app");
2.10 scoped样式
作用:让样式在局部生效,防止冲突。写法:<style scoped>,编译时会打包成一个文件,可能出现类名一致样式冲突问题。
 <style lang='less' scoped> less相比css支持嵌套语句。使用less需要安装less-loader。
 当前vue-cli创建Vue项目,webpack版本是4.xx.xx时,此时不能直接安装最新的less-loader版本,因为最新的less-loader是在webpack5的基础上适配的,应该安装less-loader 6~7之间的版本。
// 两条语句学习一下
// 查看目前发布的版本
npm view xxxx versions
// 安装指定版本的工具
npm install xxxx@版本号
三、组件通信
3.1 父传子
父组件向子组件通信采用props属性,详解本文:2.7props配置 章节。
3.2 子传父
- 通过父组件给子组件
传递函数类型的props实现:子向父传值。 
App.vue
<template>
  <div>
    <School :getSchoolName="getSchoolName"></School>
  </div>
</template>
<script>
import School from './components/School.vue';
export default {
  components: {
    School
  },
  methods: {
    getSchoolName(name) {
      console.log('app接收到了', name);
    }
	// 接收多个参数,1、可以用ES6的新语法,将其他的参数自动包装成一个params数组 2、对个参数包装成对象
	 getSchoolName(name,...params) {
      console.log('app接收到了', name,params);
    }
  }
}
</script>
School.vue
<template>
    <div>
        <h1>学校名称:{{ name }}</h1>
        <h1>学校地址:{{ address }}</h1>
        <button @click ="sendSchoolName">点击</button>
    </div>
</template>
<script>
export default {
	// props接收
    props: ['getSchoolName'],
    data() {
        return {
            name: '希望小学',
            address: '北京北京'
        }
    },
    methods: {
        sendSchoolName() {
        	// 调用
            this.getSchoolName(this.name)
        }
    }
}
</script>
- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,
使用@或者v-on)。 
通过this.$destroy() 可以销毁一个组件实例,当组件实例被销毁时,当前组件所有的自定义事件全部不奏效。
App.vue
<template>
  <div>
     //通过父组件给子组件绑定一个自定义事件实现,v-on等于@ v-on:getName效果一致  @事件名=“回调名”,二者可以一致
    <School @getName="getSchoolName"></School>
	// 只触发一次
	<School @getName.once="getSchoolName"></School>
  </div>
</template>
<script>
import School from './components/School.vue';
export default {
  components: {
    School
  },
  methods: {
    getSchoolName(name) {
      console.log('app接收到了', name);
    }
  }
}
</script>
School.vue
<template>
    <div>
        <h1>学校名称:{{ name }}</h1>
        <h1>学校地址:{{ address }}</h1>
        <button @click="sendSchoolName">点击</button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            name: '希望小学',
            address: '北京北京'
        }
    },
    methods: {
        sendSchoolName() {
        	// 触发School组件实例身上的getName事件
            this.$emit('getName', this.name)
			// 解绑
			this.$off('getName')	// 解绑一个自定义事件
			this.$off(['getName','demo'])	// 解绑对个自定义事件
			this.$off()	// 解绑所有的自定义事件
        }
    }
}
</script>
- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,
使用ref)。 
vue默认情况下,给组件添加的事件都认为是自定义事件,如果需要让vue解析为原生的DOM事件,事件后可采用
native修饰,如:@click.native
App.vue
<template>
  <div>
    <School ref="school"></School>
  </div>
</template>
<script>
import School from './components/School.vue';
export default {
  components: {
    School
  },
  methods: {
    getSchoolName(name, ...params) {
      console.log('app接收到了', name, params);
    }
  },
  mounted(){
    // 当vc挂载完毕,通过this.$refs.school即可获取实例对象,当school实例的getName方法被触发,便会调用vc的getSchoolName方法
    // 这种方法的好处是可以加定时器,做一个延迟执行:比如延迟3秒触发子向父传递数据
    //  this.$refs.xxx.$on('自定义组件名',【这里的回调函数,谁调用‘getName’,this就是谁。不懂去看尚硅谷vue-P82】)
    // 所以这里的回调要么配置在methods中,要么使用箭头函数this.$refs.xxx.$on('xx',()=>{}),否则this指向会出问题。
    this.$refs.school.$on('getName',this.getSchoolName)
	// 只触发一次
    this.$refs.school.$once('getName',this.getSchoolName)
  }
}
</script>
School.vue
<template>
    <div>
        <h1>学校名称:{{ name }}</h1>
        <h1>学校地址:{{ address }}</h1>
        <button @click="sendSchoolName">点击</button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            name: '希望小学',
            address: '北京北京'
        }
    },
    methods: {
        sendSchoolName() {
            this.$emit('getName', this.name, 666, 888)
        }
    }
}
</script>
3.3 全局事件总线
一种组件间通信的方式,适用于任意组件间通信。
 
安装全局事件总线:main.js
new Vue({
  ......
  beforeCreate(){
    Vue.prototype.$bus = this   // 安装全局事件总线,$bus就是当前应用的vm
  },
  ......
}).$mount("#app");
使用事件总线:
- 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
 
methods:{
	demo(data){...}
}
......
mounted(){
	this.$bus.$on('xxx',this.demo)
}
......
beforeDestroy(){
	this.$bus.$off('xxx')
}
- 提供数据:
this.$bus.$emit('xxx',数据) 
最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
3.4 消息订阅与发布(pubsub)
一种组件间通信的方式,适用于任意组件间通信(适用各类前端框架,Vue使用较少)。
 使用步骤:
- 安装第三方库pubsub:
npm i pubsub-js - 在需要用的地方引入:
import pubsub from 'pubsub-js' - 接收数据:A组件想要接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
 
methods:{
	demo(data){...}
}
......
mounted(){
	this.pid = pubsub.subscribe('xxx',this.demo) 	// 订阅消息,会创建一个id
}
......
beforeDestroy(){
	pubsub.unsubscribe(this.pid)
}
- 提供数据:
pubsub.publish('xxx',数据) - 最好在beforeDestroy钩子中,用
pubsub.unsubscribe(pid)去取消订阅。 
3.5 插槽
- 
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件 。插槽就是一个占位符,存放父组件内部独有的html结构。
 - 
分类:默认插槽、具名插槽、作用域插槽
 
1、默认插槽:
<!-- 父组件中: -->
 <Category>
     <div>html结构1</div>
 </Category>
<!-- 子组件Category.vue中: -->
   <template>
      <div>
         <!-- 定义插槽 -->
         <slot>插槽默认内容...</slot>
      </div>
   </template>
 2、具名插槽:
<!-- 父组件中: -->
   <Category>
       <template slot="center">
         <div>html结构1</div>
       </template>
       <template v-slot:footer>
          <div>html结构2</div>
       </template>
   </Category>
<!-- 子组件中: -->
   <template>
       <div>
          <!-- 定义插槽 -->
          <slot name="center">插槽默认内容...</slot>
          <slot name="footer">插槽默认内容...</slot>
       </div>
   </template>
3、作用域插槽:
 数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定)。
父组件中:
	<Category>
		<template scope="scopeData">
			<!-- 生成的是ul列表 -->
			<ul>
				<li v-for="g in scopeData.games" :key="g">{{g}}</li>
			</ul>
		</template>
	</Category>
	<Category>
		<!-- 这里的 slot-scope 等价于 scope,写法不同 -->
		<template slot-scope="scopeData">
			<!-- 生成的是h4标题 -->
			<h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
		</template>
	</Category>
子组件中:
    <template>
        <div>
            <slot :games="games"></slot>
        </div>
    </template>
    <script>
        export default {
            name:'Category',
            props:['title'],
            //数据在子组件自身
            data() {
                return {
                    games:['红色警戒','穿越火线','劲舞团','超级玛丽']
                }
            },
        }
    </script>
四、Vuex
4.1 概念
在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
 
 
4.2 环境搭建
创建文件:src/store/index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {}
//创建并暴露store
export default new Vuex.Store({
	actions,
	mutations,
	state
})
在main.js中创建vm时传入store配置项
......
//引入store
import store from './store'
......
//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	store
})
4.3 基本使用
src/store/index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//引用Vuex
Vue.use(Vuex)
const actions = {
    //响应组件中加的动作
	jia(context,value){
		// console.log('actions中的jia被调用了',miniStore,value)
		context.commit('JIA',value)
	},
}
const mutations = {
    //执行加
	JIA(state,value){
		// console.log('mutations中的JIA被调用了',state,value)
		state.sum += value
	}
}
//初始化数据
const state = {
   sum:0
}
//创建并暴露store
export default new Vuex.Store({
	actions,
	mutations,
	state,
})
- 
组件中读取vuex中的数据:
$store.state.sum - 
组件中修改vuex中的数据:
$store.dispatch('action中的方法名',数据)或$store.commit('mutations中的方法名',数据)备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写
dispatch,直接编写commit 
4.4 getters的使用
- 
概念:当state中的数据需要经过加工后再使用时,可以使用getters加工,类似于computed计算属性。
 - 
在
src/store/index.js中追加getters配置 
......
const getters = {
	bigSum(state){
		return state.sum * 10
	}
}
//创建并暴露store
export default new Vuex.Store({
	......
	getters
})
- 组件中读取数据:
$store.getters.bigSum 
4.5 四个map方法
注意要用什么map方法必须先引入。
 比如需要用到mapState,需要 import { mapState } from "vuex";
- 
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']),
   }
备注:mapActions与mapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象。
4.6 模块化
- 目的:让代码更好维护,让多种数据分类更加明确。
 - 修改
src/store/index.js,必须开启namespaced命名空间。这里的countAbout、personAbout可以抽取成单独的js文件。 
   const countAbout = {
     namespaced:true,//开启命名空间
     state:{x:1},
     mutations: { ... },
     actions: { ... },
     getters: {
       bigSum(state){
          return state.sum * 10
       }
     }
   }
   const personAbout = {
     namespaced:true,//开启命名空间
     state:{ ... },
     mutations: { ... },
     actions: { ... }
   }
   const store = new Vuex.Store({
     modules: {
       countAbout,
       personAbout
     }
   })
- 开启命名空间后,组件中读取state数据。
 
   //方式一:自己直接读取
   this.$store.state.personAbout.list
   //方式二:借助mapState读取: 注意需要先引入  import { mapState } from "vuex";
   ...mapState('countAbout',['sum','school','subject']),
- 开启命名空间后,组件中读取getters数据。
 
   //方式一:自己直接读取,注意这里读取的层级关系与写法
   this.$store.getters['personAbout/firstPersonName']
   //方式二:借助mapGetters读取:注意需要先引入mapGetters
   ...mapGetters('countAbout',['bigSum'])
- 开启命名空间后,组件中调用dispatch。
 
   //方式一:自己直接dispatch,注意这里读取的层级关系与写法
   this.$store.dispatch('personAbout/addPersonWang',person)
   //方式二:借助mapActions:注意需要先引入mapActions 
   ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
- 开启命名空间后,组件中调用commit。
 
   //方式一:自己直接commit,注意这里读取的层级关系与写法
   this.$store.commit('personAbout/ADD_PERSON',person)
   //方式二:借助mapMutations:注意需要先引入mapMutations
   ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
   // 使用例如:@click='increment(var)' var为携带的参数
五、vue-router
- 理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
 - 前端路由:key是路径,value是组件。
 
5.1 基本使用
- 
安装vue-router,命令:
npm i vue-router - 
在main.js中应用插件:
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
		},
		//....
		// 重定向,在项目跑起来的时候,访问 / ,立刻让它定向到首页
		{
			path:'*',
			redirect:'/home'
		}
	]
})
//暴露router
export default router
- 实现切换(active-class可配置高亮样式):
 
<router-link active-class="active" to="/about">About</router-link>
- 指定展示位置:
 
  <router-view></router-view>
- 注意点:
 
- 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
 - 每个组件都有自己的
 $route属性,里面存储着自己的路由信息。- 整个应用只有一个router,可以通过组件的
 $router属性获取到。
- 路由组件与非路由组件的区别
 
- 路由组件一般放置在pages | views文件夹,非路由组件一般放置components文件夹中。
 - 路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用。
 - 注册完路由,不管路由路由组件、还是非路由组件身上都有$route、$router属性。
 
$route:一般获取路由信息	==>[路径、query、params等等]
$router:一般进行编程式导航进行路由跳转	==>[push|replace]
- 防止连续点击多次路由报错,需要重写push | replace方法。
 
// 1、若依写法:
// 防止连续点击多次路由报错
let routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
  return routerPush.call(this, location).catch((err) => err);
};
call与apply的区别
 相同点:都可以调用函数一次,都可以篡改函数的上下文一次。
 不同点:call传递参数用逗号隔开,apply方法执行传递数组。
// 2、尚硅谷写法:
// 防止连续点击多次路由报错
let originPush = Vue.prototype.push;
let originReplace = Vue.prototype.Replace;
// 第一个参数:告诉原来的push方法,往哪跳(传递那些参数)
// 第二个参数:成功的回调
// 第三个参数:失败的回调
Vue.prototype.push = function (localtion,resolve,reject){
	if(resolve && reject){
		originPush.call(this,localtion,resolve,reject);
	}else{
		originPush.call(this,localtion,()={},()={});
	}
}
Vue.prototype.replace = function (localtion,resolve,reject){
	if(resolve && reject){
		originReplace.call(this,localtion,resolve,reject);
	}else{
		originReplace.call(this,localtion,()={},()={});
	}
}
5.2 多级路由
- 配置路由规则,使用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>
5.3 路由query参数
- 传递参数:
 
//跳转并携带query参数,to的字符串写法
<router-link :to="/home/message/detail?id=666&title=你好">跳转</router-link>
// 跳转并携带query参数,to的对象写法
<router-link 
	:to="{
		path:'/home/message/detail',
		query:{
		   id:666,
            title:'你好'
		}
	}"
>跳转</router-link>
- 接收参数:
 
$route.query.id
$route.query.title
5.4 路由params传参
注意:路由传递参数的时候,对象的写法可以是name、path形式,但是,path这种写法不能与params参数一同使用。
// 这种path与params混合的写法,报错!
this.$router.push{
	path:'/search',
	params:{
		keyword:this.keyword	
	},
	query:{
		name:this.name	
	}
} 
如果路由要求传递params参数,但是不传递时,URL会有问题导致无法跳转。如需指定params参数可传可不传,在配置路由router/index.js时,在占位的后面加上一个问号即可。
{
	name:'xiangqing',
	path:'detail/:id?', //通过问号匹配参数可传可不传
	component:Detail
}
当params参数可传可不传时,如果传递的是空串,可能导致URL路径缺失,这里可以用undefined解决
// 跳转
this.$router.push{
	path:'/search',
	params:{
		id:'' || undefined    // 加一个undefined
	}
} 
// router/index.js
// ......
{
	name:'xiangqing',
	path:'detail/:id?', //通过问号匹配参数可传可不传
	component:Detail
}
- 配置路由,声明接收params参数。
 
{
	path:'/home',
	component:Home,
	children:[
		{
			path:'news',
			component:News
		},
		{
			component:Message,
			children:[
				{
					name:'xiangqing',
					path:'detail/:id/:title', //使用占位符声明接收params参数
					component:Detail
				}
			]
		}
	]
}
- 传递参数:
 
<!-- 跳转并携带params参数,to的字符串写法 -->
<router-link :to="/home/message/detail/666/你好">跳转</router-link>
<!-- 跳转并携带params参数,to的对象写法 -->
<router-link 
	:to="{
		name:'xiangqing',
		params:{
		   id:666,
            title:'你好'
		}
	}"
>跳转</router-link>
- 接收参数:
 
$route.params.id
$route.params.title
5.5 路由命名
作用:可以简化路由的跳转。
具体使用
{
	path:'/demo',
	component:Demo,
	children:[
		{
			path:'test',
			component:Test,
			children:[
				{
                      name:'hello' //给路由命名
					path:'welcome',
					component:Hello,
				}
			]
		}
	]
}
<!--简化前,需要写完整的路径 -->
<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>
5.6 路由props配置
作用:让路由组件更方便的收到参数。
{
	name:'xiangqing',
	path:'detail/:id',
	component:Detail,
	// 第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
	// props:{a:900}
	// 第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
	// 组件直接用props:['id','title']配置即可使用,但是只适用于params传参
	// props:true
	// 第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
	// 都适用,props中可以接收route对象,这里可以用结构赋值props({ query:{id,title} }){ return{ id,title } }
	props(route){
		return {
			id:route.query.id,
			title:route.query.title
		}
	}
}
5.7 链接跳转replace属性
- 作用:控制路由跳转时操作浏览器历史记录的模式。
 - 浏览器的历史记录有两种写入方式:分别为
push和replace,push是追加历史记录,replace是替换当前记录。路由跳转时候默认为push。 - 如何开启
replace模式:<router-link replace .......>News</router-link>。 
5.8 编程式路由导航
- 
作用:不借助
<router-link>实现路由跳转,让路由跳转更加灵活 - 
具体编码:
 
//$router的两个API
this.$router.push({
	name:'xiangqing',
		params:{
			id:xxx,
			title:xxx
		}
})
this.$router.replace({
	name:'xiangqing',
		params:{
			id:xxx,
			title:xxx
		}
})
this.$router.forward() //前进
this.$router.back() //后退
this.$router.go() //可前进也可后退
5.9 缓存路由组件
作用:让不展示的路由组件保持挂载,不被销毁。
// 这里的include属性可以接收数组形式,:include="['News','xxx组件名']",后面的是组件名。
<keep-alive include="News"> 
    <router-view></router-view>
</keep-alive>
5.10 路由组件生命周期函数
作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。
- 
activated路由组件被激活时触发。 - 
deactivated路由组件失活时触发。 
5.11 路由守卫
- 
作用:对路由进行权限控制。
 - 
分类:全局守卫、独享守卫、组件内守卫。
 - 
全局守卫,配置在
router/index.js文件内部下方。 
//......
//全局前置守卫:初始化时执行、每次路由切换前执行
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() //放行
	}
})
//全局后置守卫:初始化时执行、每次路由切换后执行
router.afterEach((to,from)=>{
	console.log('afterEach',to,from)
	if(to.meta.title){ 
		document.title = to.meta.title //修改网页的title
	}else{
		document.title = 'vue_test'
	}
})
- 独享守卫,配置到各个路由中。
 
//......
{
	name:'xiangqing',
	path:'detail/:id',
	component:Detail,
	beforeEnter(to,from,next){
		console.log('beforeEnter',to,from)
		if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
			if(localStorage.getItem('school') === 'atguigu'){
				next()
			}else{
				alert('暂无权限查看')
				// next({name:'guanyu'})
			}
		}else{
			next()
		}
	}
}
//......
- 组件内守卫,配置在xxx组件中。
 
//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
}
5.12 hash与history工作模式
- 
对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。localhost:5000/#/xxxx(hash值)
 - 
hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。
 - 
hash模式:
 
- 地址中永远带着#号,不美观 。
 - 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
 - 兼容性较好。
 
- history模式:
 
- 地址干净,美观 。
 - 兼容性和hash模式相比略差。
 - 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。
 
5.13 meta元数据
我们经常会在进入一个页面时判断是否已经登陆,经常会用到路由导航守卫router.beforeEach(to,from, next),一个两个页面还好,但是多的话,就会麻烦,并且路由还会嵌套。这时可以使用meta。
 在配置路由时,经常会用到path、name、component,还有一个就是meta 元数据,给每个路由添加一个自定义的meta对象,在meta对象中可以设置一些状态,来进行一些操作。经常用它来做登录校验。
{
      path: '/imgMove/:id',
      name: 'imgMove',
      meta: {
        requiresAuth: true
      },
      component: imgMove
    },
    { //作品页面
      path: '/work',
      name: 'work',
      meta: {
        canNotLogin: true
      },
      component: work
    },
我们需要校验判断item下面的meta对象的requiresAuth是不是true,就可以进行一些限制。
router.beforeEach((to, from, next) => {
  if (to.matched.some(function (item) {
    return item.requiresAuth
  })) {
    next('/login')
  } else 
    next()
})