文章目录

  • 一、vue官方脚手架
    • 1.初识vue
    • 2.使用vue的方式(2种)
    • 3.搭建vue开发环境
    • 4.vue的重要版本
    • 5.使用@vue/cli进行vue开发环境的构建
    • 6.vue基础知识
      • 1.数据(data)
      • 2.方法(methods)
      • 3.指令(order)
      • 4.todolist
  • 二、案例练习(自定义表单)
  • 三、vue组件
    • 1.vue组件的创建
    • 2.纯javaScript创建组件
    • 3.使用vue 单文件组件
    • 4.组件的组装
    • 5.动态组件
  • 四、组件通信
    • 1.父组件向子组件通信
    • 2.子组件向父组件通信
    • 3.拖拽组件的组装
    • 3.获取真实的dom节点
    • 4.实现自定义滚动条
  • 五、组件的生命周期
    • 1.vue生命周期
    • 2.练习
  • 六、重要配置项(计算属性,监听属性,组件缓存)
    • 1.计算属性
    • 2.监听属性
    • 3.组件缓存
  • 七、vue事件和指令修饰符
    • 1.事件对象
    • 2.事件修饰符
    • 3.通用修饰符
    • 4.键盘按键修饰符
    • 5.系统修饰键
    • 6.鼠标按钮修饰符
    • 7.v-model修饰符
  • 八、8、vue过渡动画
    • 1.具体如何使用呢
  • 九、vue中的ajax请求
    • 1.vue环境中配置代理
    • 2.封装ajax请求的方法
    • 3.ajajx在vue组件中的使用示例
  • 十、vue路由
    • 1.什么是路由
    • 2.vue路由的基本使用
    • 3.路由的嵌套
    • 4.路由实例的属性和方法
    • 5.动态路由
  • 十一、vue路由进阶
    • 1.命名路由
    • 2.命名视图
    • 3.重定向和别名
    • 4.路由组件传参
    • 5.路由模式
    • 6.路由元信息
  • 十二、导航守卫
    • 1.全局前置守卫
    • 2.路由独享的守卫
    • 3.组件内的守卫
    • 4.全局解析守卫
    • 5.全局后置钩子
    • 6.完整的导航解析流程
    • 7.收货地址(练习)
  • 十三、vue路由补充
    • 1.过渡动效
    • 2.单个路由的过渡
    • 3.基于路由的动态过渡
    • 4.滚动行为
  • 十四、插槽
    • 1.插槽的基本用法
    • 2.插槽的后备内容
    • 3.具名插槽和v-slot指令
    • 4.插槽通信
  • 十五、在组件上使用v-model
    • 1.v-model的原理
    • 2.实现自定义组件的v-model
  • 十六、认识vuex
    • 1.状态管理模式
    • 2.vuex的思想
    • 3.vuex的基本用法
    • 4.vuex关联视图
  • 十七、vuex的完整使用流程
    • 1.核心配置项
    • 2.vue-devtools
    • 3.安装
    • 4.使用
    • 5.插件
    • 6.严格模式
    • 十八、vuex进阶
    • 1.模块
    • 2.项目结构
    • 3.辅助函数

一、vue官方脚手架

1.初识vue

Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是, Vue 被设计为可以自底向
上逐层应用。 Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方
面,当与现代化的工具链以及各种支持类库结合使用时, Vue 也完全能够为复杂的单页应用提供驱动。

一个使用 vue 开发的简单程序的例子如下:

<template>
<div> {{msg}} </div>
</template><script> export default {data(){return {msg: 'Hello Vue!'}}
} </scrip>

最终页面的效果如下:

2.使用vue的方式(2种)

正如前面我们写的例子所示,大家看到了 vue 的写法跟我们传统的 html css js 不太一样,即使长
的很像。但是,把上述代码无论是放在 html 文件,或者是 js 文件中,都是无法运行的。因此,我们首
先要了解如何去使用 vue 。

大体上呢,我们可以分为两种方式,一种是直接 script 引入,另一种是基于 vue 中的单文件组件
的方式。

1、 script 直接引入(不推荐)
在你的 html 文件中加入如下代码:

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"> </script>

意思很明显,就是把 vue 当作一个第三方库来使用,通过 script 标签加载该文件,此时给我
们的注册了一个全局变量 Vue ,接下来就可以通过 Vue 来进行页面开发了。当然这种方式,我
们写的代码还是需要完全符合 html css js 的语法的,也就是说上述的代码不能使用。我们
如果还是要实现刚才的界面,那么就需要编写如下代码

<!--index.html--> <!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>vue-demo</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
</head> <body> <div id="app"> {{msg}} </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"> </script> <script> new Vue({ el: '#app',data: {msg: 'Hello Vue!' }}) </script> </body> </html>

在使用vue开发的时候,一般是不会使用这种方式,因为该方式很难让vue的组件化方式
得到良好的应用,所以这种方式,仅做了解即可,基于vue单文件组件的开发方式才是正 确的使用方式

2、 基于 vue 的单文件组件(推荐)

最开始我们写的那段代码和 html js 很像的代码,我们已经知道它不应该放在 html 文件也不
应该放在 js 文件中。那么它 应该放在什么类型的文件中呢?答案是 .vue 文件中。
后缀为 .vue 的文件格式是 vue 框架独创的文件格式,目的是更加方便快捷的编写 vue 组件(关
于组件我们在以后会详细解释,现在你只需要明白一个组件代表一段视图,而一段视图又包含
了 html css js 三部分的代码)。那么, vue 格式的规则也很简单分别在 、 、 这三个标签中编写对应的 html js css 代码即可。当然,在这中类型的文件中可以直接使用 vue 相关的功能。

<template>
<!--编写视图对应的html结构-->
</template>
<script>// 编写视图所对应的交互(js代码)</script>
<style>/*编写视图所对应的样式(css代码)*/ </style>

注意, .vue 格式的文件是不可以直接被加载运行的,想要执行vue文件,并得到最终的
结果,我们需要对vue文件进行编译。那么如何对vue进行编译就涉及到了前端工程化方
面的知识了,前端工程化对nodejs有着很高的要求,但是好在有很多成熟且免费的工具
让我们使用,即使不掌握nodejs照样可以使用,但是如果想要深入理解前端工程化,那
么还是建议学好nodejs

3.搭建vue开发环境

要运行vue程序,需要相应的开发运行环境,如何搭建vue开发环境也是一门很重要的知识。在这里
有必要先简单的提一下vue的版本以及发展历史。

vue的作者是尤雨溪,最初vue完全是他的一个个人项目。时至今日,已成为全世界三大前端框架之
一,github 上拥有 15 万 Star 领先于 React 和 Angular,在国内更是首选。

4.vue的重要版本

2013年,在 Google 工作的尤雨溪,受到 Angular 的启发,开发出了一款轻量框架,最初命名为
Seed 。

2013年12月,更名为 Vue,图标颜色是代表勃勃生机的绿色,版本号是 0.6.0。

2014.01.24,Vue 正式对外发布,版本号是 0.8.0。

2014.02.25,0.9.0 发布,有了自己的代号:Animatrix,此后,重要的版本都会有自己的代号。

2015.06.13,0.12.0,代号Dragon Ball,Laravel 社区(一款流行的 PHP 框架的社区)首次使用
Vue,Vue 在 JS 社区也打响了知名度。

2015.10.26,1.0.0 Evangelion 是 Vue 历史上的第一个里程碑。同年,vue-router、vuex、vue-cli
相继发布,标志着 Vue从一个视图层库发展为一个渐进式框架。

2016.10.01,2.0.0 是第二个重要的里程碑,它吸收了 React 的虚拟 Dom 方案,还支持服务端渲
染。自从Vue 2.0 发布之后,Vue 就成了前端领域的热门话题。

2019.02.05,Vue 发布了 2.6.0 ,这是一个承前启后的版本,在它之后,将推出 3.0.0。

2019.12.05,在万众期待中,尤雨溪公布了 Vue 3 源代码,目前 Vue 3 处于 Alpha 版本。

目前我们主要使用的是vue2.x的版本

在1.0版本之前,vue仅仅提供了构建页面视图的核心逻辑,并没有考虑到工程化方面的改进。但
是,随之前端不断的发展,工程化迫在眉睫。但是好在此时市面上已经有了 babel webpack gulp rollup … 等构建工具,可以对vue程序进行构建,从而便于进行vue开发,但是依然有一定的门槛。

为了降低使用vue的门槛,在2015.10 又发布了 vue-cli ,一个零配置开箱即用的 vue 脚手架工
具。之后又对该工具进行了整理和升级,目前新的工具根据功能划分了成了以下几个包:

@vue/cli
@vue/cli-service
@vue/cli-service-global

5.使用@vue/cli进行vue开发环境的构建

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供:

  • 通过 @vue/cli 实现的交互式的项目脚手架。
  • 通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。
  • 一个运行时依赖 ( @vue/cli-service ),该依赖:
    • 可升级;
    • 基于 webpack 构建,并带有合理的默认配置;
    • 可以通过项目内的配置文件进行配置;
    • 可以通过插件进行扩展。
  • 一个丰富的官方插件集合,集成了前端生态中最好的工具。
  • 一套完全图形化的创建和管理 Vue.js 项目的用户界面。

即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也
为每个工具提供了调整配置的灵活性,无需 eject。

关于 @vue/cli 的相关功能有很多,目前对于初学者的我们只需要掌握最基本最简单的用法即
可。

  1. 安装
npm install @vue/cli @vue/cli-service-global -g

全局安装以上两个包,此时在我们的pc系统中注入了这几个命令 vue serve 、 vue build 、 vue create 、 vue ui ,可以分别查看版本

2. 使用 vue serve app.vue 对 vue 文件进行编译并显示。
3. 使用 vue build app.vue 对 vue 文件进行构建。
4. 使用 vue create vue-app 创建一个 vue 项目,其中 vue-app 为项目的名字

可查看创建好的项目,在 src/main.js 文件中带入如下:

import Vue from 'vue' // 引入Vue类
import App from './App.vue' // 引入App单文件组件 Vue.config.productionTip = false // 关闭产品提示
new Vue({ // 实例化Vue
render: h => h(App), // 该项表示组件被渲染
}).$mount('#app') // 挂载于页面中的 #app标签
  1. 使用 vue ui 可以展示一个界面,可以通过界面去管理所有的vue项目(自行研究)

我们本次对vue的学习将全部基于 @vue/cli 提供的开发环境下进行学习。

6.vue基础知识

首先,要明确vue中是通过一个单文件组件来描述视图的,而完整的视图又是由 html css js 组成
的,因此vue的单文件组件被设计成了由三部分组成,这些是在前面已经接触过了的。在这里我们还要
引入三个概念:数据(data),方法(methods),指令(order)。先来看看这三个概念出现在模板的
什么位置。

<template>
<div>
<!--这里的@还有v-html都属于指令-->
<span @click="disCount()">-</span>
<span v-html="count"></span>
<span @click="addCount()">+</span>
</div>
</template>
<script>
export default {data(){ // data用于表示该段视图中的所有数据
return {count: 0
}
},
methods: { // methods表示视图中涉及到的方法
addCount(){this.count += 1
},
disCount(){this.count -= 1
}
}
}
</script>

1.数据(data)

在编写视图的时候,肯定会产生一定量的数据,比如控制开关的布尔值,展示菜单的数组等等,这
些都是与视图相关的数据,vue中定义了data用于存储我们所用到的视图数据,请记住data必须是函
数。

<scripts>
export default {data(){return {// 这里写数据
}
}
}
</scripts>

2.方法(methods)

除了视图数据之外,我们还需要写很多的逻辑,比如点击事件的逻辑,根据不同条件进行展示的逻
辑等等,这些与视图相关的逻辑,我们可以拆成一个个的方法写在methods中。

<scripts>
export default {methods:{// 这里写方法
}
}
</scripts>

3.指令(order)

大家应该也看到了,vue中的指令是用于模板中的html标签的。在指令的帮助下,我们可以在视图
中灵活的使用我们定义的数据和方法。vue中内置了多个指令可以帮助我们实现各种视图的交互逻辑。
有的同学有疑问了,我的逻辑明明是写在methods中的,为什么还需要指令,其实指令在这里的作用是
把methos和data中的逻辑和数据关联到模板中。比如,addCount里边的逻辑需要在点击+之后才能触
发,这里我们就需要 @ 这个指令帮我们进行绑定事件。

我们可以这样理解指令:模板中写的都是标签,我们想要在模板中嵌入数据和逻辑。但是数据
和逻辑又是存在与js环境中的。那么指令就可以帮我们在模板中构建出js环境以便于我们嵌入数据
和逻辑。因此,指令的格式是key:value格式。key需要我们稍微记忆以下,但是value就是js表达
式。一般来说js中合法的都可以写在指令的值上。

那么vue中都内置了哪些指令呢?

  1. v-html 绑定js环境中的一些值到对应标签的innerHTML
  2. v-text 跟 v-html 基本是一样的,但是它会把标签字符串原样输出,简写为{{}}
  3. v-bind 把对应标签的对应属性(attribute)跟js环境中的数据进行绑定,可以简写成 :
<div v-bind:class="active">active</div>
<!--简写-->
<div :class="active">active</div>
  1. v-for 循环结构
<div v-for="(item,key) in shopData" class="panel panel-default">

v-for 循环的是它的载体及其所有后代元素
5. v-if v-else v-show

<div v-if="isShow" :style="[oCss, {backgroundColor: 'red'}]"></div>
<div v-else :style="[oCss, {backgroundColor: 'blue'}]"></div>

v-if 根据其值(布尔)控制载体的加载与卸载, v-else 配合 v-if
v-show 和 v-if 机制一样,但是 v-show 是通过 display:none 的方式来控制元素显示隐藏

6.v-model 一般用于 input 标签,作用是绑定 input 的value值,但是该绑定是双向的。

<input :value="inputVal" type="text" />
<input v-model="inputVal" type="text" />
  1. v-on 用于给对应的标签绑定事件,可以简写成 @
<p v-on:click="inputVal = inputVal.split('').reverse().join('')">
{{inputVal}}</p>
<p @click="inputVal = inputVal.split('').reverse().join('')">{{inputVal}}
</p>

4.todolist

通过实现一个之前写过的todolist来加强相关vue知识的理解

二、案例练习(自定义表单)


项目地址:自定义表单
这里有两个基本的知识点需要注意一下

  1. vue组件中的js和css可以通过外部引入
<style scoped src="../common/dragFormStyle.css"></style>
import { oftenList, selfList } from "../common/dragFormData.js";
export default {data() {return {oftenList, // 常用项的数据
selfList, // 自定义项的数据
};
},
};
  1. 需要全局引入css
// 额外的引入css文件
import './assets/icon/iconfont.css'

要学会去打印每个组件的组件实例,每个组件在被创建并实例化后就会拥有一个组件实例,该
实例记录了对应组件的所有信息,可以打印实例查看相关信息,这样有利于我们去理解vue组件。
methods中的函数内部的this就是组件实例。
一般我们可以写下边的代码,去以方便我们打印组件实例

<template>
<button @click="watchComponentObj">查看本组件实例</button>
</template>
<script>
export default {methods: {watchComponentObj(){console.log(this)
}
}
}
</script>

在编写本单元提供的案例的时候,注意应用上述的知识点和技巧。

三、vue组件

关于vue到目前为止,我们需要正式组件这个词了。组件组件叫的很顺口,到底是什么意思呢?其
实,这也是web技术发展的一个方向,就是组件化。

要讲明白组件化,我们还得从基本的网页制作开始说起,我们知道网页包含三部分,结构样式和行
为分别对应着我们的 html css javaScript 这三门语言。这没有什么不对,这样的划分也非常便于
理解,但是哪里有问题呢, html css javaScript 这三门语言的配合有问题,它们是以一个页面为
单位的。也就是说 html 是主体,然后去引入 css 和 js 。一个页面是这样的,一个页面中的某以部分,
比如一个页面中的轮播图,这一部分单独领出来的话,按照原始的方式组织的话,我们得找到跟轮播图
相关的 html css javaScript 代码,然后剥离出来之后再放到一个先的页面中(我们一般叫
demo)。下次用的时候,还得复制,还得看一下使用的页面中如何把该这个 demo 给加进去。

随着web发展的日渐成熟,这个问题就很突出了,不利于代码的复用,因为涉及到了三种不同的代
码。那么换种想法,不要再认为一个网页是界面的最小单元了,页面中的任何一段视图是界面的最小单
元。无论这个界面多小,它也包含了界面的基本三要素(结构样式和行为,即使你的这段视图现在没有
交互效果,但是你能保证将来不给这段视图添加交互效果吗?)。所以呢,我们就给它起了个名字,叫
做组件。

所以说,一个组件在页面中表示出来的是一段功能完整的视图,在代码角度上它肯定也包含了 html
css javaSciprt 这东西。好了,现在组件化思想有了。那么技术上如何实现呢?这又是一个问题。其
实目前呢,已经在推行新的组件化标准了。这也意味着,将来我们就不再是使用 link script 的方式
去组合了。不过,新标准太慢还有漫长的路要走。

但是呢,组件化这种思想很好,深入人心。大家看下边一张图,会深有体会。

我们可以把一个网页看成是一个大的组件,网页里边的每个视图都可以使一个小组件,小组件有可
以再进行拆分。这样对于开发来说,无疑是一个好消息,而且会方便复用。

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类
的组件。

刚才说道标准推行比较慢,那么这么好的思想我们就不用了吗?当然不是!伟大的程序员从来没有
停止过探索不步伐(喜欢折腾)。目前,前端的三大框架(vue react angular)都以自己的方式实现了
组件化。具体背后如何实现,我们暂不深究,先学会使用,感受组件化带给我们全新的开发方式吧!

正如我们前边练习的案例(使用vue编写),对了我们管 .vue 的文件叫什么来着?单文件组件是
吧!没错我们写的那种格式就是 vue 中实现组件化的方式。仔细想想,是不是让一个组件完美的包含了
html css javaScript 这些内容。

接下来,让我们对组件有更加深入的认识吧。

1.vue组件的创建

我们使用 vue 单文件组件已经是 vue 中最普遍最常用的方式了,当然也是最推荐的。但是关于 vue
组件除了 单文件组件 的方式之外,还有被的方式,虽然不是特别常用,但是可以加深我们对 vue 组件的
理解,所以非常有学习的必要。

记住一点,组件的形式如何改变,它一定会用自己的方式去包含 html css javaScript 的代码
的。vue单文件组件的代码组织方式是非常明显的。

2.纯javaScript创建组件

也只有 javaScript 有能力去创建组件了,因为 html 在 javaScript 中可以表现为字符串, css 可
以以行内样式的形式出现,同样也可以在 javaScript 中表现为字符串,例如:

const html = '<div style="color: red;">hello world</div>'

在 vue 中用纯 javaScript 创建组件,分为两种 全局组件 和 局部组件 。它们有区别吗?当然有!只
是,暂时先不去讲解它们之间的区别,先学习这两种组件创建的方式。然后,再深入的去对比这两种方
式的区别。

  1. 全局组件
    全局组件使用 Vue.component 方法去创建
Vue.component('组件名称',组件的配置项)

示例如下:

import Vue from 'vue'
Vue.component('global-test', {// template: '<div id="box">{{msg}}</div>',
render(h){ // template和render表示的是同一个意思,但是不能共存
return h('h1', {id: 'box'}, [this.msg])
}
data() {return {msg: 'hello i am global-test'
}
}
})

这里我们解释以下细节,组件的配置项部分是一个对象,也就是我们单文件组件中的配置项,
是一个意思。这里多了 一个 template 或者是 render 属性,这两个属性就是提供结构的,因
为这种方式不再有 标签了。唯一多了个标签名,这个我们在前边单
文件组件的时候没用到,但是这个组件名是很有用的。在下边,学习组件组装的时候会用到
的。

  1. 局部组件
    导出一个有组件配置项的对象即可

export default {
template: ‘

{{msg}}

’,
data() {
return {
msg: ‘i am local-test’
}
}
}

这里我们来看局部组件其实跟全局组件是一样的,配置项这些内容页都一样,但是没有组件名。

3.使用vue 单文件组件

单文件组件的形式呢,我们就不再多说了,因为已经用的很熟了,这一节就简单说说单文件组件如
何被编译的?以及为什么需要单文件组件?有个很直观的问题,大家应该看到了,就是使用纯
javaScript 创建的组件我们写标签的时候,需要写在字符串中,这是很痛苦的,尤其当结构比较多的
时候,简直不可想象。所以,就把 template 的配置项提成 标签了。这样,
我们的结构就很好写了。

所以,我们发现 vue 单文件组件是没法直接在浏览器中使用的,我们需要进行编译。这里就是为什
么我们要给 vue 单文件组件构建开发环境了的原因了。那么编译干了什么事呢?编译过程是借助了
webpack (一款打包工具,以后会学习)进行转码,把 标签中的内容有转换成
了 template 属性中的字符串。

这样,单文件组件就变成了纯 javaScript 编写的组件了,就可以被浏览器加载识别了。所以,
vue 单文件组件本质上就相当于是局部组件。只不过,它的结构写起来很方便。

4.组件的组装

我们已经掌握了创建组件的方式,然而,我们最终的目的还是要形成一个完整的页面。那么,如何
形成完整的页面呢?对!就是要把多个组件按照视图的逻辑关系组装起来。所以,这里我们要学习组件
组装的方式。

比如有两个组件分别为组件A和组件B,那么可以把B组件组装在A组件的内部,这样B组件所代表的
视图就会出现在A组件的内部,这种情况下,我们成A组件是B组件的父组件,而B组件是A组件的子组
件。实现方式如下:

组件A.vue

<template>
<div class="A">
<h1>{{title}}</h1>
<b-child></b-child>
</div>
</template>
<script>
import B from './B.vue'
export default {data(){return {title: 'A'
}
},
components: {'b-child': B}
}
</script>

组件B.vue

<template>
<ul class="B">
<li v-for="(item, index) in list" :key="index">{{item.text}}</li>
</ul>
</template>
<script>
export default {data(){return {list: [{text: 'a'},{text: 'b'}]
}
}
}
</script>

上述代码中的A组件是一个局部组件,前边说过,局部组件是没有组件名称的。然后,组件名称对
于我么组装组件来说是必不可少的。所以,局部组件要组装的话,我们要先给局部组件起一个名字。哪
一步呢?

<script>
import B from './B.vue' // 引入B组件
export default {components: {'b-child': B} // 注册B组件为A组件的子组件,并给B组件绑定一个组件名为
`b-child`
}
</script>

上述步骤就是把B组件导入,注册B组件为A组件的子组件,给B组件绑定组件名称为 b-child 。此
时,只能说明B组件已经成为A组件的子组件了,但是还未在A组件中嵌入B组件。哪一步呢?

<template>
<div class="A">
<h1>{{title}}</h1>
<b-child></b-child> <!-- 在A组件中使用B组件 -->
</div>
</template>

所以,大家看到组件名称的用处了吧!它可以帮我们在组装组件的时候,在父组件中调用对应的子
组件。当然了,B组件可以作为A组件的子组件,也可以作为别的组件的子组件,只要在对应的组件内部
导入并注册就可以了。而且,子组件可以在父组件的模板中多个地方使用,这就很好的实现了我们前边
所说过的组件复用。

那么全局组件呢,它在创建的时候,就有组件名称,所以全局组件是可以直接使用的,这也就是全
局组件和局部组件的区别。

如果想把组件B组装到A组件的内部,那么要看B组件是局部组件还是全局组件,如果是全局组
件,直接B组件的组件名写在A组件模板中即可。如果B组件是局部组件,那么需要先在A组件中导
入B组件,然后把B组件注册到A组件的components配置项中,并给B组件分配一个组件名,然后
把分配的组件名写入A组件的模板中。

请完成下图中红色线框的组件并组装成一个整体(线上参考地址为 https://www.jd.com/ )


5.动态组件

vue 给我们提供了一个动态组件的功能。首先,说说动态组件解决了个啥问题。我们在编写界面的
时候,经常可能会碰到类似于这样的问题,有一个选项卡,但是每个选项对应的展示内容都极为不相
同。那么,这样的情况我们只能给每个内容写一个单独的组件。但是,这些组件我们如何切换呢?大家
很容易可以去想到使用 v-if 或者 v-show 但是它只能切换两个组件,当然多个也能切换,但是相对来说
写起来很麻烦。

基于上述问题, vue 提供了动态组件的功能。好,已经明白了动态组件解决的问题类型了,那么接
下来看看动态组件如何使用?

vue 给我们提供了一个组件名称为 components 的组件,该组件是一个全局组件,我们不需要关
心,该组件是如何被创建出来的,只要明白如何使用该组件即可。因为是全局组件,我们可以在任何组
件布局直接使用该组件。

<components></components>

该组件的 is 属性接受一个组件名,那么对应的组件就会被渲染在这个位置。

<!--A.vue-->
<template>
<div>
<h1>hello world!</h1>
<components is="DragTest"></components>
</div>
</template>
<script>
import DragTest from './dragTest.vue'
export default {components: {'DragTest':Dragtest
}
}
</script>

上述组件意味着,组件名为 DragTest 的组件会被渲染在 A.vue 组件的内部。
实现一个导航切换的功能(练习一下动态组件吧)



四、组件通信

组件既然可以互相的组装,那么意味着组件之间存在着父子这样的关系,而且,我们也知道每一个
组件都有自己的数据,且数据是互相独立不影响的。但是在实际编程的时候,我们需要多个组件之间的
数据进行通信。

其实呢,组件之间的通信方式主要有三种:

  1. 父组件向子组件通信
  2. 子组件向父组件通信
  3. 跨级组件的通信(后边讲)
    接下来,让我们来学习一下组件之间是如何通信的。

1.父组件向子组件通信

父组件向子组件通信是最常见的一种通信,首先呢组件之间一定要形成父子关系。然后一定要想明
白这个之间的交互过程,所谓父子通信,就是父组件的消息发送给子组件。那么,父组件在这个过程中
扮演的是发送消息者,子组件在这个过程中扮演的是接受消息者。所以,我们需要学习的是父组件如何
把消息发出去,而子组件如何接受消息。

  1. 父组件传递信息
<template>
<div>
<h1>{{title}}</h1>
<!--以key-value的形式传递-->
<sub-title :subTitle="subTitle"></sub-title>
</div>
</template>

这里呢还有一种简便方法,就是假设有一个对象中的数据需要传递的话,我们可以使用 vbind=“obj” 来简写。举个例子:
现在有一个对象

const obj = {name: 'admin', age: 12, sex: true}

想要把该对象中的数据都以 key-value 的形式传递,那么一个一个写的话是很麻烦的,看下实
际应该如何写

<template>
<div>
<h1>{{title}}</h1>
<sub-title v-bind="obj"></sub-title>
</div>
</template>
  1. 子组件接受消息
<script>
import Vue from 'vue'
// 子组件
Vue.Component('sub-title', {data(){return {}},
props: ['subTitle'], // 放的是父组件定义的key值
template: '<p>{{subTitle}}</p>' // 在子组件中就可以使用接受的信息了。
})
</script>

我们发现了子组件接受的时候是在配置项中添加了 props 属性,该属性的值是一个数组。在数
组中,我们放入传递消息的 key 即可(因为消息是采用 key-value 的形式传递的)。设计成数
组的形式,以为着我们可以接受多个数据,那么父组件也可以传递多个数据。

当然 props 的值除了可以是一个数组外,还可以是一个对象,如果设置为对象的话,那么我们
对于接受的消息就可以添加更多的设置,这些设置如下:

key: { // 接受的消息的key
type: String/Number/Boolean/Array ... // 接受的消息的类型,这里的值是对应
的数据的类
required: true // 该消息必传
default: value / () => value // 该消息的默认值
validator: (value) => condation... // 自定义该消息的验证条件
}

示例:

<script>
export default {props: {subTitle: {type: String,
required: false,
default(){return '请输入副标题'
}
}
}
}
</script>

这里还要提一下单项数据流,所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级
prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状
态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不
应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

2.子组件向父组件通信

同样的子组件向父组件中通信的话,子组件就是消息发送者,父组件就是消息接受者。所以,我们
要学习子组件如何发送消息,父组件如何接受消息。

  1. 由父组件给子组件注册一个事件(目的是为了接收)

3.拖拽组件的组装

父子通信可以让我们复用逻辑,比如实现一个可以拖拽的组件,将其他组件传入到该组件中,从而
实现任意组件都可以拖拽的功能,以实现复用拖拽逻辑代码。
代码如下:

<template>
<div
class="drag-app"
:style="{ left: left + 'px', top: top + 'px' }"
@mousedown="mouseDown"
v-html="NewComponent"
>
</div>
</template>
<script>
export default {data() {return {left: 0,
top: 0,
};
},
props: ['NewComponent']
methods: {mouseDown(ev) {const disx = ev.clientX - ev.target.offsetLeft;
const disy = ev.clientY - ev.target.offsetTop;
document.onmousemove = (ev) => {const x = ev.clientX - disx;
const y = ev.clientY - disy;
this.left = x;
this.top = y;
};
document.onmouseup = () => {document.onmousemove = null;
document.onmouseup = null;
};
},
}
};
</script>
<style scoped>
.drag-app {position: absolute;
left: 0;
top: 0;
min-width: 30px;
min-height: 30px;
background-color: red;
}
</style>

封装一个通用的弹窗

3.获取真实的dom节点

可能你已经发现了,使用了vue之后,基本上就不再操作真实的dom节点了,但是有些时候dom节
点还是很有用的(虽然绝大多数情况下都用不到真实dom节点)。那么在vue中如何获取真实dom节点
呢?
实现方式如下:

![本质行也就是在标签上添加 ref 属性,然后在组件实例中的 $refs 中可以通过 ref 属性的值取到该
标签的真实的 dom 节点

  1. 尽量不使用dom节点
  2. 不得不使用的时候,可以使用vue预留的接口
  3. 在需要获取的dom节点上添加ref=key属性
  4. 通过this.$refs.key去获取dom节点](https://img-blog.csdnimg.cn/119551e73bf04ed3be876e5369c9f756.png#pic_center)

4.实现自定义滚动条

自定义滚动的效果如下:

要求:

  1. 对滚动条进行封装,可根据参数控制是横向的还是纵向的
  2. 使滚动条可以通用

自定义滚动条的实现原理如下

五、组件的生命周期

1.vue生命周期

我们现在已经看到了在我么编写一个组件的时候,在配置项中,预留了一些会在特定时间执行的函
数,比如 mounted 。这些在特定时期执行的函数就是组件的生命周期。一个vue组件在实例化的时候会
经历各个阶段,每个重要的阶段都给我们预留函数,方便我们添加重要的逻辑。除了实例化之外,在组
件内的数据改变的时候也会预留一些函数。具体可参考下图


2.练习

以拆分组件的方式,完成案例,组件的拆分可参考下图

六、重要配置项(计算属性,监听属性,组件缓存)

前边我们学习过的配置项有 data methods components props ,除此之外能还有一些配置项非常
有用。本单元我们要再学习两个重要的配置项: 计算属性( computed ) 和 监听属性( watch )

除了学习新的配置项外,还要再学习一个内置的全局组件 <keep-alive></keep-alive> ,缓存组件。

1.计算属性

计算属性结局的问题是什么呢?计算属性可以让 data 中的数据进行一些计算从而派生得到新的数
据形式。比如 data 中有数据 sex: true ,但是我们希望显示的是 男生女生 。这个时候我们可以编写一
个表达式 sex ? ‘男生’ : ‘女生’ ,这个表达式放在哪里呢?我们可以放在模板中

{{sex ? ‘男
生’ : ‘女生’}}

。但是,这样的话会使模板过重从而难以维护。所以我们使用计算属性
computed 。看下边的例子:

<template>
<div>
{{sexStr}}
</div>
</template>
<script>
export default {data(){return {sex: true
}
},
computed: {sexStr(){return this.sex ? '男生' : '女生'
}
}
}
</script>

可以在多个地方使用 sexStr 的结果,并观察一下这个函数执行了多少次。总结一下 computed 的特
点:

  1. 当计算结果需要复用时候,可以使用
  2. 写法像函数,但是本质上是属性
  3. 对大量计算有优化效果
  4. 仅仅在内部依赖的data中的数据发生变化的时候,才执行

2.监听属性

大家可能已经发现了一个特别神奇的功能,就是我们放在 data 中的数据只要跟模板进行绑定了之
后,每次 data 中的数据已修改,对应的视图就会更新。那么可以想到,肯定是 vue 在我们改变 data 中
的数据的时候,去做了 dom 的更新。那么 vue 肯定是能监听 data 中的数据发生了变化的,当然这也是
vue 的核心,具体如何监听呢,这个以后我们再讲,这里 vue 把对 data 数据的监听给暴露了出来。其
实不仅仅是对 data 中数据的监听,而是所有具有响应式功能的数据监听都暴露了出来。 computed 和
props 中的数据也可以进行监听。

看一下,监听属性 watch 的用法:

<template>
<div>
{{msg}}
<button @click="reverseStr">反转字符串</button>
</div>
</template>
<script>
export default {data(){return {msg: 'hell vue'
}
},
methods: {reverseStr(){this.msg = this.msg.split('').reverse().join('')
}
},
watch:{'msg'(newVal, oldVal){console.log(newVal, oldVal)
}
}
}
</script>

请运行上述组件,并点击按钮之后观察打印的结果,自行总结 watch 的用法。

3.组件缓存

组件在切换的时候,会发生组件的卸载以及重新加载,那么这个时候组件内部的状态会变成初始的
状态。如果想要避免这样的问题,那么可以使用 vue 提供的缓存组件来解决。

vue 提供了一个全局组件 ,来维护组件的状态,同时该组件还有include exclude max 等几个属性。它们的作用如下:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。
    用法示例:
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

动态组件切换的时候,组件的状态将被保留下来

七、vue事件和指令修饰符

vue 中的事件还是我们熟悉的那个事件,只不过 vue 框架采用了更加巧妙的方式去添加事件。现在
先来回忆一下,事件的三要素有哪些?

  1. 事件源-承载事件的dom节点

  2. 事件类型-以字符串标识,可以自定义

  3. 事件监听器-在事件触发的时候会跟着执行的函数

先来回忆一下原生的方式如何添加事件

oDiv.onclick = function(){}
oDiv.addEventListener('click', function(){}, false)
/**
1. oDiv - 事件源
2. Click - 事件类型
3. Function 事件监听器
/

vue组件中添加事件的时候,其实也体现出了这三个方面,只不过形式不太相同

<template>
<p @click="changeSubTitle">{{subTitle}}</p>
</template>
<script>
export default {data(){return {subTitle: '美丽的中国'}
},
methods: {changeSubTitle(){this.subTitle = '完美的世界'
}
}
}
/*
1. p标签是事件源
2. click是事件类型
3. changeSubTitle事件监听器
*/
</script>

上述代码中,因为是直接把 @click 放置在模板的标签上的,所以可以直接锁定事件源而且非常准
确方便。而在 @ 后边可以直接添加事件的类型,比如说 click 。在 = 后边就可以添加事件监听器了,一
般是放在 methods 中的函数。

1.事件对象

在事件触发的时候被浏览器传递给事件监听器的一个参数,该对象记录了事件相关的所有信息

为了更好的理解vue中事件中三要素中的监听器的指向,我们用一个原生的写法来举例子

const p = document.getElementById('p')
function c(e){alert('hello')
}
p.onclick = function(ev){c()
}

上述代码中,函数c并不是事件监听器,那个匿名函数才是,因此匿名函数的形参ev才是事件对象,
而函数c接受的参数e并不是事件对象,接下来我们看看vue

<template>
<div>
<h1 @click="c1">标题1</h1>
<h2 @click="c2($event)">标题2</h2>
</div>
</template>
<script>
export default {methods: {c1(ev){console.log(ev)
},
c2(ev){console.log(ev)
}
}
}
</script>

上述代码中h1和h2的点击事件都会打印出对应的事件对象,如何理解呢,h1上添加的事件的事件监
听器就是函数c1,所以函数c1接受到的参数就是事件对象。而h2上添加的事件的事件监听器是一个隐形
的匿名函数,该函数会在编译的时候被添加,而c2仅仅相当于在监听器中执行的一个函数而已。而在模
板中使用 $event 可以获取事件对象,是vue组件提供的特殊能力。

2.事件修饰符

我们前边学过vue指令的形式为: 指令名:参数=value 的形式,比如: v-bind:type=“t” 其中type
为参数,或者 v-on:click=“btnClick”,其中click为参数 。其实vue中指令的完整形式如下:

指令名:参数[.修饰符1.修饰符2....修饰符n]=值


通过上图我们可以知道,有些指令没有参数,有些指令没有修饰符,有些指令参数和修饰符都有。
关于事件的修饰符,我们可以划分这么几类:通用修饰符,键盘按键修饰符,系统修饰键,鼠标按钮修
饰符

3.通用修饰符

.stop 阻止事件传递的
.prevent 阻止事件的默认行为的
.capture 以捕获的方式添加事件
.self 跳过冒泡和捕获,只有直接作用在该事件源上的事件才会触发
.once 添加的事件仅执行一次
.passive 可以提升移动端的性能,但是不要和.prevent一起使用,否在失效

4.键盘按键修饰符

keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持。
因此vue提供了按键修饰符,用来精确的监听被按下的按键
可以直接使用键盘码,例如:

<template>
<input v-on:keyup.13="submit">
</template>

为了在必要的情况下支持旧浏览器,Vue 提供了绝大多数常用的按键码的别名:

.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space (按下空格键)
.up
.down
.left
.right

你还可以通过全局 config.keyCodes 对象自定义按键修饰符别名

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112

5.系统修饰键

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器

.ctrl
.alt
.shift
.meta 系统键
.exact 允许你控制由精确的系统修饰符组合触发的事件,表示仅当该系统按键按下时,才触发

例如:

<template>
<!-- Alt + C -->
<input v-on:keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div v-on:click.ctrl="doSomething">Do something</div>
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>
</template>

6.鼠标按钮修饰符

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

left 鼠标左键
.right 鼠标右键
.middle 鼠标中键

修饰符不再一一举例了,请自行尝试,并掌握其用法(不用全部记住,要知道,相关修饰符让我们
控制鼠标键盘的时候会更加方便,可以做出更多的交互。)

7.v-model修饰符

v-model指令也有一些修饰符,如下:

  1. .lazy
    在默认情况下, v-model 在每次 input 事件触发后将输入框的值与数据进行同步 。你可以添
    加 lazy 修饰符,从而转为在 change 事件之后进行同步:
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">
  1. .number
    如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:
<input v-model.number="age" type="number">

这通常很有用,因为即使在 type=“number” 时,HTML 输入元素的值也总会返回字符串。如
果这个值无法被 parseFloat() 解析,则会返回原始的值。

  1. trim
    如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:
<input v-model.trim="msg">

八、8、vue过渡动画

vue动画一节中包含的内容很多,在这里我们仅仅理解其是如何使用的即可。这样也便于我们将来
快速查阅文档。

首先要明确,vue中设置的动画针对的是元素的显示和隐藏,本质上就是vue在处理元素显示隐藏的
时候,在一些特定时机会添加和删除对应的类名,我们这需要给这些特定的类名添加动画即可。

首先呢,要明确能控制元素显示隐藏的方式如下:

  1. v-if
  2. v-show
  3. 动态组件

1.具体如何使用呢

vue提供了一个 组件,这是一个全局组件,使用该组件包裹需要切换动画的元素,代
码如下

<template>
<div>
<button @click="isShow = !isShow">显示隐藏</button>
<transition name="slide">
<div class="box" v-show="isShow"></div>
</transition>
</div>
</template>

使用 包裹了该组件,那们需要动画的组件会根据显示隐藏的节点去添加和删除类
名,具体的添加和删除的时机如下:
在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter :定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移
    除。
  2. v-enter-active :定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插
    入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟
    和曲线函数。
  3. v-enter-to :2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此
    同时 v-enter 被移除),在过渡/动画完成之后移除。
  4. v-leave :定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active :定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡
    被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时
    间,延迟和曲线函数。
  6. v-leave-to :2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效
    (与此同时 v-leave 被删除),在过渡/动画完成之后移除。


对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 ,则 v- 是这些类
名的默认前缀。如果你使用了 ,那么 v-enter 会替换为 mytransition-enter 。

接下来针对类名编写对应的过渡动画即可,代码如下:

<style>
.slide-enter{transform: translate(100px);
}
.slide-enter-active{transition: all 2s;
}
.slide-leave-active{transform: translate(100px);
transfomr: all 0.1s;
}
</style>

给所有写过的组件添加一个侧边栏,形成一个完整的页面

九、vue中的ajax请求

在vue项目中使用ajax请求和在原生的项目中使用ajax的项目是一样的,也就是说vue项目不影响
ajax的使用。这里需要注意,如果请求是在组件初始加载的时候就执行的话,那么需要在 created 或
者 mounted 生命周期中调用,如果是在交互发生的时候需要请求的话,那么就在交互发生的时候去调用
ajax 即可。

另外需要注意的是,vue环境给我们提供了配置代理的地方,也就是说如果我们请求的接口是跨域
的,我们可以通过配置代理的方式去解决跨域的问题。

1.vue环境中配置代理

如果需要在vue的开发环境中配置代理,那么需要在项目的根目录创建 vue.config.js (如果不存在的话)

在配置中添加如下代码:

module.exports = {devServer: {proxy: 'http://localhost:4000' // 配置所有的请求转发到http://localhost:4000
}
}

上述代码的配置方式表示的是所有的请求都转发到 http://localhost:4000 这台服务器上。
还可以写如下的配置方式:

module.exports = {devServer: {proxy: {'/api': { // 针对所有以 /api开头的接口
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': { // 针对所有以 /foo开头的接口
target: '<other_url>'
}
}
}
}

上述配置表示的是,所有路径以 /api 开头的请求都转发到 服务器,所有路径以 /foo 开头的
请求都转发到 <other_url> 服务器。

2.封装ajax请求的方法

这里用到的 ajax 请求方法和我们之前在学习模块的时候,用到的是一样,只不过需要改成 es6 模
块的写法。具体代码如下:

function stringify(obj) {var arr = []
for (var i in obj) {var key = i
var value = obj[i]
arr.push(`${key}=${value}`)
}
return arr.join('&')
}
async function request({url,// 请求的地址
method = 'GET',// 请求的方法
params = {},// 表示的是地址栏参数
data = {},// 请求体数据
headers = {},// 请求头
dealError = false, //是否处理自己处理请求错误,默认不处理
onProgress = () => { } // 处理请求进度的
// success = () => {},// 请求成功的回调
// fail = () => { }// 请求失败的回调
}) {// 处理地址栏参数
const paramsstr = stringify(params)
if (paramsstr) {url += `?${paramsstr}`
}
// 处理请求头
const defaultHeaders = {'Content-Type': 'application/json',
...headers
}
// 处理请求体数据
const cp = defaultHeaders['Content-Type']
let datastr = null
// 传递的数据类型为formData格式
if (Object.getPrototypeOf(data) === FormData.prototype) {delete defaultHeaders['Content-Type']
datastr = data
} else {if (cp.indexOf('application/json') !== -1) {datastr = JSON.stringify(data)
datastr = datastr === '{}' ? null : datastr
} else if (cp.indexOf('urlencoded') !== -1) {datastr = stringify(data)
datastr = datastr || null
}
if (method === 'GET') {datastr = null
}
}
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
// 完成请求头的设置
for (let i in defaultHeaders) {xhr.setRequestHeader(i, defaultHeaders[i])
}
xhr.send(datastr)
// 监听progress事件
xhr.onprogress = onProgress
try {const res = await new Promise((reslove, reject) => {xhr.onreadystatechange = function () {if (xhr.readyState === 4) {if (xhr.status < 300) {let data = null
const cp = xhr.getResponseHeader('Content-Type')
if (!cp) {data = xhr.responseText
} else {if (cp.indexOf('application/json') !== -1) {data = JSON.parse(xhr.responseText)
} else if (cp.indexOf('urlencoded') !== -1) {data = parse(xhr.responseText)
} else {data = xhr.responseText
}
}
reslove([true, data, xhr])
} else {if (!dealError) { // 采用默认处理
const { error } = JSON.parse(xhr.responseText)
alert(error)
}
reject([false, {}, xhr])
}
}
}
})
return res
} catch (err) {return err
}
}
export default request

封装的 ajax 请求方法,接受的参数为:

url,// 请求的地址
method = 'GET',// 请求的方法
params = {},// 表示的是地址栏参数
data = {},// 请求体数据
headers = {},// 请求头
dealError = false, //是否处理自己处理请求错误,默认不处理
onProgress = () => { } // 处理请求进度的

该方法响应的结果为一个数组,模式如下:

[boolean-是否成功, data-成功的时候返回的数据,失败的话该项为null, xhr对象]

3.ajajx在vue组件中的使用示例

一个分页的使用示例:

<!-- 假设分页的请求接口为 /api/getPageDetail?page=1&pageSize=10 -->
<template>
<div>
<h1>分页列表</h1>
<!--列表-->
<ul>
<li v-for="(item, index) in pageList" :key="index">{{item.text}}
</li>
</ul>
<!--页码-->
<ol>
<li @click="handleClick(item)" v-for="(item, index) in
Math.ceil(total / pageSize)" :key="index">{{item}}</li>
</ol>
</div>
</template>
<script>
import request from './request'
export default {data(){return {page: 1, // 访问数据的页数
pageSize: 10, // 每页数据的条数
total: 100, // 总的数据条数
pageList: [] // 每页请求到的数据
}
},
methods: {// 请求指定页数的数据,并更新视图
async getPageDetail(page){const [isSuccess, data] = await request({url: '/api/getPageDetail',
params: {page, pageSize: this.pageSize}
})
if(isSuccess){this.pageList = data
}
},
// 点击每个页码的逻辑
handleClick(page){this.page = page
this.getPageDetail(page)
}
},
mounted(){this.getPageDetail(this.page); // 初次加载请求第一页的数据
}
}
</script>

十、vue路由

本单元的内容为路由,这对于初学者来说应该是一个新的词了。我们先来解释什么是路由?回到浏览器来说,我们知道在浏览器中访问不同的页面的时候,浏览器地址栏中的 url 地址是不同的。那么,我们可以理解为每一个 url 地址都对应着一个页面(视图)。这其实也是我们在服务端讲过的内容,不同的 url 地址代表的资源路径不同,那么传统的 url 改变,表示的是在服务端获取的资源位置不同。这种模式,有两个缺点:1. 每次 url 改变的时候都要从服务器获取一次资源 2. 以为 url 对应的是不同的页面文件,所以很难实现一个页面中的视图切换。

关于上述的两个缺点,第一个应该很好理解。那么第二个就不太好理解了,我们举个例子,下图是
一个传统的带导航的页面。

交大官网的首页


交大官网的学校概况介绍

我们应该如何去制作上述的两个页面呢?如果按照传统的方式来做的话,那么就创建两个页面就好了,但是两个页面中有相同的部分,就是页面的头部和导航。这对于我们开发来说,无疑是非常难受的。(你要写两个一样的东西,虽然可以复制。但是如果有修改呢?每改一次,都得重新复制一份。更何况,导航不仅仅是两个,有多少个导航,就意味着要维护多少份)

因为一个页面是一个大的视图,在视图内通过导航去切换子视图。这种情况也是用传统的 url 地址对应的去切换页面比较难受的地方。

1.什么是路由

现在呢,我们可以采用新的方式,就是路由的方式。前边说传统的方式是 url 地址对应不同的页面
文件,那么路由就是 url 地址去对应不同的组件(组件的概念大家应该很清楚了)。

url 和页面的对应关系是在后端定义的,而 url 和组件的对应关系是在前端定义的。那么也就是
说,我们只需要定义了 url 地址和组件一一对应的关系之后,然后在控制组件的显示逻辑,我们就可以
做到切换浏览器地址栏中的 url 地址,从而显示对应的视图,这样使用起来就跟传统的有页面跳转一样
了。

上述的内容,其实就是前端路由的一个解释。那么要做到上述所说的的内容,对编程功底是要一定
要求的。而且还需要保证每次修改 url 地址的时候浏览器不能向服务器发送请求(否则你的页面会刷新
的)。

不过呢,好在是 vue 给我们提供了路由的功能,已经把刚才所说的问题解决了,我们只需要学会如
何使用路由即可。(以后,有兴趣的话,可以再深入研究底层的原理)

大家再思考一下,如果我们在前端的项目中完全使用了前端路由的方式的话,那么后端传统
的 url 地址加载方式,其实也就只用了一次。哪一次呢?就是第一被加载的时候。那么,可以想
象整个项目中的众多视图其实本质上都在一个页面中(只有一个html文件)。这种技术也被称之
为spa。即 single page application 单页面应用。

2.vue路由的基本使用

vue 给我们提供了路由功能,但是呢是放在了另外的一个包中 vue-router 。该包需要单独安装。

npm install vue-router -S

安装完成之后,就需要在项目中引入该包了,引入的方式如下:

创建一个 router.js ,用来编写路由信息

// route.js
import VueRouter from 'vue-router' // 引入vue路由
import Vue from 'vue' // 引入vue
Vue.use(VueRouter) // 挂载路由插件
const routes = [ // 路由表示的是url和组件的一一对应的关系,使用path属性表示url路径,使用
component属性表示组件
{path: '/a',
component: A
},
{path: '/b',
component: B
}
]
const router = new VueRouter({ // 生成一个路由实例
routes// 配置项-配置视图和路由之间的对应关系
})
export default router // 导出路由实例

在 main.js 中引入 vue 路由实例

import Vue from 'vue'
import App from 'app'
import router from './router'
new Vue({render(h => h(App)),
router // 挂载路由实例
}).$mount(document.getElementById('app'))

在视图中通过 router-view 组件去指定路由视图显示的位置

<template>
<div id="app">
<router-link to="/a">访问A组件</router-link>
<router-link to="/b">访问B组件</router-link>
<router-view></router-view>
</div>
</template>

此时也面中会呈现出两个链接,点击后即可展示对应的组件

router-link 组件和 router-view 组件都是 vue 提供的全局组件,其中, router-link 组件的
作用是进行路由跳转的,它的 to 属性可以设置路由的 url 。 router-view 组件是用于给路由组件
占位的,当 url 匹配之后,对应的路由组件会被渲染在 router-view 的位置。当然 router-view
也有自己的属性,这个我们在后边会继续讲解。

3.路由的嵌套

一个完整的视图,我们我们根据逻辑拆分完成后,可能会出现路由之间的嵌套(类似于前边我们说的
传统的 url 跳转所存在的问题),那么我们此时就需要学习路由如何嵌套,关于这个功能,在vue-router中
也有实现


如上示意图中,我们的路由配置需要两级,因为在主页面的路由中出现了二级路由.配置如下:

const routes = [
{path: '/login',
component: Login
},
{path: '/main',
component: Main,
children: [
{path: 'a',
component: A,
},
{path: 'b',
component: B,
},
{path: 'c',
component: C,
}
]
}
]

要注意,以 / 开头的嵌套路径会被当作根路径。

根据如上配置,要访问A视图,访问路径为 /main/a ,而此时Main组件一定会加载, main 组件加载完成后,才再去加载 A 组件。那么在 Main 组件内部需要使用 组件去占位。

<!-- Main组件 -->
<template>
<div>
<h1>Main组件</h1>
<router-view></router-view> <!-- 这里会根据url去加载 A B C三个组件 -->
</div>
</template>

4.路由实例的属性和方法

当路由实例 router 被注入到应用中之后,在所有的组件实例上都会出现 $router 属性和 $route 属性,这两个属性上提供了很多有用的属性和方法,以便于我们编写自己的应用,一定要打印一下看一看。

我们看看 $route 属性。


除了 fullPath 和 path 之外,还有 query 记录的是 url 地址后边的查询参数(如果有的话),params 用于动态路由,一会儿要讲到, meta 用于记录路由元信息, matched 表示匹配到的路由。

还有 $router 属性中暂时,我们只掌握 push 方法即可,该方法用于使用 js 的方式进行路由的切
换。

this.$router.push(path)

5.动态路由

路由的配置方法中,还有一个是动态路由。有什么用呢?比如,我们有个商品详情的组件,这个组
件肯定只有一个,但是呈现出来的效果是有多少件商品就有多少个视图。那么,我们既可以使用动态路
由的方式。

const routes = [
{path: '/detail/:pid', // :pid 表示这里可以匹配任意的字符串,将来可以通过pid这个表
示获取对应的路径
component: Detail
}
]

对于上述路由配置,当 Detail 被加载之后,通过路由提供的 $route 属性中,我们通过 params 属
性可以获取到具体的路径。

比如路径 /detail/1 是可以匹配到刚才配置的路由的,那么 Detail 组件加载后,我们打印出来的
this.$route.params 的结果就是:

{pid: '1'
}

`一般,在列表跳详情的时候,会大量的使用动态路由。

除了上述的动态方式之外,我们还有如下的匹配方式:``

{// 会匹配所有路径
path: '*'
}
{// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}

有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得
越早,优先级就越高。

十一、vue路由进阶

在这一单元中,我们将学习关于 vue 路由更加高级的内容。通过前边的学习,我们已经掌握了 vuerouter 的基本用法,本单元的高级用法,能够让我们更加灵活的使用 vue-router 。以便于应对将来在项目中,遇到的各种问题。

1.命名路由

所谓命名路由,就是给一个路由起个名字,以便于将来更好的识别和匹配路由。那么这里路由指的是哪部分呢?就是我们配置在 routes 数组中的每个对象。也就是说,拥有 path component 的属性的这些对象就是一个一个的路由。

那么起名字就是,在路由对象中添加 name 属性。这样,就不仅仅是 path 属性是路由的唯一表示,在这里插入代码片name 属性也可以做为路由的唯一标识。

const router = new VueRouter({routes: [
{path: '/user/:userId',
name: 'user',
component: User
}
]
})

上述代码中,如果说路由名称为 user 的路由的话,我们很快就可以找到它。还有哪些地方需要识别路由呢?比如在路由跳转的时候,在路由跳转的时候,我们有两种方式分别是 和 this.$router.push 方法。那么,之前是传递路由的 path 值,现在可以传递name 。

router-link 的方式

<template>
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</routerlink>
</template>

this.$router.push 的方式

router.push({ name: 'user', params: { userId: 123 } })

2.命名视图

命名视图指的就是给视图去命名,那么路由中具体哪一部分指的是视图呢?就是 。我们知道每个路由中的 component 所对应的组件会在其对应的 path 匹配后,被渲染在 的位置。那么我们的命名视图,就是给 起名字。 有一个name 属性,用于起名。如下:

<router-view name="a"></router-view>

话说,名字已经起好了,那么它有啥用呢?那么你要是知道路由配置中还有这么一个特性,就是一个 path 可以匹配多个组件,这个时候我们使用 components 属性进行配置,如下:

const router = new VueRouter({routes: [
{path: '/',
components: {default: Foo,
a: Bar,
b: Baz
}
}
]
})

路径 / 匹配到的组件有三个,分别是 Bar Baz Foo ,因为是多个组件,那么配置组件的属性从component 换成了 components ,且值变成了对象,那么我们给每个组件分配一个 key 值。这个 key 值就是将来我们的组件渲染到对应的 的名字。你想的没错,如果配置的是多个组件,那么对应的 需要有多个,且要通过名字区分开,这就是命名视图的作用。

针对上述配置的路由,我们对应的 写法如下:

<template>
<router-view /> <!-- Foo组件会被渲染在这里 -->
<router-view name="a" /> <!-- Bar组件会被渲染在这里 -->
<router-view name="b" /> <!-- Baz组件会被渲染在这里 -->
</template>

没有起名的视图,那么它的视图名称就是 default 。

3.重定向和别名

前端的路由底层的实现和通过 url 地址切换页面是不同的,但是它们实际的使用效果应该是一样的。所以,我们传统的重定向之类的功能,在 vue 路由中也都有实现。

如何实现重定向功能呢?只需要在,配置项中加入 redirect 配置项即可。比如,我们从 /a 重定向到 /b :

const router = new VueRouter({routes: [
{ path: '/a', redirect: '/b' }
]
})

上述配置表示,当访问路径 /a 的时候,会自动切换到 /b ,然后去匹配路由中 /b 所表示的组件。这就是重定向了,那别名又是什么意思呢?

加入路径为 /a 的路由配置了别名是 /b ,意味着,当用户访问 /b 时, url 依然会保持 /b ,但是匹配的组件却是 /a 对应的组件,就像直接访问 /a 一样。

上边描述的路由应该配置为:

const router = new VueRouter({routes: [
{ path: '/a', component: A, alias: '/b' }
]
})

“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。

4.路由组件传参

路由组件是配置在路由对象的 component 属性中的,也就是说路由组件的实例化并不是我们直接写的,而是在框架中被调用的。既然,我们不能调用,那么如果给这类型组件(路由组件)传递参数呢?

本小结就讲解下如何操作。

在组件中可以获取 $route 属性,其实我们很多的信息可以通过 url 地址传递(比如动态路由,比如查询参数),然后在路由组件中通过 $route 属性进行接受的。但是,这样的话,我们的路由组件就和 $route 耦合在一起了。也就是说这个路由组件因为内部使用了 $route ,那么它只能当路由组件使用,不同当普通组件去使用了。

那么为了解决 $route 耦合的问题,我们可以在路由对象中添加 props 属性来控制给路由组件传参,这样路由组件就可以通过它内部的 props 的方式去接受参数了,这样的行为和作为子组件去接受参数的行为一样,所以就解决了耦合的问题。

如下代码:

const User = {template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({routes: [{ path: '/user/:id', component: User }]
})

上述代码中 User 组件就为了就收动态路由的 id ,使用 $route 。那么该组件就不能作为普通的子组件了。下边看看我们如何通过配置 props 属性进行解耦

const User = {props: ['id']
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({routes: [{ path: '/user/:id', component: User, props: true }]
})

给路由对象中配置 props 的含义是,如果它的值为布尔值的话,则表示是否把this.$route.params 中的数据作为数据传递给路由组件。 true 表示传递, false 表示不传递。

这样的话,该组件就可以被用在任何地方了,只要给该组件传递对应的数据即可,便于测试和维护。

路由对象中的 props 的值除了是布尔类型之外还可以是对象模式,如果是对象的话,那么设置为props 值的对象,会原样的传递给路由组件。

const router = new VueRouter({routes: [
{path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
})

Promotion.vue

<template>
<div>{{newsletterPopup ? '接受到的是true' : '接受到的是false'}}</div>
</template>
<script>
export default {props: ['newsletterPopup']
}
</script>

路由对象中的 props 除了布尔模式和对象模式之外,还有一种函数模式。设置给 props 的函数会接受 $route 作为参数,并且该函数返回的值会被传递给路由组件。如下:

const router = new VueRouter({routes: [
{path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
})

URL /search?q=vue 会将 {query: ‘vue’} 作为属性传递给 SearchUser 组件。

5.路由模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

仔细观察我们默认的这种 hash 模式的路由,在路由的后边都有一个 # ,其实这种方式本质改变的不是 url 的路径,而是 url 中的锚点。除了这种路由模式外,还有一种模式叫做 histroy 模式,在history 模式下我们的 url 就不再代用 # 而是真正的路径,这样好看的多。

那么如何切换呢?只需要在生成路由实例的时候加入 mode 配置项。

const router = new VueRouter({mode: 'history',
routes: [...]
})

注意, mode 不是配置在路由对象中的,而是配置在和 routes 同级 的。 hash 模式和 history 模式表现的 url 的外观上不同,是因为它们底层实现的方式不同,一个是使用 onhashchange 实现的,另一个是使用 history.pushState 实现的。具体如何实现,还是很有必要了解的,不过呢这不是本节的内容,以后我们会深入讲解并尝试实现。

6.路由元信息

在配置路由对象的时候,我们可以配置 meta 字段。该字段的作用是给每个路由对象配置一些额外的信息,这些信息将来通过 $route.meta 可以访问到。

const router = new VueRouter({routes: [
{path: '/foo',
component: Foo,
children: [
{path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})

通过 this.$route.meta 可以得到结果为 {requiresAuth: true}

十二、导航守卫

导航守卫也是属于路由的一部分,因为我们创建了路由的根本作用就是可以让我们的整个应用可以方便的导航到任意的位置。那么涉及到权限的话,有时候我们就需要限制导航了。导航守卫就是帮助我们根据实际业务情况,进行导航的控制。

导航呢,我们也可以理解为路由发生了变化,最根本的就是对应的组件进行了切换。只要有导航发生,那么必然伴随着一个组件的卸载,另一个组件的加载。我们把卸载的组件称之为 from ,加载的组件称之为 to ,这样的叫法方便我们后续的描述。导航的时候, from 组件会卸载, to 组件会加载。我可以说离开了 from 组件,进入了 to 组件。在这个过程中,我们加入以下中间的控制,这些中间的控制就被称之为导航守卫。意思为 像卫兵一样,守护着导航的安全 。

其实,在一次导航发生变化的时候, vue-router 中内置了很多的守卫,接下来我们来学习一下各个守卫。

1.全局前置守卫

我们可以使用 router.beforeEach 注册一个全局前置守卫。

const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {// ...
})

前置守卫可以注册多个,以为着你可以多次调用 router.beforeEach 。全局前置守卫会按照创建顺序,依次调用。守卫呢是异步解析的,在守卫执行的过程中,导航一直处于等待状态。只有等守卫全部通过后,导航才能顺利到底目标路由。

每个守卫接方法收三个参数:

  • to: Route : 即将要进入的目标 路由对象
  • from: Route : 当前导航正要离开的路由
  • next: Function : 该方法调用则表示本次守卫通过了,可以进入下一个环节。不过呢,
  • next 方法的调用有很多的用法。

next() 方法的调用

  • next() : 表示本次守卫通过,可以直接进入下一个环节。

  • next(false) : 如果接收一个 false 参数的话,表示守卫中断,那么会重置到 from 路由的地址

  • next(‘/’) 或者 next({ path: ‘/’ }) : 表示当前路由中断,会重新发起一个导航,导航的目标路由就是 next 方法中所接收的路由。

同时,要确保 next 在每一个逻辑下仅调用一次

这里有一个在用户未能验证身份时重定向到 /login 的示例:

// 这中写法是不好的
router.beforeEach((to, from, next) => {if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// 这种写法是值得推荐的
router.beforeEach((to, from, next) => {if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})

2.路由独享的守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

const router = new VueRouter({routes: [
{path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {// ...
}
}
]
})

路由独享的守卫会在全局守卫触发之后才触发,全局守卫是所有的导航都会触发,而路由独享的守卫则是被设置了路由守卫的路由作为 to 路由的时候,才会触发。

这样的设计也是增加了不同路由之间的差异性,因为有些限制是所有路由都需要的,而有些限制仅仅是个别的路由需要的。

这些守卫与全局前置守卫的方法参数是一样的。

3.组件内的守卫

你可以在路由组件内直接定义以下路由导航守卫:

const Foo = {template: `...`,
beforeRouteEnter(to, from, next) {// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

需要注意的是, beforeRouteEnter 守卫 不能 访问 this ,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {next(vm => {// 通过 `vm` 访问组件实例
})
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和beforeRouteLeave 来说, this 已经可用了所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {// just use `this`
this.name = to.params.name
next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

beforeRouteLeave (to, from, next) {const answer = window.confirm('Do you really want to leave? you have unsaved
changes!')
if (answer) {next()
} else {next(false)
}
}

4.全局解析守卫

你可以用 router.beforeResolve 注册一个全局解析守卫,它基本上和 router.beforeEach 类似,它们的区别是解析守卫是在所有的导航守卫执行结束之后同时路由组件被解析,导航被确认之前执行的。也以为着,当解析守卫如果已经通过的话,那么导航就可以被确认了。

5.全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

router.afterEach((to, from) => {// ...
})

全局的后置钩子一般用于在导航完成之后做一些善后处理工作。

6.完整的导航解析流程

我们上述学习的所有的关于导航中的控制方法,并非全部都是守卫。其中 afterEach 并非是守卫,而是导航最终被确认之后才执行的一个钩子函数。只有当所有的守卫方法都解析并执行通过之后,导航才会被确认。所以,你可能发现了所有的守卫方法都有 next 方法,用来控制守卫是否允许本次导航通过。

我们整理一下整个导航从被发起到确认的完整的一个流程,如下:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter 。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter 。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

7.收货地址(练习)

使用 vue 配合 vue-router ,完成收货地址管理的项目。之前,我们写过相关的后台接口。现在可以找出来这套案例的相关接口(见node课程),然后,前端界面部分使用 vue 完成相应的内容。

项目截图为:
登录页面

注册页面

收货地址列表

新增收货地址

编辑收货地址

对应的接口项目见如下目录:

十三、vue路由补充

关于 vue-router 我们已经学习了相当多的内容了,包括很多的细节。本单元呢,是对 vue 路由的一个补充。我们要学习两个内容一个是路由切换的动画,另一个是路由中的滚动行为。这样,会让我们的页面更加完整和丰富。

1.过渡动效

路由切换的时候,我们在视图中需要使用 ,所以根据前边我们学过的过渡动画,我们可以给 加上我们的过渡动画组件 。

<transition>
<router-view></router-view>
</transition>

剩下的就是给 去写动画了, Transition 的所有功能 在这里同样适用。

2.单个路由的过渡

上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 并设置不同的 name。

const Foo = {template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
}

3.基于路由的动态过渡

还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:

<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {'$route' (to, from) { // 监听每次路由变化
const toDepth = to.path.split('/').length // 获取目标路由路径的长度
const fromDepth = from.path.split('/').length // 获取离开路由路径的长度
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left' //
如果是后退则向右滑,如果是前进则向左滑。
}
}

4.滚动行为

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

注意: 这个功能只在路由模式为 histroy 的路由中可用。

当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:

const router = new VueRouter({routes: [...],
scrollBehavior (to, from, savedPosition) {// return 期望滚动到哪个的位置
}
})

scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当导航是通过浏览器的 前进/后退 按钮触发时才可用。该方法返回的滚动位置信息长这样 {x: number, y:number} 也可以是 {selector: hash值} 。

对于所有路由导航,简单地让页面滚动到顶部的一个例子。

scrollBehavior (to, from, savedPosition) {return { x: 0, y: 0 }
}

返回 savedPosition ,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:

scrollBehavior (to, from, savedPosition) {if (savedPosition) {return savedPosition
} else {return { x: 0, y: 0 }
}
}

如果你要模拟“滚动到锚点”的行为:

scrollBehavior (to, from, savedPosition) {if (to.hash) {return {selector: to.hash
}
}
}

你也可以返回一个 Promise 来得出预期的位置描述:

scrollBehavior (to, from, savedPosition) {return new Promise((resolve, reject) => {setTimeout(() => {resolve({ x: 0, y: 0 })
}, 500)
})
}

还可以将 behavior 选项添加到 scrollBehavior 内部返回的对象中,就可以为支持它的浏览器启用原生平滑滚动:

scrollBehavior (to, from, savedPosition) {if (to.hash) {return {selector: to.hash,
behavior: 'smooth',
}
}
}

十四、插槽

如何给组件中传递消息是我们在组件通信章节所学的内容,回忆一下,当时我们能传递的内容基本上都是 javaScript 中的数据类型。现在有一个很重要的需求,就是需要给一个组件中传递另一个组件。这个时候再使用我们之前所学过的通过组件属性传递,在组件内部通过 props 配置项接收,就不好用了。

1.插槽的基本用法

针对上述所说的需求,我们需要就需要用到插槽功能了。插槽功能允许你像下边示例的方式去合成组件,我们称之为组件的嵌套。

<navigation-link url="/profile">
Your Profile
</navigation-link>

Your Profile 不是通过属性传递的,而是放在了两个标签中间进行的传递。那么这个组件内如何接受这部分内容呢?

<!-- navigation-link 的模板 -->
<template>
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot> <!-- 这里将渲染为 Your Profile -->
</a>
</template>

我么可以理解为 是一个全局组件,用于接受组件通过标签中嵌套的方式传递的内容。当然这种方式传递的内容是任意的,包括组件也可以传递。

<navigation-link url="/profile">
<!-- 添加一个图标的组件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>

如果 的 template 中没有包含一个 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

2.插槽的后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 组件中:

<button type="submit">
<slot></slot>
</button>

我们可能希望这个 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 标签内:

<button type="submit">
<slot>Submit</slot>
</button>

这样我们使用 这个组件的时候,不给标签中嵌套内容的话,也会有默认的显示,就相当于设置了默认值。

3.具名插槽和v-slot指令

在一个组件内,我们也可以放置多个插槽,以便于我们渲染不同的内容。例如对于一个带有如下模板的 组件:

<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>

对于这样的情况, 元素有一个特殊的 attribute: name 。这个 attribute 可以用来定义额外的插槽:

<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

一个不带 name 的 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 vslot 的参数的形式提供其名称:

<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

这里的 v-slot 是一个 vue 内置的指令(还记得质量的完整形式吗), : 后边的内容为它的参数。那么我们可以理解 v-slot 指令的含义是 向参数指定的组件中渲染内容 ,该指令的简写形式为 # 。 v-slot 的参数可以写成动态形式如 v-slot:[box] ,那么 box 在这里表示的就是一个标量,该变量的值才是 v-slot 真正的参数。

注意 v-slot 只能添加在 上。

4.插槽通信

插槽的这种方式也涉及到通信的问题,具体什么通信问题呢?我们先看一个例子:

现在有 box.vue 组件:

<template>
<div class="box">
<h1>这是一个列表</h1>
<slot></slot>
</div>
</template>
<script>
export default {data() {return {list: [{ text: 1 }, { text: 2 }],
};
},
};
</script>

上述代码中,需要通过外部传进来一个列表,该列表组件如下:

<template>
<ul class="list">
<li v-for="(item, index) in list" :key="index">{{ item.text }}</li>
</ul>
</template>
<script>
export default {props: ["list"],
};
</script>

现在我们在 app.vue 中引入这两个组件:

<template>
<div class="app">
<box-com>
<template>
<list-com></list-com>
</template>
</box-com>
</div>
</template>
<script>
import Box from "./box";
import List from "./list";
export default {components: {"box-com": Box,
"list-com": List,
},
};
</script>

此时,我们看不到列表出现,是因为数据放在了 box-com 组件中并没有传递到 list-com 组件中。那应该如何传递呢?先通过 组件把数据传递出去,修改 box.vue 代码如下:

<template>
<div class="box">
<h1>这是一个列表</h1>
<slot :list="list"></slot> <!--传递数据list给外边的模板-->
</div>
</template>
<script>
export default {data() {return {list: [{ text: 1 }, { text: 2 }],
};
},
};
</script>

因为模板在 app.vue 中,接下来我们去修改 app.vue 中的代码进行接收:

<template>
<div class="app">
<box-com>
<template v-slot:default="box">
<list-com :list="box.list"></list-com>
</template>
</box-com>
</div>
</template>
<script>
import Box from "./box";
import List from "./list";
export default {components: {"box-com": Box,
"list-com": List,
},
};
</script>

我们通过 v-slot:default 去接受 组件传递的信息, v-slot:default=“box” 这样的写法中, box 表示一个对象接受的是 组件传递的所有的消息。

此时,视图中已经可以出现我们所需要的列表了。

十五、在组件上使用v-model

这里再说说我们的 v-model 指令,我们知道这个指令是用在表单元素上,用于双向绑定。可以让数据驱动视图,同时也能让视图驱动数据。使用起来,是非常方便的。

1.v-model的原理

那么可以不可以在我们自定义的组件上,也去使用该指令呢?当然可以,这就需要我们对 v-model的本质加以了解。我么以一个表单来举例,如果不适用 v-mode 可不可以实现视图和数据双向驱动的功能
呢。

<template>
<div class="form">
<input type="text" />
</div>
</template>
<script>
export default {data() {return {val: "哈喽",
};
},
};
</script>

如果我们使用 v-model 可以很容易的让 val 数据和模板中的 input 产生关联,现在我们尝试使用 vbind 来实现。我们修改以下为下边的代码,大家尝试一下

<template>
<div class="form">
<input type="text" :value="val" @input="val = $event.target.value" />
<div>{{ val }}</div>
</div>
</template>
<script>
export default {data() {return {val: "哈喽",
};
},
};
</script>

上述代码应该很容易理解,使用 v-bind:value=“val” 始终会把 val 的值和 input 框中的 value 绑定在一起。而 input 事件可以实时监听表单中内容的变化,通过 $event.target.value 可以实时的获取实际输入的值,并且把实际输入的值实时的赋值给 val 属性。

这样,就达到了数据驱动视图,视图驱动数据的效果。那么我们发现 :value=“val” 和@input=“val = $event.target.value” 合并之后就可以写成 v-model=“val” 。

<template>
<div class="form">
<input type="text" :value="val" @input="val = $event.target.value" />
<input type="text" v-model="val" />
<div>{{ val }}</div>
</div>
</template>
<script>
export default {data() {return {val: "哈喽",
};
},
};
</script>

2.实现自定义组件的v-model

既然已经知道了 v-model 的原理,那么我们就从它的原理入手,在自定义的组件上实现。

<template>
<div>
<self-com :value="val" @input="val = $event"></self-com>
</div>
</template>
<script>
import SelfCom from './selfCom.vue'
export default {data(){return {val: false
}
},
components: {'self-com': SelfCom
}
}
</script>

上述写法以为着在 self-com 组件中,需要通过 value 接收数据,还需要触发 input 事件,代码如下:

<template>
<div class="self-com">
<div class="box" v-show="value"></div>
<button @click="$emit('input', true)">显示</button>
<button @click="$emit('input', false)">隐藏</button>
</div>
</template>
<script>
export default {props: ['value']
}
</script>

我们保持了 self-com 组件内部的写法之后,那么在外部我们就可以把 :value=“val” 和@input=“val = $event” 切换成 v-model=“val” 了。

<template>
<div>
<self-com v-model="val"></self-com>
</div>
</template>

这种 v-model 的交互方式比较方便,尤其是子组件和父组件通信不叫频繁时。而且,这样有个好处就是我们不需要在创建大量的事件名称了。

十六、认识vuex

vuex 是一个与 vue 配套使用的状态管理库,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

简单点如何理解呢?还记得前边说过的组件通信吧!其中,跨级组件如何传递参数呢。就可以使用vuex 来实现。思路是把状态(数据)集中放在一个位置,这个位置我们称之为仓库,然后能保证仓库可以和其他的视图组件进行通信即可。

1.状态管理模式

要说明白是模式状态管理模式,我们需要一个简单的计数器组件。

<template>
<div @click="increament">{{ count }}</div>
</template>
<script>
export default {data() {return {count: 0,
};
},
methods: {increament() {this.count++;
},
},
};
</script>

上述应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的简单示意:

这种模式对有一个组件还好说,但是对于多个组件,尤其是多个组件去共享状态的时候,就会破坏我们这种单向数据流的简介性,设置让代码难以维护。

2.vuex的思想

vuex 作为一个专门的状态管理库,它的思想是把需要共享的状态放在一个公共的仓库中,然后仓库和其他的视图组件保持通信即可。见下图:

作为初学者,我们仅仅看这张图,还不太能明白它的意思。但是我们要知道 Vue Components 指的就是我们平常写的组件,这里我们称它为视图。而其他的概念我们还没有接触过,所以比较陌生,这里先做个简单的介绍,后边我们会在实践中逐渐理解其中的含义。

共享的数据是放在 state 中的,根据 state 中的状态,我们可以渲染出视图。而视图上我们可以发出各种交互,这个被称之为 Actions , Actions 可以中包含了我们的 state 如何改变的信息。而Mutations 则是根据 Actioins 提供的信息去实际改变 state 的。当 state 中的状态发生变化后,视图又跟着更新了。至此,形成了一个完整的单项数据流。

3.vuex的基本用法

在了解了 vuex 的思想后,我们接下来就学习一下 vuex 的基本用法。还是以刚才的计数器举例,我们把该组件的交互方式加上 vuex 。
首先需要安装 vuex ,使用如下命令:

npm install vuex -S

安装好之后,我们创建一个 store.js ,内容如下:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({ // 创建一个Store实例
state: { // 用来存放数据
count: 0
},
mutations: { // 用来改变state中的数据
increatmentCount(state) {state.count++
}
}
})
window.store = store // 挂在store在window上,并与在控制台进行测试

我么可以看出来,上述代码中 state 存储的是计数器中的数据,而 mutations 中配置的increatmentCount 方法是用来让 count 增加的。

目前我们的这个状态还没有和视图关联在一起,我们先摸清它的变化。把 store 挂载在 window 对象上,就是便于在控制台测试。

接下来记得在 main.js 中引入 store.js ,不然该文件不会加载执行

import './store.js'

启动我们的项目之后,我们可以在控制台中打印 store 对象,如下:

我们发现 store 中有众多的属性和方法,其中 state 表示的就是仓库中放入的数据,点开后可以看到:

接下来,我们改变一下这个数据,怎么改变呢?还记得前边那张图中,需要 commit 才能触发mutaitons 中的方法。

刚好大家发现在 store 实例上是不是有一个 commit 方法,没错该方法就是用来触发 mutaitons 中的方法的,那么我们在控制台中输入如下代码:

store.commit('increatmentCount')

此时,对应的方法会执行,该方法中把 state 中的数据改变了。再次查看 state.count 的数据

console.log(store.state.count) // 1

4.vuex关联视图

我么已经可以简单的在仓库中存入数据,并且修改数据了,那么记下来就是要学习如何让仓库和视图进行交互。仓库和视图的交互主要分为两部分,1. 视图如何从仓库中获取数据 2. 视图中如何触发动作修改仓库中的数据

我们的 store 示例需要注入到所有的组件中,所以改写下 store.js 代码

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({ // 创建一个Store实例
state: { // 用来存放数据
count: 0
},
mutations: { // 用来改变state中的数据
increatmentCount(state) {state.count++
}
}
})
window.store = store // 挂在store在window上,并与在控制台进行测试
export default store // 把store示例导出

在 main.js 中引入该 store 实例

import Vue from 'vue'
import store from './store'
import router from './router'
Vue.config.productionTip = false
new Vue({render: h => h(App),
router,
store // 在这里注入store实例后,所有的组件都可以通过 this.$store去访问store实例
}).$mount('#app')

现在我们在改写下之前的计数组件

<template>
<!-- 通过store实例的state属性去获取数据 -->
<div @click="increament">{{ $store.state.count }}</div>
</template>
<script>
export default {methods: {increament() {// 通过store实例的commit方法去修改数据
this.$store.commit("increatmentCount");
},
},
};
</script>

此时,我们就完成了 vuex 的一个基本使用。不过呢,关于 vuex 的 一些细节还有很多,我们留到下一个单元在去学习。

十七、vuex的完整使用流程

基础的 vuex 应用流程,我们已经掌握了,那么接下来我么说一下整个使用过程中的一些细节。

1.核心配置项

在生成 vuex 实例的时候,我们需要配置一下内容,比如 state mutations 等,这里我们需要讲解一下它的完整配置,并且要掌握每种配置的细节。

const store = new Vuex.Store({state:{ // 用来配置仓库中的数据的,可以通过 store.state[propName]获取其中的数据
count: 0
},
getters: {// 从state中派生出一些状态
// 需要配置成函数的形式
// 函数的第一个参数是 state
// 函数的第二个参数是 getters
// getters函数返回的值就是我们派生出来的新的状态
// 可以通过 store.getters[propsName]来获取值,比如store.getters.strCount返回
的值是 本次的计数结果是0
strCount(state,getters){return `本次的计数结果是${state.count}`
}
},
mutations: {// 用于修改state中的数据
// 配置的是函数
// 函数的第一个参数为 state 第二个参数为 payload
// payload表示载荷,就是去调用mutations中的方法的时候,给传递的数据
// mutations中的函数内部必须都是同步代码
// 可以通过 store.commit('mutationsName', payload) 来触发mutations中的函数,
并传递载荷
setCount(state, payload){// 本函数中的 payload表示 一次计数增长的步长
state.count += payload
}
},
actions: {// 用于处理一些异步操作
// 配置的是函数,一般配置为 async函数更方便
// 函数的第一个参数是 Vuex.Store类的实例,函数的第二个参数是被传递的数据payload
// actions中配置的函数内部可以是异步的代码,且在actions内部可以触发mutations中的函
数
// actions中的函数需要通过 store.dispatch(actionsName, payload)来触发
// 如果整个数据改变的过程中,没有异步的操作,可以省略actions,直接在视图层触发
mutations即可
stepTimeout(store, payload){// 该函数表示延迟payload的时长,随机生成一个计数的步长
setTimeout(() => {const step = Math.ceil(Math.random() * 10)
store.commit('setCount', step)
}, payload)
}
}
})
export default store

接下来,我们把核心配置项结合视图使用一下。

<template>
<div>
<!-- 通过store实例的state属性去获取数据 -->
<p>{{ $store.state.count }}</p>
<!-- 通过store实例的getters属性去获取数据 -->
<p>{{ $store.getters.strCount }}</p>
<button @click="handleClick">点击进行计数</button>
</div>
</template>
<script>
export default {methods: {handleClick() {// 通过store实例的dispatch方法去触发后续的一系列修改
// 每次点击3秒之后,会随机增加计数
this.$store.dispatch("stepTimeout", 3000);
},
},
};
</script>

此时页面中的效果如下

每次点击之后,约等待3秒就会随机计数了。不过呢,这种交互不太好,我们修改一下在等待着3秒的时候,按钮变成不可点击状态。

需要在 state 中加入一个数据用来表示按钮的状态

state: {loading: false // 表示按钮的状态,false表示还没开始计数
}

getters 中也派生出一个新的数据

getters: {buttonText(state) {return state.loading ? '生成步长中....' : '点击进行计数'
}
}

在视图的模板中可以做如下修改

<template>
<div>
<!-- 通过store实例的state属性去获取数据 -->
<p>{{ $store.state.count }}</p>
<!-- 通过store实例的getters属性去获取数据 -->
<p>{{ $store.getters.strCount }}</p>
<button :disabled="$store.state.loading" @click="handleClick">
{{ $store.getters.buttonText }}
</button>
</div>
</template>

此时已经给按钮绑定了状态,接下来就要载正确的时机对按钮的状态加以控制了。所以,需要先在mutations 中添加一个函数,用于修改 loading 的状态。

mutations: {setLoading(state, payload) {state.loading = payload
}
}

然后在 actions 中的 stepTimeout 函数中控制 loading 状态的改变,我们应该在延时器之前设置loading 为 true ,在延时器结束后,设置 loading 为 false

修改 mutations 中的 stepTimeout 函数

actions: {stepTimeout(store, payload) {store.commit('setLoading', true)
// 该函数表示延迟payload的时长,随机生成一个计数的步长
setTimeout(() => {const step = Math.ceil(Math.random() * 10)
store.commit('setCount', step)
store.commit('setLoading', false)
}, payload)
}
}

现在的效果如下:

2.vue-devtools

vue-devtools —— The Vue.js devtools allows you to inspect and debug your applications.

上述是 vue-devtools 官网对它的解释,翻译过来就是: vue-devtools 是一个 vue.js 的调试工具,它允许你去链接并调试你的 vue 应用。

接下来,看下如何使用!

3.安装

本质上 vue-devtools 是一款浏览器插件,不够呢是上传在 npm 服务器的,所以我们可以使用 npm进行安装

npm install vue-devtools -g

然后使用 npm config get prefix 找到全局包的安装路径,并在资源管理器中打开。


在 node_modules 下找到 vue-devtools

打开 vue-devtolls 目录下的 vender 目录中就是我们的 vue-devtools 插件,这个是需要安装在浏览器中的

接下来,在 chrome 浏览器的地址栏中输入 chrome://extensions ,然后打开 开发者模式

4.使用

安装好之后,接下来就是要使用了。这个时候呢,我们在浏览器的右上角应该发现如下的图标,表示的是 vue 扩展。

然后呢,我们在浏览器中预览 vue 项目,并打开 chrome 调试工具,应该发现有 vue 这一项。

5.插件

vuex 还提供了插件接口,意味着我们可以按照规则为 vuex 开发插件。接下来,我们看看 vuex 插件的接口规则是怎样的。

可以在生成 vuex 实例的时候,配置 plugins 选项。

const myPlugin = store => {// 当 store 初始化后调用
store.subscribe((mutation, state) => {// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
})
}
const store = new Vuex.Store({plugins: [myPlugin]
})

通过上述代码,我们很容易可以知道,这个插件是如何工作的。 vuex 插件需要的呢是一个函数,这个插件函数会在 store 实例化之后执行,并且就是 store 实例作为参数,其实也就这么简单。这样的话,我们就可以在 store 实例化之后做一些事情了。

这里需要提一下 store.subscribe ,这个方法的作用是监听 mutations 是否触发。每次mutations 触发之后由 subscribe 注册的函数都会执行,并且接受 mutationsObj 和 state 作为参数, state 就是我们仓库中的数据了,而 mutationsObj 是一个形式为 {type: mutaitions函数的名称,payload: 对应mutations函数执行的时候接受的载荷} 的对象。

一定要自己尝试一下,那样的话,你会看的更加明白。

vuex 里边给我们内置了一个插件叫 Logger ,该插件的作用就是打印仓库日志,一般在开发阶段使用,便于调试,在生产环境下不要使用。用法如下:

import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({plugins: [createLogger()]
})

createLogger 就是用来创建我们的插件函数的,它有几个配置项(了解即可):

const logger = createLogger({collapsed: false, // 自动展开记录的 mutation
filter (mutation, stateBefore, stateAfter) {// 若 mutation 需要被记录,就让它返回 true 即可
// 顺便,`mutation` 是个 { type, payload } 对象
return mutation.type !== "aBlocklistedMutation"
},
actionFilter (action, state) {// 和 `filter` 一样,但是是针对 action 的
// `action` 的格式是 `{ type, payload }`
return action.type !== "aBlocklistedAction"
},
transformer (state) {// 在开始记录之前转换状态
// 例如,只返回指定的子树
return state.subTree
},
mutationTransformer (mutation) {// mutation 按照 { type, payload } 格式记录
// 我们可以按任意方式格式化
return mutation.type
},
actionTransformer (action) {// 和 `mutationTransformer` 一样,但是是针对 action 的
return action.type
},
logActions: true, // 记录 action 日志
logMutations: true, // 记录 mutation 日志
logger: console, // 自定义 console 实现,默认为 `console`
})

使用了该插件后,我们在仓库中的每次数据变化都会形成日志,被打印出来。

6.严格模式

我们在创建 store 的时候,传入 strict: true ,即可开启严格模式,该模式有什么作用呢?

const store = new Vuex.Store({// ...
strict: true
})

我们知道 vue 中的数据是响应式的,包括 vuex 中的状态也是这样的,那么意味着我们通过直接修改 state 中的数据(不使用mutations)也是可以的(大家可以试一试)。不过呢,这样就不好了,会让我们难以捕捉状态何时改变的。那么,启用了 严格模式后,就不允许不通过 mutations 而擅自修改状态了,否则会报错。

不过呢,在生产环境下启用严格模式会损失性能,所以我们推荐的策略是在开发环境下使用严格模式,而在生产环境下要关闭严格模式。使用如下写法即可:

const store = new Vuex.Store({// ...
strict: process.env.NODE_ENV !== 'production'
})

十八、vuex进阶

本单元,我们将学习实际开发中,应该如何正确的使用 vuex 。

1.模块

试想一下,当我们的项目比较庞大的时候,我们所有的共享状态都会存在于 state 中,而且有很多数据是毫无关联的,这样放在一起的话对于我们去维护项目是很不友好的。

所以, vuex 提供了模块的功能,目的就是可以把仓库中的数据分开管理。这样每个模块有有自己的 state getters mutations actions 而且还可以嵌套。示例如下:

const moduleA = {state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({modules: {a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

这意味着我们在生成 store 对象的配置项中,引入了 modules 选项,该选项可以配置多个模块,而每个模块下还可以继续去嵌套模块。

大家应该发现了,每个模块的配置项还是我们之前熟悉的核心配置项,但是有一点需要注意,就是state 变成了一个函数。还有 getters 可以接受第三个参数和第四个参数了,第三个参数表示的是全局的状态,第四个参数表示的是全局的 getters 。

getters: {// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},

官网中,对于模块的介绍说了很多细节问题,其实呢不是特别好理解。这里我教大家一种方法,可以快速理解 vuex 中模块的用法。

我们知道我们配置的各种内容,最终都会在 store 对象中体现出来,看一下 store 对象的打印结果。


知道和四个属性特别关键,也很重要。

现在我们加入 modules 配置项,看看他们有什么变化。现在加入一个 modules ,其中的数据用来表示那个按钮的点击状态(把按钮点击状态那个变量移入模块)

modules: {a: {state() {return {loading: false
}
}
}
},

此时,查看 store.state ,效果如下:

可以看到在 state 中自动得为模块中的数据加了一层,使用的名称就是模块的名称,那我们再访问loading 就需要使用 store.state.loading 了,访问 count 还是 state.count 。

再看看 getters

modules: {a: {state() {return {loading: true
}
},
getters: {buttonText(state) {return state.loading ? '生成步长中....' : '点击进行计数'
}
}
}
},


我们发现 getters 并没有分层,这就意味着我们在不同的模块中去写 getters 的话,名称是不能相同的。那么其他两项呢, mutations 和 actions 呢?它们也是不分层的。

modules: {a: {namespaced: true,
state() {return {loading: false
}
},
getters: {buttonText(state) {return state.loading ? '生成步长中....' : '点击进行计数'
}
},
mutations: {setLoading(state, payload) {state.loading = payload
}
},
actions: {stepTimeout(store, payload) {store.commit('setLoading', true)
// 该函数表示延迟payload的时长,随机生成一个计数的步长
setTimeout(() => {const step = Math.ceil(Math.random() * 10)
store.commit('setCount', step, { root: true })
store.commit('setLoading', false)
}, payload)
}
}
}
},


总结一下,也就是说只有 state 会区分模块,其他三项是不会的。那么实际上,我们希望模块有区分吗?当然是希望的,怎么办呢?只要在该模块的配置上加入 namespeced: true ,即可。

modules: {a: {namespaced: true,
//....
}
}

state 还是跟以前一样,看看其它三项

这样的话,将来就需要 store.getters[‘a/buttonText’] store.commit(‘a/setLoading’)store.dispatch(‘a/stepTimeout’) 这样的方式了。

我们发现它们有改变了,但是并不是分层,而是对应的属性名添加了模块名作为标志,不过呢这就够了。

另外一个呢,就是需要注意的是,在模块内的 getters mutations actions 中可能会需要取仓库中的数据,那么它们接受的第一参数 state 表示的是本模块的状态,要想取到全局的状态的话,需要用的到 rootState 和 rootGetters 参数了,在 getters 函数中会作为第三个和第四个参数,在 mutations 中我们不需要,因为一般来说能分模块,那我们本模块肯定修改的是本模块中的数据。在 actions 中的话,它的第一个参数是一个 store 对象,可以通过 rootState 和rootGetters 访问全局的内容。

而在模块中使用 commit 和 dispatch 方法的话,我们一般不需要把携带模块名,比如a/stepTimeout ,如果我们是在 a
模块中触发的话,写成 dispatch(‘stepTimeout’) 即可,那么如果全局恰好有一个 action 的方法名为
stepTimeout 的话,在模内触发的话,可以使用dispatch(‘stepTimeout’, payload,{root:
tue}) 。 commit 方法一样。

modules: {foo: {namespaced: true,
getters: {// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // ->
'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}

2.项目结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到 action 里面。

只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation和 getter 分割到单独的文件。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块

3.辅助函数

现在关于 vuex 还有一个问题,就是组件和 store耦合的问题,也就是我们在组件中还得使用store 耦合的问题,也就是我们在组件中还得使用store耦合的问题,也就是我们在组件中还得使用store 才可以和仓库中的数据进行交互。同时因为过长的名字会让模板过重,这样的开发体验都是很糟糕的。

因此 vuex 提供了辅助函数,分别为 mapState, mapGetters, mapMutations, mapActions ,根据名字就能知道它们的作用就是把 vuex 中的数据和函数映射到组件内。这些方法都接受一个数组或者对象,目的是把 vuex 中的数据和函数映射到组件中,并设定新的名称。

先看一个 mapState 的例子:

<template>
<p>{{ count }}</p>
</template>
<script>
import { mapState } from "vuex";
export default {computed: {...mapState(["count"]),
},
}
</script>

但是我们还有一个状态 loading ,它是在莫块中的,所以我们对于它只能用函数了。

<template>
<p>{{ count }}</p>
<button :disabled="loading" @click="handleClick">
{{ $store.getters["a/buttonText"] }}
</button>
</template>
<script>
import { mapState } from "vuex";
export default {computed: {...mapState({count: (state) => state.count,
loading: (state) => state.a.loading,
}),
},
}
</script>

剩下的 mapGetters mapMutations mapActions 它们因为不是直接分层的而是名称有一些变化,所以它们不支持上述函数的写法,现在我们完全进行改写。

<template>
<div>
<!-- 通过store实例的state属性去获取数据 -->
<p>{{ count }}</p>
<!-- 通过store实例的getters属性去获取数据 -->
<p>{{ strCount }}</p>
<button :disabled="loading" @click="handleClick">
{{ buttonText }}
</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {computed: {...mapState({count: (state) => state.count,
loading: (state) => state.a.loading,
}),
...mapGetters({strCount: "strCount",
buttonText: "a/buttonText",
}),
},
methods: {...mapActions({setpTimeout: "a/stepTimeout",
}),
handleClick() {// 通过store实例的dispatch方法去触发后续的一系列修改
// 每次点击3秒之后,会随机增加计数
this.setpTimeout(3000);
},
},
};
</script>

可以尝试在我们的收货地址项目中,引入vuex

一套史诗级版vue详解!相关推荐

  1. c语言50到100套,c语言51-100套试题答案及详解.pdf

    c语言51-100套试题答案及详解 2 double ave=0.0; 第 51 套 试题答案及详解 3 *n=0; 4 for(i=0; i 5 ave /=N; /* 计算平均值 */ 一.程序填 ...

  2. server2016安装mysql_windows server2016安装MySQL5.7.19解压缩版教程详解

    记录了MySQL 5.7.19 winx64解压缩版安装教程,具体内容如下 系统环境:Win7 x64 软件准备:mysql 5.7.19 winx64 配置安装流程 具体安装如下: 1.把 mysq ...

  3. mysql-8.0.12语法_mysql-8.0.12 (免安装版) 安装详解

    mysql-8.0.12 (解压版) 安装详解 错误解决 第一步:mysql-8.0.12 (解压版) 下载地址:https://www.mysql.com/downloads/ 第二步:配置初始化m ...

  4. 耳机插头3.5与2.5三段与四段i版与n版等详解

    一.耳机插头3.5与2.5三段与四段i版与n版等详解 在2009之前相信大家对各类数码产品诸多不同规格的耳机插头非常的头痛,不同的设备要用不同的耳机,非常的不便,在2009年9月1日国内统一标准,规定 ...

  5. 微信机器人网页版接口详解

    微信网页版接口详解 本系列会讲述微信机器人技术的实现,第一讲主要了解微信网页版给我们提供的http接口,这一步是做一个基于微信网页版机器人的基础和难点. 本讲将微信网页版的主要接口罗列出,并给出了入参 ...

  6. qt5 开发及实例(第4版)_才聪学习网_中级微观经济学第4版练习题详解

    钟根元<中级微观经济学学习指南>(第4版)练习题详解 市 场 说明:作为范里安<微观经济学:现代观点>教材的第一章,本章通过考察一个特定的市场模型--住房市场进行实例分析.对于 ...

  7. Socket 套接字原理详解

    Socket 套接字原理详解 socket 编程介绍 Socket编程封装了常见的TCP.UDP操作,可以实现非常方便的网络编程. socket() 函数介绍 # socket.socket(fami ...

  8. php代码加固,织梦模板加固版教程详解

    原标题:织梦模板加固版教程详解 我们的织梦模板怎么把它变成加固版,大家加固之前最好把原来的网站备份一下,以免操作错误造成损失. 第一步:登录网站后台把我们的网站备份数据库 网站后台点击 "系 ...

  9. OpenStack Queens版搭建详解

    目录 OpenStack Queens版搭建详解 1.基础环境配置 1.2 节点网络规划 1.3 关闭防火墙 1.4 配置yum源 1.5 配置节点IP 1.6 配置主机名 1.7 配置主机名解析(h ...

最新文章

  1. Android Studio中的代码格式快捷方式
  2. 我们用最详细的方式解析Android消息机制的源码,经典好文
  3. 【Android 逆向】Android 权限 ( ro.product.cpu.abi 属性 | ro.zygote 属性 | dhcp.eth0 属性 | net.* 属性 )
  4. 网安面试只要掌握这十点技巧,轻轻松松吊打面试官
  5. 外设驱动库开发笔记34:OLED显示屏驱动
  6. 腾讯:中小企业数字化转型路径报告|附PDF下载
  7. html小球跳跃技术原理,HTML5在文本上跳跃的小球
  8. android sqlite使用之模糊查询数据库数据的三种方式
  9. 使用ecstore-sdk开发包制作易开店和启明星模板
  10. office新建文件打开提示文件格式或扩展名无效
  11. GIF制作软件哪个好,怎么制作搞笑GIF
  12. mysql运维备份_MySQL运维经验
  13. 修改阿里云CentOS的远程登录默认22端口
  14. 《无名之辈》小人物的自我证明
  15. python除以10取整_python中整数除以整数的结果是取整数
  16. 如何在unity使用数据库
  17. 数据结构与算法(C++) -- adjacency matrix
  18. html页面高度和宽度,html和body元素的高度和宽度
  19. 互联网+不是全民皆商
  20. [WUSTCTF2020]佛说:只能四天

热门文章

  1. 约瑟夫环的两种实现方法
  2. 扫地机器人 杂牌_扫地机器人哪个牌子好?国产扫地机器人排行榜
  3. 人工智能-阿尔法狗背后的简单原理:贝叶斯公式
  4. Happy Father's Day 告诉父亲你爱他
  5. 批量转换 LF 和 CRLF 的小技巧【详细步骤】
  6. lgv20刷twrp_LG港版v20 root
  7. f**k命令(解压神器)
  8. 来自一个老码农的技术理想
  9. 用计算机玩王者荣耀,王者荣耀你们用电脑玩王者荣耀顺手吗? - 游戏发言 - 酷酷跑手机游戏...
  10. SQL语句学习(自学记录)