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);
常见的模块化规范
CommonJS
、AMD
、CMD
,也有ES6
的modules
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
是什么意思?CLI
是Command-Line Interface
, 翻译为命令行界面
, 但是俗称脚手架
Vue CLI
是一个官方发布vue.js
项目脚手架- 使用
vue-cli
可以快速搭建Vue
开发环境以及对应的webpack
配置
2.1.2 Vue CLI 使用前提 - Node
安装NodeJS
检测安装的版本
- 默认情况下自动安装
Node
和NPM
- 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-Compiler
和Runtime-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配置
”,移除的配置文件根目录下的,build
和config
等目录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的优化
缺点
- 一种情况是
整个页面的模块由后端人员来编写和维护的
- 另一种情况是
前端开发人员如果要开发页面, 需要通过
PHP和
Java等语言来编写页面代码
- 而且通常情况下
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
URL
的hash
也就是锚点(#)
, 本质上是改变window.location
的href
属性- 可以通过
直接赋值location.hash
来改变href
, 但是页面不发生刷新
3.2.2 HTML5 的 history 模式
history
接口是HTML5
新增的, 它有五种模式改变URL
而不刷新页面
pushState
history.pushState()
- history.pushState()方法
向浏览器历史添加了一个状态(增加一个记录)
- pushState()方法带有三个参数:
一个状态对象、一个标题(现在被忽略了)以及一个可选的URL地址
- pushState方法
不会触发页面刷新
,只是导致history对象发生变化
,地址栏会有反应
- 如果pushState的
url参数
,设置了一个新的锚点值(即hash)
,并不会触发hashchange事件
- 如果设置了一个
跨域网址
,则会报错
- history.pushState()方法
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
- 目前前端流行的三大框架, 都有自己的路由实现
Angular
的ngRouter
React
的ReactRouter
Vue
的vue-router
vue-router
vue-router
是Vue.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
- 如果希望使用
HTML5
的history
模式, 进行如下配置即可:
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-active
的class
- 设置
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
- vue 源码中在 data 中
3.4.6 动态路由
- 当页面的
path
路径不确定
时- 比如
进入用户界面
时,希望是如下的路径:/user/aaaa
或/user/bbbb
- 除了前面的
/user
之外,后面还跟上了用户的 ID
- 比如
- 这种
path
和Component
的匹配关系,称之为动态路由
(也是路由传递数据
的一种方式) 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 传递参数的方式
传递参数
主要有两种类型:params
和query
params
的类型配置路由格式
:/router/:id
传递的方式
: 在path
后面跟上对应的值
传递后形成的路径
:/router/123
,/router/abc
this.$router.push({
name:'xxx'
params:{
id:id
}
})
query
的类型配置路由格式
:/router
, 也就是普通配置
传递的方式
: 对象中使用query
的key
作为传递方式传递后形成的路径
:/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
是有区别的$router
为VueRouter
实例,想要导航到不同URL
,则使用$router.push
方法$route
为当前router
跳转对象,里面可以获取name、path、query、params
等
3.8 路由导航守卫
3.8.1 什么是导航守卫
vue-router
提供的导航守卫主要用来监听路由的进入和离开
的vue-router
提供了beforeEach
和afterEach
的钩子函数
- 它们会在路由即将
改变前
和改变后
触发
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 完整的导航解析流程
- 导航被触发
- 在失活的组件里调用 beforeRouteLeave 守卫
- 调用全局的 beforeEach 守卫
- 在重用的组件里调用 beforeRouteUpdate 守卫
- 在路由配置里调用 beforeEnter
- 解析异步路由组件
- 在被激活的组件里调用 beforeRouteEnter
- 调用全局的 beforeResolve 守卫 (2.5+)
- 导航被确认
- 调用全局的 afterEach 钩子
- 触发 DOM 更新
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
3.9 keep-alive
keep-alive
是Vue
内置的一个组件- 可以
使被包含的组件保留状态
,或避免重新渲染
两个重要的属性
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
最简单的方式了 - 我们来对使用步骤,做一个简单的小节:
- 提取出一个公共的
store对象
,用于保存
在多个组件中共享的状态
- 将
store对象
放置在new Vue对象
中,这样可以保证在所有的组件中都可以使用到 - 在其他组件中使用
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状态更新
Vuex
的store状态
的更新唯一方式:提交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响应规则
Vuex
的store
中的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
中, 并且在成功或者失败后, 调用对应的resolve
或reject
- 在
- 来看下面的代码:
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)
, 而每个模块拥有自己的state
、mutations
、actions
、getters
等
- 我们按照什么样的方式来组织模块呢?
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
中添加state
、mutations
、getters
mutation
和getters
接收的第一个参数是局部状态
对象
- 我们在
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>
- 注意:
- 虽然, 我们的
doubleCount
和increment
都是定义在对象内部
的 - 但是在调用的时候, 依然是通过
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
作者就在GitHub
的Issues
中说明了去掉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框架学习的总结笔记,复习的时候参考