Vue.js 框架进阶笔记

Vue.js 框架进阶笔记

1. 模块化开发

1.1 为什么要模块化

1.1.1 JavaScript 原始功能
  • 早期
    • js作为一种脚本语言,做简单的表单验证动画实现少量代码
  • 后期
    • 随着ajax异步请求的出现,形成了前后端的分离
    • 客户端需要完成的事情越来越多,代码量与日俱增
  • 如何应对
    • 通常会将代码组织在多个js文件中,进行维护
      • 缺陷
        • 比如全局变量同名问题
        • 这种代码的编写方式对js文件的依赖顺序几乎是强制性
        • js文件过多,理清文件顺序很困难
// aaa.js文件中,小明定义了一个变量,名称是flag,并且为true
flag = true

// bbb.js文件中,小丽也喜欢用flag这个变量名称,值为false
flag = false

// main.js文件中,小明想通过flag进行一些判断,完成后续的事情
if (flag) {
    
    
	console.log('小明是个天才');
}
1.1.2 匿名函数的解决方案
  • 使用匿名函数来解决方面的重名问题
    • aaa.js文件中,我们使用匿名函数
(function(){
    
    
	var flag = true
})()
  • 问题
    • 如果在main.js文件中,用到flag,应该如何处理呢?
      • 另外一个文件中不容易使用,因为flag是一个局部变量
1.1.3 使用模块作为出口
  • 需要暴露到外面的变量,使用一个模块作为出口
    • 匿名函数内部,定义一个对象
    • 给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可)
    • 将这个对象返回,并且在外面使用了一个MoudleA接收
    • man.js中,只需要使用属于自己模块的属性和方法即可
var ModuleA = (function() {
    
    
	// 1. 定义一个对象
	var obj = {
    
    }
	// 2. 在对象内部添加变量和方法
	obj.flag = true
	obj.myFunc = function(info){
    
    
		console.log(info);
	}
	// 3. 将对象返回
	return obj
})()
if (ModuleA.flag){
    
    
	console.log('小明是个天才');
}

ModuleA.myFunc('小明长得真帅')

console.log(ModuleA);
  • 常见的模块化规范
    • CommonJSAMDCMD,也有ES6modules

1.2 CommonJS

  • 模块化有两个核心:导出导入
  • CommonJS 导出
    • module.exports
module.exports = {
    
    
	flag: true,
	test(a,b){
    
    
		return a + b
	},
	demo(a,b){
    
    
		return a * b
	}
}
  • CommonJS 导入
    • require
// CommonJS模块
let {
    
    test,demo,flag} = require('moduleA');

// 等同于
let _mA = require('moduleA');
let test = _mA.test;
let demo = _mA.demo;
let flag = _mA.flag;

1.3 ES6 的 export 指令

1.3.1 export 基本使用
  • export指令用于导出变量
// info.js
export let name = 'why'
export let age = 18
export let height = 1.88

// 另一种写法
// info.js
let name = 'why'
let age = 18
let height = 1.88

export {
    
    name,age,height}
1.3.2 导出函数或类
  • 上面主要是输出变量,也可以输出函数或者输出类
export function test(content){
    
    
	console.log(content);
}

export class Person{
    
    
	constructor(name,age){
    
    
		this.name = name;
		this.age = age;
	}

	run(){
    
    
		console.log(this.name + '在奔跑');
	}
}
function test(content){
    
    
	console.log(content);
}

class Person{
    
    
	constructor(name,age){
    
    
		this.name = name;
		this.age = age;
	}

	run(){
    
    
		console.log(this.name + '在奔跑');
	}
}

export {
    
    test,Person}
1.3.3 export default
  • 某些情况下,一个模块中包含某个的功能,希望让导入者可以自己来命名
  • 可以使用export default
## info.js

export default function(){
    
    
	console.log('default function');
}
  • main.js中可以根据需要命名它对应的名字
## mian.js

import myFunc from './info.js'

myFunc()
  • 注意
    • export default同一个模块中,不允许同时存在多个

1.4 ES6 的 import 指令

1.4.1 import 使用
  • 使用export指令导出了模块对外提供的接口
  • 之后就可以通过import命令来加载对应的模块
  • 首先,需要在HTML代码中引入两个js文件,并且类型需要设置为module
<script src="info.js" type="module"></script>
<script src="main.js" type="module"></script>
  • import指令用于导入模块中的内容
## mian.js

import {
    
    name,age,height} from "./info.js"

console.log(name,age,height);
  • 如果需要某个模块中所有的信息都导入
    • 通过*可以导入模块中所有的export变量
    • 但是通常情况下需要给*起一个别名,方便后续的使用
## mian.js

import * as info from './info.js'

console.log(info.name, info.age, info.height, info.friends);

2. Vue CLI 详解

2.1 Vue CLI

2.1.1 Vue CLI 是什么?
  • 使用Vue.js开发大型应用时,需要考虑代码目录结构项目结构部署热加载代码单元测试等事情
  • 通常会使用一些脚手架工具来完成
  • CLI是什么意思?
    • CLICommand-Line Interface, 翻译为命令行界面, 但是俗称脚手架
    • Vue CLI是一个官方发布 vue.js 项目脚手架
    • 使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置
2.1.2 Vue CLI 使用前提 - Node
  • 安装NodeJS
  • 检测安装的版本
    • 默认情况下自动安装NodeNPM
    • Node环境要求8.9以上或者更高版本
    • node -v
    • npm -v
  • 什么是NPM
    • NPM的全称是Node Package Manager
    • NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准
2.1.3 Vue CLI 使用前提 - Webpack
  • Vue.js官方脚手架工具就使用了webpack模板
    • 对所有的资源会压缩等优化操作
    • 它在开发过程中提供了一套完整的功能,能够使得我们开发过程中变得高效
  • Webpack的全局安装
    • npm install webpack -g
2.1.4 Vue CLI 的使用
  • 安装Vue脚手架
    • npm install -g @vue/cli
    • vue --version
  • 注意:上面安装的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目时是不可以
    在这里插入图片描述
  • Vue CLI2初始化项目
    • vue init webpack my-project
  • Vue CLI3初始化项目
    • vue create my-project

2.2 Vue CLI2 的使用

2.2.1 Vue CLI2 详解

在这里插入图片描述

2.2.2 目录结构详解

在这里插入图片描述

2.2.3 Runtime-Compiler 和 Runtime-only 的区别

在这里插入图片描述

  • 如果之后的开发中,依然使用template,就需要选择Runtime-Compiler
  • 如果之后的开发中,使用的是.vue文件夹开发,那么可以选择Runtime-only
2.2.4 render 和 template
  • Runtime-CompilerRuntime-only
new Vue({
    
    
	el: '#app',
	components: {
    
     App },
	template: '<App/>'
})
new Vue({
    
    
	el: '#app',
	render: h => h(App)
})
2.2.5 Vue 程序运行过程

在这里插入图片描述

2.2.6 render 函数的使用
  • 方式一
new Vue({
    
    
	el: '#app',
	render: (createElement) => {
    
    
		// 1. 使用方式一:
		return createElement('标签','相关数据对象(可以不传)'.['内容数组'])
		// 1.1 render函数基本使用
		return createElement('div',{
    
    class: 'box'}, ['xxx'])
		// 1.2 嵌套render函数
		return createElement('div',{
    
    class: 'box'}, ['xxx', createElement('h2', ['标题'])])
	}
})
  • 方式二
const cpn = Vue.component('cpn',{
    
    
	template: '<div>我是cpn组件</div>',
	data() {
    
    
		return {
    
    
		}
	}
})

new Vue({
    
    
	el: '#app',
	render: (createElement) => {
    
    
		// 2. 使用方式二:传入一个组件对象
		return createElement(cpn)
	}
})
2.2.7 npm run build

在这里插入图片描述

2.2.8 npm run dev

在这里插入图片描述

2.2.9 修改配置:webpack.base.conf.js
resolve: {
    
    
	extensions: ['.js','.vue','.json'],
	alias: {
    
    
		'@': resolve('src'),
		'pages': resolve('src/pages'),
		'common':resolve('src/common'),
		'components': resolve('src/components'),
		'network': resolve('src/network')
	}
}

2.3 Vue CLI3 的使用

2.3.1 认识 Vue CLI3
  • vue-cli 3 与 2 版本有很大区别
    • vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
    • vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,buildconfig等目录
    • vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
    • 移除了static文件夹,新增了public文件夹,并且index.html移动到public
      在这里插入图片描述
2.3.2 目录结构详解

在这里插入图片描述

2.3.3 Vue cli3 配置
  • UI方面的配置
    • 启动配置服务器:vue ui

在这里插入图片描述
在这里插入图片描述

2.3.4 自定义配置:起别名
const path = require('path')
function resolve (dir) {
    
    
	return path.join(__dirname,dir)
}

module.exports = {
    
    
	// 1. 基础的配置方式
	configureWebpack: {
    
    
		resolve: {
    
    
			alias: {
    
    
				'components': '@/components',
				'pages': '@/pages'
			}
		}
	},
	// 2. 利用webpack4的webpack-chain来配置
	chainWebpack: (config) => {
    
    
		config.resolve.alias
			.set('@$',resolve('src'))
			.set('components',resolve('src/components')
	}
}

3. Vue Router

3.1 认识路由

3.1.1 什么是路由
  • 概念
    • 路由routing)就是通过互联的网络把信息从源地址传输到目的地址的活动. — 维基百科
  • 作用
    • 路由器提供了两种机制: 路由转送
      • 路由
        • 决定数据包从来源到目的地的路径
      • 转送
        • 将输入端的数据转移到合适的输出端
  • 路由表
    • 路由表本质上就是一个映射表, 决定了数据包的指向
3.1.2 后端路由阶段
  • 后端路由过程
    • 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示
    • 网站有这么多页面,服务器如何处理
      • 一个页面有自己对应的网址, 也就是URL
      • URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理
      • Controller进行各种处理, 最终生成HTML或者数据, 返回给前端
      • 这就完成了一个IO操作
    • 当页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端
    • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化
  • 缺点
    • 一种情况是整个页面的模块由后端人员来编写和维护的
    • 另一种情况是前端开发人员如果要开发页面, 需要通过PHPJava等语言来编写页面代码
    • 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情
3.1.3 前端路由阶段
  • 前后端分离
    • 随着Ajax的出现, 有了前后端分离的开发模式
    • 后端只提供API返回数据, 前端通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中
  • 优点
    • 前后端责任的清晰
      • 后端专注于数据
      • 前端专注于交互可视化
    • 并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可
  • 单页面富应用阶段(Single Page Application)
    • SPA最主要的特点就是在前后端分离的基础上加了一层前端路由
    • 前端来维护一套路由规则
  • 前端路由核心
    • 改变URL,但是页面不进行整体的刷新

3.2 前端路由的规则

3.2.1 URL 的 hash
  • URL的hash
    • URLhash也就是锚点(#), 本质上是改变window.locationhref属性
    • 可以通过直接赋值location.hash来改变href, 但是页面不发生刷新
      在这里插入图片描述
3.2.2 HTML5 的 history 模式
  • history接口是HTML5新增的, 它有五种模式改变URL不刷新页面
  • pushState
    • history.pushState()
      • history.pushState()方法向浏览器历史添加了一个状态(增加一个记录)
      • pushState()方法带有三个参数:一个状态对象、一个标题(现在被忽略了)以及一个可选的URL地址
      • pushState方法不会触发页面刷新,只是导致history对象发生变化地址栏会有反应
      • 如果pushState的url参数设置了一个新的锚点值(即hash)并不会触发hashchange事件
      • 如果设置了一个跨域网址,则会报错
        在这里插入图片描述
  • replaceState
    • history.replaceState()
    • 参数与pushState一样,区别为修改浏览历史中当前纪录
      在这里插入图片描述
  • go
    • history.go()
    • go() 方法可加载历史列表中的某个具体的页面
      在这里插入图片描述
  • 补充说明:
    • history.back() 等价于 history.go(-1)
    • history.forward() 则等价于 history.go(1)
    • 这三个接口等同于浏览器界面的前进后退

3.3 vue-router 基础

3.3.1 认识 vue-router
  • 目前前端流行的三大框架, 都有自己的路由实现
    • AngularngRouter
    • ReactReactRouter
    • Vuevue-router
  • vue-router
    • vue-routerVue.js官方的路由插件,它和vue.js深度集成的,适合用于构建单页面应用
    • 官方网站 https://router.vuejs.org/zh/
  • vue-router是基于路由组件
    • 路由用于设定访问路径, 将路径和组件映射起来
    • vue-router的单页面应用中, 页面的路径的改变就是组件的切换
3.3.2 安装和使用 vue-router
  • 安装vue-router
    • npm install vue-router --save
  • 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)
    • 第一步:导入路由对象,并且调用 Vue.use(VueRouter)
    • 第二步:创建路由实例,并且传入路由映射配置
    • 第三步:在Vue实例中挂载创建的路由实例
import Vue from 'vue'
import VueRouter form 'vue-router'

Vue.use(VueRouter)
  • 使用vue-router的步骤
    • 第一步: 创建路由组件
    • 第二步: 配置路由映射: 组件和路径映射关系
    • 第三步: 使用路由( 通过<router-link><router-view>
3.3.3 创建 router 实例

在这里插入图片描述

3.3.4. 挂载到 Vue 实例中

在这里插入图片描述

3.3.5 vue-router 基本使用
  • 步骤一:创建路由组件
    在这里插入图片描述

  • 步骤二:配置组件和路径的映射关系
    在这里插入图片描述

  • 步骤三:使用路由
    在这里插入图片描述

  • <router-link>: 该标签是一个vue-router中已经内置的组件, 它会被渲染成一个<a>标签

  • <router-view>: 该标签会根据当前的路径, 动态渲染出不同的组件

    • 网页的其他内容, 比如顶部的标题/导航, 或者底部的一些版权信息等会和<router-view>处于同一个等级
    • 路由切换时, 切换的是<router-view>挂载的组件, 其他内容不会发生改变
  • 效果:
    在这里插入图片描述

3.4 vue-router 细节补充

3.4.1 路由的默认路径
  • 如何可以让路径默认跳到首页, 并且<router-view>渲染首页组件呢?
  • 只需要配置多配置一个映射就可以
const routes = [
	{
    
    
		path: '/',
		redirect: '/home'
	}
]
  • 配置解析
    • 我们在routes中又配置了一个映射
    • path配置的是根路径: /
    • redirect重定向
      • 根路径重定向/home的路径下, 这样就可以让路径默认跳到首页
3.4.2. HTML5的History模式
  • 改变路径的方式有两种
    • URL的hash
    • HTML5的history
    • 默认情况下, 路径的改变使用的URL的hash
  • 如果希望使用HTML5history模式, 进行如下配置即可:
const router = new VueRouter({
    
    
	routes,
	mode: 'history'
})
3.4.3. router-link 补充
  • <router-link>中, 使用属性: to, 用于指定跳转的路径
  • <router-link>还有一些其他属性:
    • tag
      • tag 指定<router-link>之后渲染成什么标签组件, 比如<router-link to='/home' tag='button'>会被渲染成一个<button>标签组件, 而不是默认<a>标签组件
    • replace
      • replace 不会留下 history 记录, 所以指定replace的情况下, 后退键不能返回到上一个页面中
    • active-class
      • <router-link>对应的路由匹配成功时, 会自动给当前元素设置一个router-link-activeclass
      • 设置active-class可以修改默认的名称
        • active-class="active"则 router-link-active 变为 active
      • 在进行高亮显示的导航菜单或者底部tabbar时, 会使用到该类
      • 但是通常不会修改类的属性, 会直接使用默认的router-link-active即可
        在这里插入图片描述
3.4.4 修改 linkActiveClass
  • 修改 router-link-active 的默认名称
  • class具体的名称也可以通过router实例的属性进行修改
    在这里插入图片描述
    在这里插入图片描述
  • exact-active-class
    • 类似于active-class, 只是在精准匹配下才会出现的class
    • 后面看到嵌套路由时, 再看下这个属性
3.4.5 路由代码跳转
  • 页面的跳转可能需要执行对应的JavaScript代码
  • 可以使用 this.$router.push('/home')
  • 原理
    • vue 源码中在 data 中默认添加了 $router 属性
    • 调用 $router 属性中的 push() 方法
    • push => pushState
    • 调用 $router 属性中的 replace() 方法不能返回上一页
    • replace => replaceState
      在这里插入图片描述
3.4.6 动态路由
  • 当页面的path路径不确定
    • 比如进入用户界面时,希望是如下的路径:
      • /user/aaaa/user/bbbb
      • 除了前面的/user之外,后面还跟上了用户的 ID
  • 这种pathComponent的匹配关系,称之为动态路由(也是路由传递数据的一种方式)
  • router.js
{
    
    
	path: '/user/:id',
	component: User
}
<!-- 将 userId 这个参数传出去 -->
<router-link :to="'/user/'+userId">用户</router-link>
...
data(){
	return {
		userId:'123'
	}
}
  • 动态展示获取路由 ID 信息
    • $router 就是 router 文件夹 index.js 中定义的 router 对象
    • $route 就是当前活跃状态的路由对象
  • this.$route.params.userId
    • parmas 方式接收参数
<h2>{
   
   {userId}}</h2>
...
data(){
	return {
		userId(){
			// 接收 path: '/user/:id/' 中的 id
			return this.$route.params.id
		}
	}
}

在这里插入图片描述

3.5 路由的懒加载

3.5.1 认识路由懒加载
  • 官方解释
    • 打包构建应用时,Javascript 包会变得非常大影响页面加载
    • 如果把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样更加高效
  • 如何理解
    • 首先, 路由中通常会定义很多不同的页面
    • 一般情况下, 这些页面最后被打包在一个js文件中
    • 但是页面都放在一个js文件中, 必然会造成这个页面非常的大
    • 如果一次性从服务器请求这些页面, 可能需要花费一定的时间, 甚至还会出现了短暂空白的情况
  • 如何解决
    • 路由懒加载
  • 路由懒加载作用
    • 路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块
    • 只有在这个路由被访问到的时候, 才加载对应的组件
3.1.2 路由懒加载的效果

在这里插入图片描述

3.1.3 懒加载的方式
  • 方式一: 结合Vue的异步组件和Webpack的代码分析
const Home = resolve => {
    
     require.ensure(['../components/Home.vue'], () => {
    
     resolve(require('../components/Home.vue')) })};
  • 方式二: ADM写法
const About = resolve => require(['../components/About.vue'], resolve);
  • 方式三: 在ES6写法
const Home = () => import('../components/Home.vue')

3.6 路由嵌套使用

3.6.1 认识嵌套路由
  • 嵌套路由是一个很常见的功能
    • 比如在home页面中, 通过/home/news/home/message访问一些内容
    • 一个路径映射一个组件, 访问这两个路径也会分别渲染两个组件
  • 路径和组件的关系如下:
    在这里插入图片描述
  • 嵌套路由步骤
    • 创建对应的子组件
    • 在路由映射中配置对应的子路由
    • 在组件内部使用<router-view>标签
3.6.2 嵌套路由实现
  • 定义两个组件
    在这里插入图片描述
  • 使用 children 设置子路由进行路由嵌套
    在这里插入图片描述
  • 使用 <router-link> 链接子组件
    在这里插入图片描述
    在这里插入图片描述
3.6.3 嵌套默认路径

在这里插入图片描述

3.7 路由传递参数

3.7.1 准备工作
  • 为了演示传递参数, 再创建一个组件, 并且将其配置好
    • 第一步: 创建新的组件Profile.vue
    • 第二步: 配置路由映射
    • 第三步: 添加跳转<router-link>
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
3.7.2 传递参数的方式
  • 传递参数主要有两种类型: paramsquery
  • params的类型
    • 配置路由格式: /router/:id
    • 传递的方式: 在path后面跟上对应的值
    • 传递后形成的路径: /router/123, /router/abc
this.$router.push({
    
    
  name:'xxx'
  params:{
    
    
    id:id
  }
})
  • query的类型
    • 配置路由格式: /router, 也就是普通配置
    • 传递的方式: 对象中使用querykey作为传递方式
    • 传递后形成的路径: /router?id=123, /router?id=abc
this.$router.push({
    
    
  path:'/xxx'
  query:{
    
    
    id:id
  }
})
  • 如何使用
    • <router-link>的方式
    • JavaScript代码方式
3.7.3 传递参数方式一
  • <router-link>的方式
    • <router-link :to="{ path: '/路由/' + 参数, query: { key: value } }"></router-link>
      在这里插入图片描述
3.7.4 传递参数方式二
  • JavaScript代码形式
    • 添加方法
    • this.$router.push
    • path: '/路由/' + 参数
    • query: { key : value }
      在这里插入图片描述
3.7.5 接收参数
  • 接收参数通过$route对象获取的
    • 在使用了 vue-router 的应用中,路由对象会被注入每个组件中,赋值为 this.$route
    • 并且当路由切换时,路由对象会被更新
  • 通过$route获取传递的信息如下
    • this.$route.params.id
    • this.$route.query.name/age
      在这里插入图片描述
3.7.6 $route$router是有区别的
  • $route$router是有区别的
    • $routerVueRouter实例,想要导航到不同URL,则使用$router.push方法
    • $route为当前router跳转对象,里面可以获取name、path、query、params
      在这里插入图片描述

3.8 路由导航守卫

3.8.1 什么是导航守卫
  • vue-router提供的导航守卫主要用来监听路由的进入和离开
  • vue-router提供了beforeEachafterEach钩子函数
  • 它们会在路由即将改变前改变后触发
3.8.2 为什么使用导航守卫?
  • 需求: 在一个单页面应用中, 如何改变网页的标题呢?
    • 网页标题是通过<title>来显示的, 但是SPA应用只有一个固定的HTML, 切换不同的页面时, 标题并不会改变
    • 可以通过JavaScript来修改<title>的内容
      • window.document.title = '新的标题'
    • Vue项目中, 在哪里修改? 什么时候修改?
  • 普通的修改方式
    • 每一个路由对应的组件.vue文件中
    • 通过mounted声明周期函数, 执行对应的代码进行修改即可
    • 当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码)
  • 导航守卫方式
3.8.3 全局导航守卫
  • 全局守卫
    • beforeEach 进入组件前调用
    • afterEach 进入组件后调用
  • 可以利用beforeEach来完成标题的修改
    • 首先, 在钩子当中定义一些标题, 可以利用meta来定义(meta元数据--描述数据的数据
    • 其次, 利用导航守卫,修改标题
// 2.创建VueRouter对象
const routes = [
  {
    
    
    path: '',
    // redirect重定向
    redirect: '/home'
  },
  {
    
    
    path: '/home',
    component: Home,
    meta: {
    
    
      title: '首页'
    },
    children: [
      // {
    
    
      //   path: '',
      //   redirect: 'news'
      // },
      {
    
    
        path: 'news',
        component: HomeNews
      },
      {
    
    
        path: 'message',
        component: HomeMessage
      }
    ]
  },
  {
    
    
    path: '/about',
    component: About,
    meta: {
    
    
      title: '关于'
    }
  },
  {
    
    
    path: '/user/:id',
    component: User,
    meta: {
    
    
      title: '用户'
    },
  },
  {
    
    
    path: '/profile',
    component: Profile,
    meta: {
    
    
      title: '档案'
    },
  }
]
...
// 前置守卫(guard)
router.beforeEach((to, from, next) => {
    
    
  // 从from跳转到to
  document.title = to.matched[0].meta.title
  next()
})

## beforeEach 源码
beforeEach (guard: NavigationGuard): Function;

export type NavigationGuard<V extends Vue = Vue> = (
  to: Route,
  from: Route,
  next: (to?: RawLocation | false | ((vm: V) => any) | void) => void
) => any
 
  • 导航钩子的三个参数解析
    • to: 即将要进入的目标的路由对象
    • from: 当前导航即将要离开的路由对象
    • next: 调用该方法后, 才能进入下一个钩子
  • 注意
    • 如果是后置钩子, 也就是afterEach
    • 不需要主动调用next()函数
// 后置钩子(hook)
router.afterEach((to, from) => {
    
    

})

## afterEach 源码
afterEach (hook: (to: Route, from: Route) => any): Function;
3.8.4 路由独享的守卫
  • 路由独享的守卫
    • beforeEnter 进入指定的组件之前调用
const router = new VueRouter({
    
    
	routes: [
		{
    
    
			path: '/foo',
			component: Foo,
			beforeEnter: (to, from, next) => {
    
    
				// ...
			}
		}
	]
})
3.8.5 组件内的守卫
  • 组件内的守卫
    • 在路由组件内直接定义路由导航守卫
    • beforeRouteEnter
    • beforeRouterUpdate
    • beforeRouteLeave
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`
	}
}
3.8.6 完整的导航解析流程
  1. 导航被触发
  2. 在失活的组件里调用 beforeRouteLeave 守卫
  3. 调用全局的 beforeEach 守卫
  4. 在重用的组件里调用 beforeRouteUpdate 守卫
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)
  9. 导航被确认
  10. 调用全局的 afterEach 钩子
  11. 触发 DOM 更新
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

3.9 keep-alive

  • keep-aliveVue 内置的一个组件
    • 可以使被包含的组件保留状态,或避免重新渲染
    • 两个重要的属性
      • include - 字符串或正则表达,只有匹配的组件会被缓存
      • exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
  • router-view 也是一个组件
    • 如果直接被包在 keep-alive 里面所有路径匹配到的视图组件都会被缓存
<keep-alive>
	<router-view>
		<!-- 所有路径匹配到的视图组件都会被缓存! -->
	</router-view>
</keep-alive>
  • 通过create声明周期函数来验证
3.9.1 记录路由信息
  • 记录离开时的路由信息,当返回时依然是这个路由
<template>
  <div>
    <h2>我是首页</h2>
    <p>我是首页内容, 哈哈哈</p>

    <router-link to="/home/news">新闻</router-link>
    <router-link to="/home/message">消息</router-link>

    <router-view></router-view>

    <h2>{
   
   {message}}</h2>
  </div>
</template>

<script>
  export default {
     
     
    name: "Home",
    data() {
     
     
      return {
     
     
        message: '你好啊',
        path: '/home/news' // 默认显示新闻的路由
      }
    },
    created() {
     
     
      console.log('home created');
    },
    destroyed() {
     
     
      console.log('home destroyed');
    },
    // 这两个函数, 只有该组件被保持了状态使用了keep-alive时, 才是有效的
    activated() {
     
     
      this.$router.push(this.path);
      console.log('activated');
    },
    deactivated() {
     
     
      console.log('deactivated');
    },
    beforeRouteLeave (to, from, next) {
     
     
      console.log(this.$route.path);
      this.path = this.$route.path; // 离开前将路由记录在默认path中
      next()
    }
  }
</script>

<style scoped>

</style>

3.10 TabBar练习

3.10.1 TabBar 实现思路
  • 如何封装自定义 TabBar 组件
    • TabBar处于底部,并且设置相关的样式
    • TabBar中显示的内容由外界决定
      • 插槽
      • flex布局平分TabBar
    • 自定义TabBarItem,可以传入图片和文字
      • 定义TabBarItem,并且定义两个插槽:图片、文字
      • 给两个插槽外层包装div,用于设置样式
      • 填充插槽,实现底部TabBar的效果
    • 传入高亮图片
      • 定义另外一个插槽,插入active-icon的数据
      • 定义一个变量isActive,通过v-show来决定是否显示对应的icon
    • TabBarItem 绑定路由数据
      • 安装路由:npm install vue-router —save
      • 完成router/index.js的内容,以及创建对应的组件
      • main.js中注册router
      • APP中加入<router-view>组件
    • 点击 item 跳转到对应路由,并且动态决定 isActive
      • 监听item的点击,通过this.$router.replace()替换路由路径
      • 通过this.$route.path.indexOf(this.link) !== -1来判断是否是active
    • 动态计算 active 样式
      • 封装新的计算属性this.isActive ? {'color': 'red'} : {}
        在这里插入图片描述
3.10.2 代码实现
  • TabBar.vue
<template>
  <div id="tab-bar">
    <slot></slot>
  </div>
</template>

<script>
  export default {
     
     
    name: "TabBar"
  }
</script>

<style scoped>
  #tab-bar {
     
     
    display: flex;
    background-color: #f6f6f6;

    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;

    box-shadow: 0 -1px 1px rgba(100,100,100,.2);
  }
</style>
  • TabBarItem.vue
<template>
  <!--所有的item都展示同一个图片, 同一个文字-->
  <div class="tab-bar-item" @click="itemClick">
    <div v-if="!isActive"><slot name="item-icon"></slot></div>
    <div v-else><slot name="item-icon-active"></slot></div>
    <div :style="activeStyle"><slot name="item-text"></slot></div>
  </div>
</template>

<script>
  export default {
     
     
    name: "TabBarItem",
    props: {
     
     
      path: String,
      activeColor: {
     
     
        type: String,
        default: 'red'
      }
    },
    data() {
     
     
      return {
     
     
        // isActive: true
      }
    },
    computed: {
     
     
      isActive() {
     
     
        // /home -> item1(/home) = true
        // /home -> item1(/category) = false
        // /home -> item1(/cart) = true
        // /home -> item1(/profile) = true
        return this.$route.path.indexOf(this.path) !== -1
      },
      activeStyle() {
     
     
        return this.isActive ? {
     
     color: this.activeColor} : {
     
     }
      }
    },
    methods: {
     
     
      itemClick() {
     
     
        this.$router.replace(this.path)
      }
    }
  }
</script>

<style scoped>
  .tab-bar-item {
     
     
    flex: 1;
    text-align: center;
    height: 49px;
    font-size: 14px;
  }

  .tab-bar-item img {
     
     
    width: 24px;
    height: 24px;
    margin-top: 3px;
    vertical-align: middle;
    margin-bottom: 2px;
  }
</style>
  • MainTabBar.vue
<template>
  <tab-bar>
    <tab-bar-item path="/home" activeColor="pink">
      <img slot="item-icon" src="~assets/img/tabbar/home.svg" alt="">
      <img slot="item-icon-active" src="~assets/img/tabbar/home_active.svg" alt="">
      <div slot="item-text">首页</div>
    </tab-bar-item>
    <tab-bar-item path="/category" activeColor="pink">
      <img slot="item-icon" src="../../assets/img/tabbar/category.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/category_active.svg" alt="">
      <div slot="item-text">分类</div>
    </tab-bar-item>
    <tab-bar-item path="/cart" activeColor="pink">
      <img slot="item-icon" src="../../assets/img/tabbar/shopcart.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/shopcart_active.svg" alt="">
      <div slot="item-text">购物车</div>
    </tab-bar-item>
    <tab-bar-item path="/profile" activeColor="deepPink">
      <img slot="item-icon" src="../../assets/img/tabbar/profile.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/profile_active.svg" alt="">
      <div slot="item-text">我的</div>
    </tab-bar-item>
  </tab-bar>
</template>

<script>
  import TabBar from 'components/tabbar/TabBar'
  import TabBarItem from 'components/tabbar/TabBarItem'

  export default {
     
     
    name: "MainTabBar",
    components: {
     
     
      TabBar,
      TabBarItem
    }
  }
</script>

<style scoped>

</style>

4. Vuex 详解

4.1 认识 Vuex

4.1.1 Vuex 作用
  • 官方解释
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
      • 采用 集中式存储管理 应用的所有组件的状态
      • 并以相应的规则保证状态以一种可预测的方式发生变化
    • Vuex 也集成到 Vue 的官方调试工具 devtools extension
      • 提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能
  • 状态管理
    • 将其看成把需要多个组件共享的变量全部存储在一个对象里面
    • 将这个对象放在顶层的Vue实例中,让其他组件可以使用
    • 多个组件就可以共享这个对象中的所有变量属性
    • Vuex就是为了提供这样一个在多个组件间共享状态的插件
4.1.2 管理什么状态
  • 多个状态,在多个界面间的共享问题
    • 比如用户的登录状态用户名称头像地理位置信息等等
    • 比如商品的收藏购物车中的物品等等
    • 这些状态信息,都可以放在统一的地方,对它进行保存和管理,而且数据还是响应式
4.1.3 单界面的状态管理
  • State:就是组件的状态
  • View视图层,可以针对State的变化,显示不同的信息
  • Actions:这里的Actions主要是用户的各种操作点击、输入等等,会导致状态的改变
    在这里插入图片描述
4.1.4 单界面状态管理的实现
  • 案例
    • counter需要某种方式被记录下来,也就是State
    • counter目前的值需要被显示在界面中,也就是View部分
    • 界面发生某些操作时(用户的点击,也可以是用户的input),需要去更新状态,也就是Actions
<template>
	<div class="test">
		<div>当前计数:{
   
   {counter}}</div>
		<button @click="counter+=1">+1</button>
		<button @click="counter-=1">-1</button>
	</div>
</template>

<script>
	export default {
     
     
		name: 'HelloWorld',
		data() {
     
     
			return {
     
     
				counter: 0
			}
		}
	}
</script>
4.1.5 多界面状态管理
  • 多个视图都依赖同一个状态
    • 一个状态改了,多个界面需要进行更新
    • 不同界面的 Actions 都想修改同一个状态
      • Home.vue需要修改,Profile.vue也需要修改这个状态
  • 如何理解
    • 对于某些状态(状态1/状态2/状态3)来说只属于某一个视图
      • 状态1/状态2/状态3 自己管理自己用
    • 但是也有一些状态(状态a/状态b/状态c)属于多个视图共同想要维护
      • 状态a/状态b/状态c 统一管理
    • Vuex就是提供统一管理工具
4.1.6 全局单例模式
  • Vuex基本思想
    • 将共享的状态抽取出来,交给Vuex统一进行管理
    • 每个视图,按照规定好的规定,进行访问和修改等操作
4.1.7 Vuex状态管理图例
  • Devtools 工具
    • 对组件变化状态进行跟踪
    • 通过 Devtools 知道哪个具体的组件改变了 State
      在这里插入图片描述

4.2 Vuex 基本使用

4.2.1 简单的案例
  • 创建一个文件夹store
  • 在其中创建一个index.js文件
  • index.js 中存放Vuex代码
import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

const store = new Vuex.Store({
    
    
	state: {
    
    
		count: 0
	},
	mutations: {
    
    
		increment(state) {
    
    
			state.count++
		},
		decrement(state) {
    
    
			state.count--
		}
	}
})
4.2.2 挂载到 Vue 实例中
  • 引入 Vuex
    • main.js文件,导入store对象,并且放在new Vue
    • 在其他Vue组件中,就可以通过this.$store的方式,获取到这个store对象
  • main.js
import Vue from 'vue'
import App from './App'
import store from './store'

new Vue({
    
    
	el: '#app',
	store,
	render: h => h(App)
})
4.2.3 使用 Vuex 的 count
<template>
	<div id="app">
		<p>{
   
   {count}}</p>
		<button @click="increment">+1</button>
		<button @click="decrement">-1</button>
	</div>
</template>

<script>
	export default {
     
     
		name: 'App',
		components: {
     
     
		},
		computed: {
     
     
			count: function() {
     
     
				return this.$store.state.count // 拿到 Vuex 中的 count
			}
		},
		methods: {
     
     
			increment: function() {
     
     
				this.$store.commit('increment')
			},
			decrement: function() {
     
     
				this.$store.commit('decrement')
			}
		}
	}
</script>
  • 这就是使用Vuex最简单的方式了
  • 我们来对使用步骤,做一个简单的小节:
    1. 提取出一个公共的store对象,用于保存在多个组件中共享的状态
    2. store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
    3. 在其他组件中使用store对象中保存的状态即可
      • 通过this.$store.state.属性的方式来访问状态
      • 通过this.$store.commit('mutation中方法')修改状态
  • 注意事项:
    • 我们通过提交mutation的方式,而非直接改变store.state.count
    • 这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值

5. Vuex核心概念

1. state

State单一状态树
  • Vuex提出使用单一状态树, 什么是单一状态树呢?
    • 英文名称是Single Source of Truth,也可以翻译成单一数据源
  • 但是,它是什么呢?我们来看一个生活中的例子
    • 我用一个生活中的例子做一个简单的类比
    • 我们知道,在国内我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等。
    • 这些信息被分散在很多地方进行管理,有一天需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。
    • 这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。
  • 这个和我们在应用开发中比较类似:
    • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理维护等等都会变得特别困难。
    • 所以Vuex也使用了单一状态树管理应用层级的全部状态
    • 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护调试过程中,也可以非常方便的管理维护

2. Getters

Getters基本使用
  • 有时候,我们需要从store中获取一些state变异后的状态,比如下面的Store中:
    • 获取学生年龄大于20的个数
const store = new Vuex.Store({
    
    
	state: {
    
    
		students: [
			{
    
    id: 110, name: 'zs', age: 18},
			{
    
    id: 111, name: 'ls', age: 21},
			{
    
    id: 112, name: 'we', age: 25},
			{
    
    id: 113, name: 'zx', age: 30}
		]
	}
})
  • 我们可以在Store中定义getters
computed: {
    
    
	getGreaterAgesCount() {
    
    
		return this.$store.students.filter(age => age >= 20).length
	}
},
getters: {
    
    
	greaterAgesCount: state => {
    
    
		return state.students.filter(s => s.age >= 20).length
	}
}
Getters作为参数和传递参数
  • 如果我们已经有了一个获取所有年龄大于20岁学生列表的getters, 那么代码可以这样来写
getters: {
    
    
	greaterAgesStus: state => {
    
    
		return state.students.filter(s => s.age >= 20)
	},
	greaterAgesCount: (state,getters) => {
    
    
		return getters.greaterAgesStus.length
	}
}
  • getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数.
    • 比如上面的案例中,我们希望根据ID获取用户的信息
getters: {
    
    
	stuByID: state => {
    
    
		return id => {
    
    
			return state.students.find(s => s.id === id)
		}
	}
}

3. Mutation

Mutation状态更新
  • Vuexstore状态的更新唯一方式:提交Mutation
  • Mutation主要包括两部分:
    • 字符串的事件类型(type)
    • 一个回调函数(handler),该回调函数的第一个参数就是state
  • mutation的定义方式:
mutations: {
    
    
	increment(state) {
    
    
		state.count++
	}
}
  • 通过mutation更新
increment: function () {
    
    
	this.$store.commit('increment')
}
Mutation传递参数
  • 在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数
    • 参数被称为是mutation的载荷(Payload)
  • Mutation中的代码:
decrement(state,n) {
    
    
	state.count -= n
}
decrement: function () {
    
    
	this.$store.commit('decrement',2)
}
  • 但是如果参数不是一个呢?
    • 比如我们有很多参数需要传递.
    • 这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象
    • 这个时候可以再从对象中取出相关的信息
changeCount(state,payload) {
    
    
	state.count = payload.count
}
changeCount: function () {
    
    
	this.$store.commit('changeCount',{
    
    count: 0})
}
Mutation提交风格
  • 上面的通过commit进行提交是一种普通的方式
  • Vue还提供了另外一种风格, 它是一个包含type属性的对象
this.$store.commit({
    
    
	type: 'changeCount',
	count: 100
})
  • Mutation中的处理方式是将整个commit的对象作为payload使用, 所以代码没有改变, 依然如下:
changeCount(state,payload) {
    
    
	state.count = payload.count
}
Mutation响应规则
  • Vuexstore中的state响应式的, 当state中的数据发生改变时, Vue组件会自动更新
  • 这就要求我们必须遵守一些Vuex对应的规则:
    • 提前在store中初始化好所需的属性.
    • 当给state中的对象添加新属性时, 使用下面的方式:
      • 方式一: 使用Vue.set(obj, 'newProp', 123)
      • 方式二: 用新对象给旧对象重新赋值
  • 我们来看一个例子:
    • 当我们点击更新信息时, 界面并没有发生对应改变.
  • App.vue
<template>
	<div id="app">
		<p>我的个人信息:{
   
   {info}}</p>
		<button @click="updateInfo">更新信息</button>
	</div>
</template>

<script>
	export default {
     
     
		name: 'App',
		components: {
     
     
		},
		computed: {
     
     
			info () {
     
     
				return this.$store.state.info
			}
		},
		methods: {
     
     
			updateInfo () {
     
     
				this.$store.commit('updateInfo', {
     
     height: 1.88})
			}
		}
	}
</script>
  • Vuex
const store = new Vuex.Store({
    
    
	state: {
    
    
		info: {
    
    
			name: 'xxx', age: 18
		}
	},
	mutations: {
    
    
		updateInfo(state,payload) {
    
    
			state.info['height'] = payload.height
		}
	}
})
  • 如何才能让它改变呢?
    • 查看下面代码的方式一和方式二
    • 都可以让state中的属性是响应式
mutations: {
    
    
	updateInfo(state,payload) {
    
    
		// state.info['height'] = payload.height
		// 方式一: Vue.set()
		Vue.set(state.info, 'height', payload.height)
		// 方式二: 给 info 赋值一个新的对象
		state.info = {
    
    ...state.info, 'height': payload.height}
	}
}
Mutation常量类型 – 概念
  • 我们来考虑下面的问题:
    • mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
    • 当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
    • 方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.
  • 如何避免上述的问题呢?
    • 在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
    • 我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.
  • 具体怎么做呢?
    • 我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
    • 定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.
Mutation常量类型 – 代码
  • mutation-types.js
export const UPDATE_INFO = 'UPDATE_INFO'
  • index.js
import Vuex from 'vuex'
import Vue from 'vue'
import * as types from './mutation-types'

Vue.use(Vuex)

const store = new Vuex.Store({
    
    
	state: {
    
    
		info: {
    
    
			name: 'xxx', age: 18
		}
	},
	mutations: {
    
    
		[types.UPDATE_INFO](state, payload) {
    
    
			state.info = {
    
    ...state.info, 'height': payload.height}
		}
	}
})

export default store
  • App.vue
<script>
	import {
     
     UPDATE_INFO} from "./store/mutation-types";

	export default {
     
     
		name: 'App',
		components: {
     
     
		},
		computed: {
     
     
			info() {
     
     
				return this.$store.state.info
			}
		},
		methods: {
     
     
			updateInfo() {
     
     
				this.$store.commit(UPDATE_INFO, {
     
     height: 1.88})
			}
		}
	}		
</script>
Mutation同步函数
  • 通常情况下, Vuex要求我们Mutation中的方法必须是同步方法
    • 主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照.
    • 但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.
  • 比如我们之前的代码, 当执行更新时, devtools中会有如下信息:
    在这里插入图片描述
  • 但是, 如果Vuex中的代码, 我们使用了异步函数:
mutations: {
    
    
	[types.UPDATE_INFO](state,payload) {
    
    
		setTimeout(() => {
    
    
			state.info = {
    
    ...state.info, 'height': payload.height}
		},10000)
	}
}			

在这里插入图片描述

4. Action

Action基本定义
  • 我们强调, 不要再Mutation中进行异步操作
    • 但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
    • Action类似于Mutation, 但是是用来代替Mutation进行异步操作
  • Action的基本使用代码如下:
  • context是什么?
    • context是和store对象具有相同方法和属性的对象.
    • 也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等.
    • 但是注意, 这里它们并不是同一个对象
  • 这样的代码是否多此一举呢?
    • 事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了
const store = new Vuex.Store({
    
    
	state: {
    
    
		count: 0
	},
	mutations: {
    
    
		increment(state) {
    
    
			state.count++
		}
	},
	actions: {
    
    
		increment(context) {
    
    
			context.commit('increment')
		}
	}
})	
Action的分发
  • Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch
methods: {
    
    
	increment() {
    
    
		this.$store.dispatch('increment')
	}
}
  • 同样的, 也是支持传递payload
methods: {
    
    
	increment() {
    
    
		this.$store.dispatch('increment',{
    
    cCount: 5})
	}
}
mutations: {
    
    
	increment(state,payload) {
    
    
		state.count += payload.cCount
	}
},
actions: {
    
    
	increment(context,payload) {
    
    
		setTimeout(() => {
    
    
			context.commit('increment',payload)
		},5000)
	}
}
Action返回的Promise
  • 学习ES6语法的时候, Promise经常用于异步操作
    • Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolvereject
  • 来看下面的代码:
actions: {
    
    
	increment(context) {
    
    
		return new Promise((resolve) => {
    
    
			setTimeout(() => {
    
    
				context.commit('increment')
				resolve()
			},1000)
		})
	}
}
methods: {
    
    
	increment() {
    
    
		this.$store.dispatch('increment').then(res => {
    
    
			console.log('完成了更新操作')
		})
	}
}

5. Module

认识Vuex的Module
  • Module是模块的意思, 为什么在Vuex中我们要使用模块呢?
    • Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理.
    • 当应用变得非常复杂时,store对象就有可能变得相当臃肿.
    • 为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的statemutationsactionsgetters
  • 我们按照什么样的方式来组织模块呢?
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 的状态
Module的局部状态
  • 上面的代码中, 我们已经有了整体的组织结构, 下面我们来看看具体的局部模块中的代码如何书写.
    • 我们在moduleA中添加statemutationsgetters
    • mutationgetters接收的第一个参数是局部状态对象
const moduleA = {
    
    
	state: {
    
    
		count: 0
	},
	mutations: {
    
    
		increment(state) {
    
    
			state.count++
		}
	},
	getters: {
    
    
		doubleCount(state) {
    
    
			return state.count * 2
		}
	}
}

const moduleB = {
    
    
}

const store = new Vuex.Store({
    
    
	state: {
    
    
		gCount: 111
	},
	modules: {
    
    
		a: moduleA,
		b: moduleB
	}
})

export default store;
<script>
	export default {
     
     
		name: 'App',
		components: {
     
     
		},
		computed: {
     
     
			count() {
     
     
				return this.$store.getters.doubleCount
			}
		},
		methods: {
     
     
			increment() {
     
     
				this.$store.commit('increment')
			}
		}
	}
</script>
  • 注意:
    • 虽然, 我们的doubleCountincrement都是定义在对象内部
    • 但是在调用的时候, 依然是通过this.$store来直接调用的
Module的Actions写法
  • actions的写法呢? 接收一个context参数对象
    • 局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
const moduleA = {
    
    
	// ...
	actions: {
    
    
		incrementIfOddOnRootSum ({
    
     state,commit,rootState }) {
    
    
			if ((state.count + rootState.count) % 2 === 1) {
    
    
				commit('increment')
			}
		}
	}
}	
  • 如果getters中也需要使用全局的状态, 可以接受更多的参数
const moduleA = {
    
    
	// ...
	getters: {
    
    
		sumWithRootCount (state,getters,rootState) {
    
    
			return state.count + rootState.count
		}
	}
}

5. 网络模块封装

1. 网络模块的选择

Vue中发送网络请求有非常多的方式, 那么, 在开发中, 如何选择呢?

1. 选择 Ajax
  • 传统的Ajax是基于XMLHttpRequest(XHR)
  • 为什么不用它呢?
    • 非常好解释, 配置调用方式等非常混乱.
    • 编码起来看起来就非常蛋疼.
    • 所以真实开发中很少直接使用, 而是使用jQuery-Ajax
2. 选择 jQuery-Ajax
  • 在前面的学习中, 我们经常会使用jQuery-Ajax,相对于传统的Ajax非常好用.
  • 为什么不用它呢?
    • 首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
    • 那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
    • jQuery的代码1w+行.
    • Vue的代码才1w+行.
    • 完全没有必要为了用网络请求就引用这个重量级的框架.
3. 选择 Vue-resource
  • 官方在Vue1.x的时候, 推出了Vue-resource.
    • Vue-resource的体积相对于jQuery小很多.
    • 另外Vue-resource是官方推出的.
  • 为什么不选择它呢?
    • Vue2.0退出后, Vue作者就在GitHubIssues中说明了去掉vue-resource, 并且以后也不会再更新.
    • 那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新维护.
    • 对以后的项目开发和维护都存在很大的隐患.
4. 选择 axios
  • 在说明不再继续更新和维护vue-resource的同时, 作者还推荐了一个框架: axios
  • axios有非常多的优点, 并且用起来也非常方便.

2. jsonp的封装

jsonp
  • 在前端开发中, 我们一种常见的网络请求方式就是JSONP
    • 使用JSONP最主要的原因往往是为了解决跨域访问的问题.
  • JSONP的原理是什么呢?
    • JSONP的核心在于通过<script>标签src来帮助我们请求数据.
    • 原因是我们的项目部署在domain1.com服务器上时, 是不能直接访问domain2.com服务器上的资料的.
    • 这个时候, 我们利用<script>标签src帮助我们去服务器请求到数据, 将数据当做一个javascript的函数来执行, 并且执行的过程中传入我们需要的json.
    • 所以, 封装jsonp的核心就在于我们监听window上的jsonp进行回调时的名称.

在这里插入图片描述

JSONP封装
let count = 1
export default function originPJSONP(option) {
    
    
	// 1. 从传入的option中提取URL
	const url = option.url
	
	// 2. 在body中添加script标签
	const body = document.getElementsByTagName('body')[0]
	const script = document.createElement('script');

	// 3. 内部生产一个不重复的callback
	const callback = 'jsonp' + count++

	// 4. 监听window上的jsonp的调用
	return new Promise((resolve,reject) => {
    
    
		try {
    
    
			window[callback] = function (result) {
    
    
				body.removeChild(script);
				resolve(result)
			}
			const params = handleParam(option.data);
			script.src = url + '?callback=' + callback + params;
			body.appendChild(script)
		} catch (e) {
    
    
			body.removeChild(script)
			reject(e)
		}
	})
}
function handleParam(data) {
    
    
	let url = ''
	for (let key in data) {
    
    
		let value = data[key] !== undefined ? data[key] : ''
		url += `&${
      
      key}=${
      
      encodeURIComponent(value)}`
	}
	return url
}

3. axios的使用

1. 认识 axios
  • 为什么选择axios

    • 功能特点:
      • 在浏览器中发送 XMLHttpRequests 请求
      • node.js 中发送 http请求
      • 支持 Promise API
      • 拦截请求和响应
      • 转换请求和响应数据
      • 等等
  • axios请求方式

    • 支持多种请求方式:
axios(config)
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
2. 发送基本请求
  • 发送get请求演示
import axios from 'axios'

export default {
    
    
	name: 'app',
	created() {
    
    
		// 提问:为什么这里没有跨域的问题
		// 1. 没有请求参数
		axios.get('接口地址')
			.then(res => {
    
    
			console.log(res);
		  }).catch(err => {
    
    
		  	console.log(err);
		  })

		// 2. 有请求参数
		axios.get('接口地址',
			{
    
    params: {
    
    type: 'sell', page: 1}})
			.then(res => {
    
    
				console.log(res);
			}).catch(err => {
    
    
				console.log(err);
		})
	}
}
  • 发送并发请求演示
    • 有时候, 我们可能需求同时发送两个请求
      • 使用axios.all, 可以放入多个请求数组.
      • axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2
import axios from 'axios'

export default {
    
    
	name: 'app',
	created() {
    
    
		// 发送并发请求
		axios.all([axios.get('接口地址'),
		axios.get('接口地址',
			{
    
    params: {
    
    type: 'sell', page: 1}})])
	.then(axios.spread((res1, res2) => {
    
    
		console.log(res1);
		console.log(res2);
	}))
}
  • 全局配置
    • 在上面的示例中, 我们的BaseURL固定
      • 事实上, 在开发中可能很多参数都是固定的.
      • 这个时候我们可以进行一些抽取, 也可以利用axios全局配置
axios.defaults.baseURL =123.207.32.32:8000’
axios.defaults.headers.post[‘Content-Type’] = ‘application/x-www-form-urlencoded’;
created() {
    
    
	// 提取全局的配置
	axios.defaults.baseURL = '接口地址'

	// 发送并发请求
	axios.all([axios.get('/category'),
			  axios.get('/home/data',
			  	{
    
    params: {
    
    type: 'sell', page: 1}})])
		.then(axios.spread((res1, res2) => {
    
    
			console.log(res1);
			console.log(res2);
		}))
}		  
  • 常见的配置选项
## 请求地址
url: '/user',

## 请求类型
method: 'get',

## 请根路径
baseURL: 'http://www.mt.com/api',

## 请求前的数据处理
transformRequest:[function(data){
    
    }],

## 请求后的数据处理
transformResponse: [function(data){
    
    }],

## 自定义的请求头
headers:{
    
    'x-Requested-With':'XMLHttpRequest'},

## URL查询对象
params:{
    
     id: 12 },

## 查询对象序列化函数
paramsSerializer: function(params){
    
     }

## request body
data: {
    
     key: 'aa'},

## 超时设置s
timeout: 1000,

## 跨域是否带Token
withCredentials: false,

## 自定义请求处理
adapter: function(resolve, reject, config){
    
    },

## 身份验证信息
auth: {
    
     uname: '', pwd: '12'},

## 响应的数据格式 json / blob /document /arraybuffer / text / stream
responseType: 'json',

4. axios的实例

axios的实例
  • 为什么要创建axios的实例呢?
    • 当我们从axios模块中导入对象时, 使用的实例是默认的实例.
    • 当给该实例设置一些默认配置时, 这些配置就被固定下来了.
    • 但是后续开发中, 某些配置可能会不太一样.
    • 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等.
    • 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.
// 创建新的实例
const axiosInstance = axios.create({
    
    
	baseURL: '接口地址',
	timeout: 5000,
	headers: {
    
    
		'Content-Type': 'application/x-www-form-urlencoded'
	}
})
// 发送网络请求
axiosInstance({
    
    
	url: '/category',
	method: 'get'
}).then(res => {
    
    
	console.log(res);
}).catch(err => {
    
    
	console.log(err);
})
axios 封装
import originAxios from 'axios'

export default function axios(options) {
    
    
	return new Promise((resolve,reject) => {
    
    
		// 1. 创建axios的实例
		const instance = originAxios.create({
    
    
			baseURL: '/api',
			timeout: 5000,
			headers: ''
		});

		// 2. 传入对象进行网络请求
		instance(option).then(res => {
    
    
			resolve(res)
		}).catch(err => {
    
    
			reject(err)
		})
	})
}

5. 拦截器

1. 如何使用拦截器?
  • axios提供了拦截器,用于我们在发送每次请求或者得到相应后,进行对应的处理。
  • 如何使用拦截器呢?
// 配置请求和响应拦截
instance.interceptors.request.use(config => {
    
    
	console.log('来到了request拦截success中');
	return config
}, err => {
    
    
	console.log('来到了request拦截failure中');
	return err
})

instance.interceptors.response.use(response => {
    
    
	console.log('来到了request拦截success中');
	return response.data
}, err => {
    
    
	console.log('来到了request拦截failure中');
	return err
})
axios({
    
    
	url: '/home/data',
	method: 'get',
	params: {
    
    
		type: 'sell',
		page: 1
	}
}).then(res => {
    
    
	console.log(res);
}).catch(err => {
    
    
	console.log(err);
})

在这里插入图片描述

2. 拦截器中都做什么呢?
  • 请求拦截可以做到的事情:
instance.interceptors.request.use(config => {
    
    
	console.log('来到了request拦截success中');
	// 1. 当发送网络请求时,在页面中添加一个loading组件,作为动画

	// 2. 某些请求要求用户必须登录,判断用户是否有token,如果没有token跳转到login页面

	// 3. 对请求的参数进行序列化
	config.data = qs.stringify(config.data)
	console.log(config);

	// 4. 等等
	return config
  • 请求拦截中错误拦截较少,通常都是配置相关的拦截
    • 可能的错误比如请求超时,可以将页面跳转到一个错误页面中。
  • 响应拦截中完成的事情:
    • 响应的成功拦截中,主要是对数据进行过滤。

在这里插入图片描述

instance.interceptors.response.use(response => {
    
    
	console.log('来到了request拦截success中');
	return response.data
  • 响应的失败拦截中,可以根据status判断报错的错误码,跳转到不同的错误提示页面。
err => {
    
    
	console.log('来到了request拦截failure中');
	if (err && err.response) {
    
    
		switch (err.response.status) {
    
    
			case 400:
				err.message = '请求错误'
				break
			case 401:
				err.message = '未授权的访问'
				break
		}
	}
	return err

6. 总结

  • 对于Vue框架学习的总结笔记,复习的时候参考

猜你喜欢

转载自blog.csdn.net/qq_43645678/article/details/109036024
今日推荐