从零开始学习Vue(五)

374 阅读7分钟

Promise

Promise是异步编程的一种解决方案,能有效避免回调地狱。

Promise的基本使用

new Proise((resolve, reject) => {
	setTimeout(() => {
    	resolve('Hello World');
        reject('Error Data');
    }, 1000)
}).then(data => {
	console.log(data);	// Hello World
}).catch(error => {
	console.log(error);	// Error Data
})

Promise的三种状态:

  • pending:等待状态,比如正在进行网络请求或者定时器没有到时间
  • fulfill:满足状态,当我们主动回调了resolve时,就处于该状态,并且会回调.then()
  • reject:拒绝状态,当我们主动回调了reject时,就处于改状态,并且会回调.catch()

Promise的链式调用

Promise回调then或者catch方法时都会返回一个Promise对象,所以我们可以对其进行链式调用,通过Promise对新数据进行包装并返回。

new Promise((resolve, reject) => {
	setTimeout(() => {
    	resolve('Hello World');
    }, 1000)
}).then(data => {
	console.log(data);	// Hello World
    return Promise.resolve(data + '1');
}).then(data => {
	console.log(data);	// Hello World1
    return Promise.reject(data + 'error');
}).then(data => {
	console.log(data);	// 这里没有输出,这部分代码不会执行
    return Promise.resolve(data + '2');
}).catch(error => {
	console.log(data);	// Hello World1error
    return Promise.resolve(data + '3');
}).then(data => {
	console.log(data);	// Hello World1error3
})

// 可直接通过return data代替Promise.resolve进行简写
new Promise((resolve, reject) => {
	setTimeout(() => {
    	resolve('Hello World');
    }, 1000)
}).then(data => {
	console.log(data);	// Hello World
    return data + '1';
}).then(data => {
	console.log(data);	// Hello World1
    return Promise.reject(data + 'error');
}).then(data => {
	console.log(data);	// 这里没有输出,这部分代码不会执行
    return data + '2';
}).catch(error => {
	console.log(data);	// Hello World1error
    return data + '3';
}).then(data => {
	console.log(data);	// Hello World1error3
})

Promise.all

Promise.all([
	new Promise((resolve,reject) => {
    	setTimeout(() => {
        	resolve('result1')
        }, 1000)
    }),
    new Promise((resolve,reject) => {
    	setTimeout(() => {
        	resolve('result2')
        }, 2000)
    })
]).then(results => {
	console.log(results);	// ['result1', 'result2']
})

Vuex

Vuex是一个专为Vue.js应用程序开发的状态管理模式

  • 它采用集中式存储管理应用的所有组件的状态,并以对应的规则保证状态以一种可预测的方式发生变化。
  • Vuex也集成到Vue的官方调试工具devtools extension,提供了诸如零配置的time-travel调试、状态快照导入导出等高级调试功能。

单界面的状态管理

单个组件中进行状态管理是一件非常简单的事件:

  • state:状态
  • View:视图层,针对status的变化显示不同的信息
  • Actions:导致状态改变的操作:点击、输入等
<template>
	<div class="test">
    	<div>当前计数:{{counter}}</div>
        <button @click="counter++">+1</button>
        <button @click="counter--">-1</button>
    </div>
</template>

<script>
	export default {
    	name: 'HelloWorld',
        data() {
        	return {
            	counter: 0
            }
        }
    }
</script>
// 解析:
- status: counter
- View: 对应的页面显示
- Actions:按钮的点击事件

多界面的状态管理

当某些状态属于的多个界面共同维护时,我们就需要用到Vuex。Vuex的基本思想:将共享的状态抽取出来,每个视图按照规定进行访问和修改操作。

Vuex状态管理图例

简单的案例

  1. 创建一个文件夹store,并在其中新建一个index.js文件
import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

const store = new Vue.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count ++;
        },
        decrement(state) {
            state.count --;
        }
    }
})

export default store
  1. 挂载到Vue实例中
  import Vue form 'vue'
  import App form './App'
  import stor form './store'
  
  new Vue({
  	el: '#app',
      store,
      render: h => h(app)
  })
  1. 使用Vuex的count
  <template>
    <div class="test">
        <div>当前计数:{{counter}}</div>
        <button @click="increment">+1</button>
        <button @click="decrement">-1</button>
    </div>
	</template>

  <script>
      export default {
          name: 'HelloWorld',
          computed: {
          	counter() {
              	return this.$store.state.count
              }
          },
          methods: {
          	increment() {
              	this.$store.commit('increment')
              },
              decrement() {
              	this.$store.commit('decrement')
              }
          }
      }
  </script>
  • 小结:
      1. 提取一个公共的store对象,用于保存在多个组件中共享的状态。
      1. 将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
      1. 在其他组件中使用store对象中保存的状态即可;
        • 通过this.$store.state.属性的方法来访问状态
        • 通过this.$store.commit('mutation中方法名')来修改状态
  • 注意事项:
    • 通过提交mutation的方式而非直接改变store.state.count,这是因为Vuex可以更明确追踪状态的变化。

Vuex的核心概念

  • State
  • Getters
  • Mutation
  • Action
  • Module

State 单一状态树

用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

Getters

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

基本使用

const store = new Vue.Store({
	state: {
    	students: [
        	{id: 110, name: 'apple', age: 18},
            {id: 111, name: 'banana', age: 21},
            {id: 112, name: 'orange', age: 25}
        ]
    },
    getters: {
    	// 获取年龄大于20的学生
    	greaterAgesStus: state => {
        	return state.students.filter(s => s.age >= 20).length
        },
        // 获得年龄大于20的学生
        greaterAgesCount: (state, getters)=> {
        	return getters.greaterAgesStus.length
        },
        // getters默认是不能传递参数的,如果希望传递参数,则只能让getters本身返回一个函数。
        // 根据id获取学生信息
        stuById: state => {
        	return id => {
            	return state.students.find(s => s.id === id);
            }
        }
    }
})

Mutation

Vuex的store状态更新的唯一方式:提交Mutations Mutations主要包括两个部分:

  • 字符串的事件类型(type)
  • 回调函数(handler),该回调函数的第一个参数就是state

基本用法

// 定义
mutations: {
	increment(state) {
    	state.count ++
    }
}

// 调用
increment() {
	this.$state.commit('increment');
}

参数传递

在通过mutation更新数据时,可以携带额外的参数,这些参数被称为mutation的载荷(Payload)。

/* 单个参数传递 */
// 定义
mutations: {
	decrement(state, n) {
    	state.count -= n;
    }
}

// 调用
decrement() {
	this.$store.commit('decrement', 2);
}

/* 多个参数传递通过对象 */
// 定义
mutations: {
	decrement(state, payload) {
    	state.count -= payload.count;
    }
}

// 调用
decrement() {
	this.$store.commit('decrement', {count: 2});
}

提交风格

Vue还提供了另外一种风格,它是一个包含type属性的对象

this.$store.commit({
	type: 'decrement',
    count: 100
})

// Mutations的处理方式是将整个commit的对象作为payload使用,所以代码没有改变
mutations: {
	decrement(state, payload) {
    	state.count -= payload.count;
    }
}

响应规则

Vuex的store中的state是响应式的,当state中的数据发生改变时,Vue组件会自动更新。这就要求我们遵守Vuex对应的规则:

  • 提前在store中初始化好所需的属性
  • 当给state中的对象添加新属性时,使用下面的方式:
    • 方式一:使用Vue.set(obj, 'newProp', 123)
    • 方式二:用新对象给旧对象重新赋值
mutations: {
	updateInfo(state, payload) {
    	// 界面不会自动更新
    	state.info['height'] = payload.height;
        
        // 方式一:
        Vue.set(state.info, 'height', payload.height);
        
        // 方式二
        state.info = {...state.info, 'height': payload.height};
    }
}

常量类型

问题:
在mutations中, 我们定义了很多事件类型(也就是其中的方法名称).
当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutations中的方法越来越多.
方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.

解决方法:
使用常量替代Mutations事件的类型. 我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.

// 创建一个文件:mutation-types.js
export const UPDATE_INFO = 'UPDATE_INFO'

// 在store中引入
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: 'apple',
            age: 18
        }
    },
    mutations: {
    	[types.UPDATE_INFO](state, payload) {
        	state.info = {...state.info, 'height': payload.height};
        }
    }

})

export default store

// 在组件中调用
import {UPDATE_INFO} from './mutation-types';
methods: {
	updareInfo() {
    	this.$store.commit(UPDATE_INFO, {height: 1.88})
    }
}

同步函数

通常情况下,Vuex要求我们Mutations中的方法必须时同步方法

主要的原因时当我们使用devtools时,devtools可以帮助我们捕捉mutations的快照,如果时异步操作,那么devtools将不能很好的跟踪这个操作什么时候会被完成。

Action

我们强调在Mutations中不允许进行异步操作,在某些情况下必须进行异步操作则可用Action来代替Mutations进行异步操作。

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

基本使用

const store = new Vuex.Store({
	state: {
    	count: 0
    },
    mutations: {
    	increment(state) {
        	state.count ++
        }
    },
    actions: {
      increment(context) {
          context.commit('increment')
      }
  	}
})

注意: Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

分发(dispatch)

在Vue组件中,如果我们调用action中的方法,那么就需要使用dispatch。

methods: {
	increment() {
    	this.$store.dispatch('increment');
    }
    // 支持参数传递payload
    increment() {
    	this.$store.dispatch('increment', {count: 2});
    }
}

// 定义 payload
mutations: {
	increment(state, payload) {
    	state.count -= payload.count
    }
},
actions: {
	increment(context, payload) {
    	setTimeout( () => {
        	context.commit('increment', payload)
        }, 5000)
    }
}

异步Promise返回

在Action中我们可以将异步操作放在一个Promise中,并且在成功或者失败后,调用对应的resolve或reject

// 定义
actions: {
	increment(context) {
    	return new Promise(resolve => {
        	setTimeout(() => {
            	context.commit('increment');
                resolve()
            }, 5000)
        })
    }
}

// 调用
methods: {
	increment() {
    	this.$store.dispatch('increment').then(res => {
        	console.log('完成了更新操作');
        });
    }
}

Module

问题:
Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理. 当应用变得非常复杂时,store对象就有可能变得相当臃肿.

解决方法:
Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutation、action、getters等

基本使用

const moduleA = {
	state: {
    	count: 0
    },
    mutations: {
    	increments(state) {
        	state.count ++
        }
    },
    actions: {
    	// context -> {state, commit, rootState};
    	incrementIfOnRootSum({state, commit, rootState}) {
        	if((state.count + rootState.count) % 2 === 1) {
            	commit('increment');
            }
        }
    },
    getters: {
    	doubleCount(state) {
        	return state.count * 2
        },
        sumWithRootCount(state, getters, rootState) {
        	return state.count + rootState.count;
        }
    }
}

const moduleB = {
	state: {},
    mutations: {},
    actions: {},
    getters: {}
}

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

store.modules.a	// -> moduleA的状态state
store.modules.a.count	// 0
store.modules.b	// -> moduleB的状态state

// 在组件内使用
<script>
export default {
	name: 'App',
    computed: {
    	count() {
        	return this.$store.getters.doubleCount
        }
    },
    methods: {
    	increment() {
        	this.$store.commit('increment')
        }	
    }
}
</script>

// 注意: 虽然doubleCount和increment都是定义在对象内部的,但是在调用的时候,依然是通过this.$store来直接调用的。

目录结构

当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰.

网络模块封装

网络模块的选择

  • 传统的Ajax是基于XMLHttpRequest(XHR)
    • 配置和调用方式等非常混乱
  • jQuery-Ajax
    • 在Vue的整个开发中都是不需要使用jQuery
    • 为了一个网络请求而引入jQuery是不合理的,体积太大
  • Vue-resource
    • vue-resource不再支持新版本,对以后的项目开发和维护都存在很大的隐患
  • 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[, config])
  • axios.put(url[, config])
  • axios.patch(url[, config])

axios.get

import axios from 'axios'

export default {
	name: 'App',
    created() {
    	axios.get('http://123.207.32.32:8000/category')
        .then(res => {
        	console.log(res)
        }).catch(err => {
        	console.log(err)
        });
        
        axios.get('http://123.207.32.32:8000/home/data', 
        {params: {type: 'sell', page: 1}})
        .then(res => {
        	console.log(res)
        }).catch(err => {
        	console.log(err)
        })
    }
}   

发送并发请求

import axios from 'axios'

export default {
	name: 'app',
    created() {
    	// 发送并发请求
        axios.all([axios.get('http://123.207.32.32:8000/category'),
         axios.get('http://123.207.32.32:8000/home/data', 
        {params: {type: 'sell', page: 1}})])
        .then(axios.spread((res1, res2) => {
        	console.log(res1);
            console.log(res2);
        }))
    }

}

全局配置

import axios from 'axios'

export default {
	name: 'app',
    created() {
      // 提取全局的配置
      axios.defaults.baseURL = 'http://123.207.32.32:8000';
      axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

      // 发送并发请求
      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'
  • 请求根路径
  • 请求前的数据处理
    • transformRequest: [function(data){}]
  • 请求后的数据处理
    • transformResponse: [function(data){}]
  • 自定义的请求头
    • headers: {'x-Requested-With': 'XMLHttpRequest'}
  • URL查询对象
    • params: {id: 2}
  • 查询对象序列化函数
    • paramsSerializer: function(params){}
  • post request body
    • data: {key: 'aa'}
  • 超时设置
    • timeout: 1000
  • 跨域是否自带Token
    • withCredentials: false
  • 自定义请求处理
    • adapter: function(resolve, reject, config)
  • 身份验证信息
    • auth: {uname: '', pwd: '12'}
  • 响应的数据格式
    • responseType: 'json'
    • json/blob/document/arraybuffer/text/stream

axios实例

我们从axios模块中导入对象时,使用的是默认的实例,当设置一些默认配置时,这些配置就被固定下来了,当某些请求需要一些区别于默认配置的特殊配置时,我们可以创建新的实例,并且配置专属的配置信息。

const axiosInstance = axios.create({
	baseURL: 'http://123.207.32.32:8000',
    timeout: 5000,
    headers: {
    	'Content-Type': 'application/x-www-form-urlencoded'
    }
})

axiosInstance({
	url: '/category',
    method: 'get'
}).then(res => {

}).catch(err => {

})

axios封装

import originAxios from 'axios'

export default function axios(option) {
	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)
        })
    })
}

拦截器

axios提供了拦截器,用于我们在每次请求或者得到响应后进行对应的处理。

instance.interceptors.request.use(config => {
	console.log('来到了request拦截success中')
    //1. 发送网络请求时,在页面添加一个loading组件
    //2. 某些请求要求用户必须登陆,判断用户是否有token,如果没有token跳转到login页面
    //3. 对请求的参数进行序列化
    config.data = qs.stringify(config.data)
    return config
},err => {
	console.log('来到了request拦截failure中')
    // 错误拦截比较少,例如请求超时,跳转到错误页面
    return err
})

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

axios({
	url: '/home/data',
    method: 'get',
    params: {
    	type: 'sell',
        page: 1
    }
}).then(res => {

}).catch(err => {

})

// 输出结果
来到了request拦截success中
来到了response拦截success中