promiseAndAxios

文章目录

Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

一个 Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

使用

举两个不同写法的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 回调地狱,主要是到最后无法分清闭合的 ) 或者 } 是谁的
setTimeout(() => {
console.log('模拟异步请求1')
setTimeout(() => {
console.log('模拟异步请求2')
setTimeout(() => {
console.log('模拟异步请求3')
setTimeout(() => {
console.log('模拟异步请求4')
}, 1000)
}, 1000)
}, 1000)
}, 1000)

// Promise 链式操作,拆分每层,只要是异步操作,就丢进去,并在最后 resolve,在 then() 里传入一个 resolve 被成功执行后的回调函数,可以接收到 resolve 传递来的值。第一个参数是 resolve 函数,调用后将使该 Promise 的状态变为 fulfilled。 第二个参数是 reject(可选),调用后使该 Promise 的状态变为 rejected,表示失败回调,可以被 catch 里的回调函数捕获,同样的,传递的值也会被接受。由于 catch 函数可以捕获到之前所有的 rejected,所以不管前面返回多少层的 Promise,都可以在最后写一次来捕获之前的所有 Promise 的 rejected 状态。
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('模拟异步请求1')
resolve('msg1')
}, 1000)
}).then(res => {
console.log(res)
return new Promise(resolve => {
setTimeout(() => {
console.log('模拟异步请求2')
resolve('msg2')
}, 1000)
})
}).then(res => {
console.log(res)
return new Promise(resolve => {
setTimeout(() => {
console.log('模拟异步请求3')
resolve('msg3')
})
})
}).then(res => {
console.log(res)
setTimeout(() => {
console.log('模拟异步请求4')
resolve('msg4')
})
}).catch(err => {
console.log(err)
})

简写

当我们只想用 Promise 来分布操作的代码,但这些代码不是异步操作,每次都要返回一个新的 Promise,并且还需要 resolve 出去,非常繁琐,这时你可以简写它。

简写一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
原本写法 resolve
return new Promise((resolve, reject) => {
resolve('msg')
})
原本写法 reject
return new Promise((resolve, reject) => {
reject('err')
})
*/
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('模拟异步请求1')
resolve('msg')
})
}).then(resolve => {
console.log('模拟自己的代码')
return Promise.resolve('msg') // resolve 的简写,省去了 new
}).then((resolve, reject) => {
console.log('模拟自己的代码')
return Promise.reject('err') // reject 的简写,省去了 new
}).catch(err => {
console.log(err)
})

简写二

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('模拟异步请求1')
resolve('msg')
})
}).then(resolve => {
console.log('模拟自己的代码')
return 'msg' // 简写 resolve 方法 2,直接 return,会自动帮我们包装成 Promise 对象
}).then((resolve, reject) => {
throw 'err' // 简写 reject 方法 2,直接 throw,会自动帮我们包装成 Promise 对象,不过如果这个错误是异步的,比如写在定时器中,则无法被 catch 捕获,只能使用 rejected 了。当然我们这里只是模拟报错,
}).catch(err => {
console.log(err)
})

Promise.all()

如果需要同时多个 Promise 完成了之后才能执行操作,可以使用 Promise.all().then() 方法

这是老写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 网络请求1
new Promise((resolve, reject) => {
$.ajax({
url: 'url1',
success: function(data) => {
resolve(data)
}
})
})

// 网络请求2
new Promise((resolve, reject) => {
$.ajax({
url: 'url2',
success: function(data) => {
resolve(data)
}
})
})

// 由于我们需要两个请求的数据都拿到了之后才能执行操作,但我们无法确定哪个请求先被完成,如果按正常的思路,就是我们需要一个 flag

// 方法一
let isResult1 = false
let isResult2 = false

function handleResult() {
if (isResult1 && isResult2) { // 只有当两个值都为 true 时才执行
console.log('执行操作')
}
}

// 方法二
function handleResult() {
let resNum = 0
return function () { // 闭包保存
resNum++
if (resNum === 2) {
console.log('执行操作')
}
}()
}

$.ajax({
url: 'url1',
success: function(data) => {
console.log('结果一')
isResult1 = true // 这是方法一的内容
handleResult()
}
})

$.ajax({
url: 'url2',
success: function(data) => {
console.log('结果二')
isResult2 = true // 这是方法二的内容
handleResult()
}
})

Promise.all 写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Promise.all([
new Promise((resolve, reject) => {
$.ajax({
url: 'url1',
success: function(data) => {
resolve(data)
}
})
}),
new Promise((resolve, reject) => {
$.ajax({
url: 'url2',
success: function(data) => {
resolve(data)
}
})
})
]).then(results => {
console.log(results[0]) // 保存第一个 Promise 的返回值
console.log(results[1]) // 保存第二个 Promise 的返回值
})

axios.all

axios 中,也有类似的方法 axios.all,来处理并发请求,也就是当多个请求都返回结果了之后才执行回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
axios.all([
axios({
url: 'url1',
// method: 'get' // 不屑 method 默认请求方式为 get
params: { // 传入服务器所需要的参数
type: 'type1',
page: 1
}
}),
axios({
url: 'url2'
})
]).then(res => { // 传入的 res 是一个数组,里面包含了每个请求的结果,当所有请求都完成了才会执行 then
console.log(res[0]) // 拿到第一个请求的数据
console.log(res[1]) // 拿到第二个请求的数据
})


// 上面的写法由于我们想要获取 res 的每项需要通过下标,有点麻烦,axios 提供了 axios.spread() 方法来分离返回的数组内容,也就是帮忙展开了数组。

axios.all([
axios({
url: 'url1',
// method: 'get' // 不屑 method 默认请求方式为 get
params: { // 传入服务器所需要的参数
type: 'type1',
page: 1
}
}),
axios({
url: 'url2'
})
]).then(axios.spread((res1, res2) => { // 分离出了 res1,res2,不过这个方法挺少用的。
console.log(res1) // 第一个请求结果
console.log(res2) // 第二个请求结果
}))

async、await

async 是 ES6 新增的关键字, await 是 ES7 新增。 async 添加在函数的开头,会将 async 函数的内容包装成一个 Promise 对象,这个函数的 return 将作为这个 Promise 的 resolve 。await 相当于 Promise.then() 它会把 await 之后的内容包裹进这个 then 中。 如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function func1() {
console.log(2)
}

async function func() {
await func1()
console.log(1)
}

// 相当于
async function func() {
func1().then(() => {
console.log(1)
})
}

// async 会把函数包装成 Promise
async function fun() {
return 1
}

fun().then(res => console.log(res)) // 1

Promise 执行顺序

推荐看完下面关于 Javascript 事件再来看这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/* 下面的流程讲解写的很早,可能当时并不清楚,有些错误,但不打算修改,留着看看当时奇怪的想法,hhh */

// 定时器会在页面所有同步操作执行完毕之后才执行
// JS 代码,先执行同步操作再执行异步操作
// JS 执行顺序: 同步操作 =》 Promise.then() 的操作(微任务) =》 异步操作

// 模拟一道普通的面试题
async function f1() {
console.log(2)
await f2()
console.log(4)
}

async function f2() {
console.log(3)
}

new Promise((resolve) => {
setTimeout(() => {
console.log(1)
}, 0)
resolve(7)
}).then(res => console.log(res))

f1()
console.log(5)
// 最终打印 2 3 5 4 1

// 调用 f1() ,f1 中先打印了 2,之后调用 f2 是通过 await,这是个异步操作,将会先执行这行代码,并且阻塞后续代码,会暂时跳出函数,重新寻找页面中的同步操作,先执行它们之后在回来执行这个函数中之后的内容,因此会打印 f2 中的 3 之后暂时跳出函数。跳出后发现外面有一个同步操作,打印 5.之后发现没有同步操作了,返回函数内,打印 4,最后打印异步操作的 Promise


// 改写
async function f1() {
console.log(2)
await f2()
await f3()
console.log(4)
}

async function f2() {
console.log(3)
}

async function f3() {
console.log(6)
}

f1()

new Promise((resolve) => {
setTimeout(() => {
console.log(1)
}, 0)
resolve(7)
}).then(res => console.log(res))

console.log(5)
// 最终打印 2 3 5 6 7 4 1
// 执行顺序 f1() =》 console.log(2) => f2() =》 console.log(3) 跳出函数 =》 执行 new Promise() 的 定时器 和 resolve =》 同步执行完毕,返回函数内执行 f3() => console.log(6) =》 执行 Promise 的 then() =》 console.log(res) 即打印 7 =》 最后执行定时器, console.log(1)
// 关键在于 6 和 7 和 4 的顺序,实际上,f3() 的执行相当于 f2().then(() => f3()) ,因此会先执行 f2 这个 Promise 的 then, 也就是打印 6,再执行之前执行的 Promise 的 resolve(7) 的 then,也就是打印 7 ,之后就到了 f3() 的 then(() => console.log(4)) ,就打印 4。
// 6、7、4 的顺序实际上就是 3 个 Promise 的执行顺序, 先 f2 => 外层的 new Promise => f3 ,async 就是 Promise 的语法糖,async 函数内的内容都将被包装成 Promise,它的返回值将作 为Promise 的 resolve。


// f1 相当于
async function f1() {
console.log(2)
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
new Promise(resolve => {
console.log(6)
resolve()
}).then(() => {
console.log(4)
})
})
}

/*
回来重新写一下正确的第二个内容的流程吧
1. 整个 script 作为第一个宏任务,开始执行
2. 遇到 f1(),立即执行 console.log,输出 2
3. 遇到 await f2,立即执行 console.log,输出 3,把 f3 及其之后的代码发布到微任务队列
4. 遇到 new Promise,执行里面的内容
5. 遇到 setTimeout,将其发布到微任务队列
6. 把 then 发布到微任务队列
7. 遇到 console.log(5),输出 5
8. 开始执行微任务
9. 遇到 f3() ,输出 6,把 console.log(4),发布到微任务队列中的最后一个(即最后一个执行,先进先出)
10. 遇到 then,输出 7
11. 执行 console.log(4),输出 4
12. 第一轮事件循环结束,输出 2,3,5,6,7,4
13. 开始下一个宏任务 setTimeout
14. 遇到 console.log(1),输出 1
15. 最后结果是 2,3,5,6,7,4,1
*/

Javascript 事件

javascript 是一门 单线程 语言,一切 javascript 多线程都是单线程模拟来的!。广义下,我们把任务分为两种,同步和异步任务,任务进入主线程才会被执行。

事件的定义

但实际上,我们对任务有更精细的定义:

  • macro-task(宏任务):都是整体代码块, script 标签内的代码,setTimeout 的回调函数内的代码,setInterval 回调函数内的代码(它们都算作一个宏任务,而每执行一个宏任务之后就会暂停宏任务,先去执行微任务)。
  • micro-task(微任务):Promisethenprocess.nextTick,注意只有一个微任务队列,只有当微任务队列被清空了之后,才会结束当前这轮的事件循环,接着开始下一个宏任务。

不同类型的任务会进入对应的 Event Queue,比如 setTimeoutsetInterval 会进入相同的 Event Queue.

事件顺序

事件循环的顺序,决定 js 的代码执行顺序。进入整体代码(宏任务)后,开始第一次循环,宏任务此时进入主线程。接着执行所有的微任务(即微任务进入主线程)。然后再次从宏任务,接着其中一个任务队列执行完毕,再执行所有微任务,如此反复。

举例一

简单举例,参考自掘金

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() {
console.log('setTimeout');
})

new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})

console.log('console');

流程

  1. 这段代码开始作为宏任务,进入主线程。
  2. 先遇到 setTimeout,那么将其回调函数注册后分发到宏任务 Event Queue。
  3. 接下来遇到了 Promisenew Promise 立即执行,then 函数分发到微任务 Event Queue。
  4. 遇到 console.log(),立即执行。
  5. 好了,整体代码 script 作为第一个宏任务执行结束,现在看看当前的微任务,我们发现 then 在微任务队列中,执行它
  6. 至此,第一轮事件循环结束了,我们开始第二轮循环。当然,从宏任务队列开始,我们发现了宏任务中 setTimeout 对应的回调函数,立即执行它
  7. 结束。

举例二

另一个较为复杂的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})

流程

  1. 第一轮事件循环
    1. 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出1
    2. 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中,我们将其记为 setTimeout1
    3. 遇到 process.nextTick(),其回调函数被分发到微任务 Event Queue 中。我们记为 process1
    4. 遇到 Promisenew Promise 直接执行,输出 7。then 被分发到微任务 Event Queue 中,我们记为 then1
    5. 又遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中,我们记为 setTimeout2
    6. 至此第一次宏任务执行完毕,此时已经输出 1 和 7。同时当前宏任务 Event Queue 存在两个任务,微任务 Event Queue 也两个任务。由于第一个宏任务已经结束,此时需要执行微任务了。
    7. 执行 process1,输出 6
    8. 执行 then1,输出8
    9. 微任务 Event Queue 全部执行完毕,现在是空的。至此,第一轮事件循环正式结束,输出结果是 1,7,6,8。接着查看宏任务 Event Queue,发现下一个任务是 setTimeout1
  2. 第二轮事件循环
    1. 执行 setTimeout1,遇到 console.log,输出 2
    2. 接下来遇到 process.nextTick(),将其分发到微任务 Event Queue,记为 process2
    3. 遇到 new Promise,立即执行 console.log,输出 4。并把 then 分发到微任务 Event Queue 中,记为 then2
    4. 第二轮事件循环的宏任务结束,新增输出 2 和 4,此时发现有 process2then2 两个微任务可以执行。
    5. process2 输出 3
    6. then2 输出 5
    7. 此时微任务 Event Queue 已清空,至此第二轮事件循环结束,第二轮输出 2, 4, 3, 5。接着查看宏任务 Event Queue,发现下一个任务是 setTimeout2
  3. 第三轮事件循环
    1. 执行 setTimeout2,遇到 console.log,输出 9。
    2. 遇到 process.nextTick(),将其分发到微任务 Event Queue 中。记为 process3
    3. 遇到 new Promise,立即执行 console.log,输出 11。并将 then 分发到微任务 Event Queue 中,记为 then3
    4. 第三轮事件循环的宏任务结束,新增输出 9 和 11。此时发现有 process3then3 ;两个微任务需要执行
    5. process3 输出 10
    6. then3 输出 12
    7. 此时微任务 Event Queue 已清空,至此第三轮事件循环结束,新增输出 9, 11, 10, 12。

总结:总共三轮事件循环,分别是 script 以及它对应的微任务 , setTimeout1 以及它对应的微任务, setTimeout2 以及它对应的微任务。 最后输出结果是 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12。(由于 node 环境下的事件监听依赖 libuv 与前端环境不完全相同,输出结果可能会有误差)。

libuv 库流程大体分为6个阶段:timersI/O callbacksidle、preparepollcheckclose callbacks,和浏览器的 microtaskmacrotask 那一套有区别。

写在最后

1. javascript 的异步

再说一次 javascript 是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

2. 事件循环 Event Loop

事件循环是 javascript 实现异步的一种方法,也是 js 的执行机制。

3. javascript 的执行和运行

执行和运行有很大区别,javascript 在不同的环境下,比如 node,浏览器,Ringo 等等,执行方式是不同的。而运行大多指 javascript 解析引擎,是统一的。

4. setImmediate

微任务和宏任务还有很多分类,比如 setImmediate 等,执行都是由共同点的,可以自行了解。

5. 再次说明
  • javascript 是一门单线程语言
  • Event Loop 是 javascript 的执行机制

牢记着两个基本点。

axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js (这是其他请求库所不具备的)中。它支持多种请求方式。 下面 [] 内的内容是可选部分

  • 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]])

现在我们随便写两个请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
axios({
baseURL: 'http://123.207.32.32:8000',
timeout: 5,
url: '/home/multidata'
})

axios({
baseURL: 'http://123.207.32.32:8000',
timeout: 5,
url: '/home/data',
params: {
type: 'sell',
page: 5
}
})

马上就发现了,我们这两个 axios 都有相同的配置,我们应当把它抽离出来。axios 提供了全局配置 axios.defaults

1
2
3
4
5
6
7
8
9
10
11
12
13
axios.defaults.baseURL = 'http://123.207.32.32:8000'  // 请求根路径
axios.defaults.timeout = 5
axios({
url: '/home/multidata'
})

axios({
url: '/home/data',
params: {
type: 'sell',
page: 5
}
})

如果我们需要在 header 里加入自定义请求头,可能需要加上 token 之类的。

1
2
3
axios.defaults.headers:{'x-Requested-With':'XMLHttpRequest'}
// 例
axios.default.headers.Authorization = window.sessionStorage.getItem('token')

直接列出常见配置项吧。。一个个写太麻烦了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 请求地址
url: '/user'
// 请求类型
methods: 'get'
// 请求根路径
baseURL: 'http://www.xx.com/api'
// 请求前的数据处理
transformRequest: [function(data){}]
// 请求后的数据处理
transformResponse: [function(data){}]
// 自定义请求头
headers: {'x-Requested-With':'XMLHttpRequest'}
// URL 查询对象 params 和 get 请求对应,post 里写这个无效,因为这个最终会被拼接到 url 中,而 post 不需要, post 需要把请求参数放到请求体里
params: {page: 1}
// 查看对象序列化函数
paramsSerializer: function(params) {}
// request body 这个和 post 请求对应
data: {key: 'aa'}
// 超时设置 ms
timeout: 1000
// 跨域是否带 Token
withCredentials: false
// 自定义请求处理
adapter: function(resolve, reject, config) {}
// 身份验证信息
auth: { uname:'', pwd: '12'}
// 响应的数据格式 json/blob/document/arraybuffer/text/stream
responseType: 'json'

axios 的封装

由于我们可能在多个组件内 import axios from 'axios',之后再使用它。这就导致了我们的项目对 axios 这个插件的依赖过高,一旦 axios 停止维护,或者出现重大 bug,被要求使用别的请求插件。那么,整个项目用到 axios 的地方都需要修改了。为了防止这种情况,我们需要对 axios 单独封装一层,再引入这个封装完毕的文件而不是 axios,之后如果 axios 真的出问题了,我们也只需要修改自己封装的这个 request 文件,使用新的插件并改写 export 的内容就行,不用一个个修改用到 axios 的组件了。

思想

这种思想是解耦的思想,不管我们用什么插件,如果在多个组件中引入了,我们都可以对它进行封装,多封一层,防止对这个插件的依赖过高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// @/network/request.js
// 由于 axios.defaults 设置的是全局样式,假如我们设置了 baseURL, 但有好几个接口是从另一个服务器地址获取的,这时候就会造成麻烦。所以我们这里可以创建多个 axios 实例,分别传递不同的配置项,而不是使用全局配置。所以这里 export 的不是 default,而是指定了名字的 request 函数。
import axios from 'axios'

export funcion request(config) {
// 1. 创建 axios 实例, 它提供了 axios.creata 方法来创建实例
const instance = axios.create({
baseURL: 'yourUrl',
timeout: 5000,
headers: ''
})

return instance(config)
// 由于 axios 本身返回的就是一个 Promise,前面就不需要 new Promise(/* ... */) 了,如果使用其他框架,它返回的不是 Promise,那么这里的写法就需要修改为下面这样,整个操作都包在 Promise 里,并返回这个 Promise
// return new Promise((resolve, reject) => 发送请求操作
// if(请求成功) { resolve(data) }
// else(即失败情况) { rejected(err) })
}

export function request2(congifg) {
/*
这里新建第二个 axios 实例,然后返回,这样两个 axios 的配置就不会互相干扰了。但这样也意味着你不能使用 export default。当然,如果只有一种配置项,仍然可以 export default
*/
}


/*
另一种写法,直接让使用者传入成功回函和失败回调的方法
export funcion request(config, success, failure) {
// 创建 axios 实例 ,它有个 axios.create() 方法
const instance = axios.create({
baseURL: 'yourUrl',
timeout: 5000
})

// 直接在这里写了回调方法的执行,就是在这边写了 then 和 catch,当然前面的 success 和 failure 也可以写道 config 里,直接在 config 上定义两个属性 success 和 failure,这边从上面拿就行 config.success 和 config.failure,看起来好一点,但没啥区别。传入的参数写法
const config = {
baseConfig: {
// axios 用到的配置项,也就是上面的 config,它现在必须用 config.baseConfig 拿到了
},
success: function (res) {},
failure: function (err) {}
}
当然,这里这样写的原因也是 axios 是个 Promise,否则外面还得再像上面一样包一层 Promise
instance(config)
.then(res => success(res))
.catch(err => failure(err))
}
*/

// 组件内使用
import { request } from '@/network/request.js' // 因为上面不是 export default,所以需要加上 {},然后取出要用的模块。

export default {
methods: {
http() {
request({
url: '/api',
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
}
}
}

// 其实不应该直接在组件内使用,应当再多封装一层,组件内应该引入下面这个文件,这样,如果其他组件内页用到了同样的请求,就不需要再写一次 url 了,并且,如果有一天后台接口崩了,需要修改 url 地址,这样做了之后也只需要修改一次,否则可能要跑到每个组件中去修改它的 url,当然这种情况少见,一般也就改改根路径,即在 request.js 定义的路径。不过,要体会这种思想就是了。
// home.js
import { request } from './request'

export function getHomeMultiData() {
return request({
url: '/home/multidata'
})
}

坏处

这种写法的坏处在于,这样简单的封装之后,只允许使用 axios(config) 这种格式的请求了,如果需要使用比如 axios.get(url[, config]) 这种格式的请求,就需要自己再在这个 http.js 文件中加点新的方法定义,然后 export 出一个对象,对象上有上面的 request 和新增的 get 方法。

所以,如果我们简单使用 axios,一般都是直接挂载到 Vue.prototype 上,如 Vue.prototype.$http = axios,之后调用就是 this.$http.get 之类的写法。

拦截器

总共两个拦截器,用于每次在发送请求或得到响应后,进行对应的处理。

  • axios.interceptors.request.use(config => config, err => err) request(请求) 拦截,第一个参数是发送请求成功的拦截,第二个参数是发送请求失败的拦截
  • axios.interceptors.response.use(response => response, err => err) response(响应) 拦截,第一个参数是响应成功的拦截,第二个是响应失败的拦截

参数

use 要求传入的东西,可以看到,它要求的是两个函数。题外:这些插件虽然不是用 ts 写的,但他们一般都会建立一个 .d.ts 文件,用来告诉我们它要求传入的参数类型和返回值类型。

1
2
3
4
export interface AxiosInterceptorManager<V> {
use(onFulfilled?: (value: V) => V) | Promise<V>, onRejected?: (error: any) => any: number;
eject(id: number): void
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// @/network/request.js
import axios from 'axios'

export funcion request(config) {
// 1. 创建 axios 实例, 它提供了 axios.creata 方法来创建实例
const instance = axios.create({
baseURL: 'yourUrl',
timeout: 5000,
headers: '' // 这里也可以修改头部信息
})

// 为什么不是 axios.interceptors ? 因为直接使用 axios,就是全局拦截器,我们只希望局部的,所以在实例上修改即可,尽量不要去动全局的东西
// 请求分为好几步,第一步,本地先向服务器请求,是否能返回数据,如果服务器能返回数据,就会告诉客户端我可以返回,第二步,然后客户端再传过去 config,服务器再处理这个 config,之后再返回,同时返回一个结束标记,客户端收到数据和结束标记后关闭与服务器的通信
instance.interceptors.request.use(config => { // 只要请求成功发送,就会拦截,进入这个函数,很明显,是在第二步拦截, config 就是我们上面写的配置项

// 请求成功拦截可能用到的地方
// 1. config 中的一些信息不符合服务器要求,如 headers,我们在这里也可以修改
// 如 config.headers.Authorization = window.sessionStorage.getItem('token')
// 2. 每次发送请求时,都希望在界面显示一个请求图标之类的东西,可以在这里控制一下
// 如 我们可能会用到页面顶部的蓝色进度条插件 NProgress,每次发送的时候 NProgress.start() 让它显示,或者使用其他一些让界面显示转圈圈 logo 的插件。
// 3. 某些网络请求(比如登录),必须携带特殊信息(登录的 token),如果有的话就继续发送,没有的话就提示一下用户先去登录,这里也可以直接帮他们跳到 login 页面。当然路由的导航守卫也能完成,但有的框架里没有 router,比如 uniapp,当然它也没有 axios,但它有拦截器,所以就得在拦截器里写了。

return config // 必须返回 config,这也就是为什么它叫拦截器而不是监听器的原因,它把 config 给截胡了,如果不 return 一个配置项,就会发过去一个 undefined,到时候就会进去 request(config).then().catch(err => err) 的 catch 处理中,因为报错了。
}, err => {
// 请求失败拦截基本用不到,比如可能你网络炸了,发不出去,就来到这里
return err
})

instance.interceptors.response.use(res => {
// 这里是响应成功的拦截
// 这里得关掉上面的转圈动画或进度条动画
// NProgress.done()

return res.data // 因为 axios 会在返回的 data 外面包一层 data,这里获取了之后,别的地方就不需要写成 request(config).then(res => const { data: realRes } = res) 这种用解构拿取真正的 data 的写法了。
}, err => {
// 这里是响应失败的拦截
// 这里也得关掉上面的转圈动画或进度条动画
// NProgress.done()
return err
})

return instance(config)
}

完整版本的简单封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import axios from 'axios'

export funcion request(config) {
const instance = axios.create({
baseURL: 'yourUrl',
timeout: 5000,
headers: ''
})

instance.interceptors.request.use(config => {
return config
}, err => err)

instance.interceptors.response.use(res => {
return res.data
}, err => {
return err
})

return instance(config)
}


// dashboard.js
// 另一个 js 文件的使用
import request from './request'

const dashboardApi = {
getDashboardData(data) {
return request({
url: '/api/leju/admin/dashboard/baseInfo',
data
})
}
}

export default dashboardApi

拓展

目前拓展分为 token 需求和 restful 风格的 url 需求两种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 不同 token 的需求
// dashboard.js
import request from './request'

const dashboardApi = {
getDashboardData(data) {
return request({
url: '/api/leju/admin/dashboard/baseInfo',
data,
token: '' // 如果某几个页面需要 token 值,就可以在这里加上是否需要 token 的标识符,如果项目里只有一种 token, 可以写成 isToken,把值设置为布尔值,如果有多种不同的 token,可以像这样把 token 设置为不同 token 的字符串,可以导入 utils 方法,把获取 token 的方法写在那个文件中,并在这里写入,方便管理。
})
}
}

export default dashboardApi

// restful 风格的路由,就是传参由之前的 xxx.com/api/?start=1&limit=10 这样的 ? 形式改成类似 vue-router 里的 params 形式,地址栏里会好看一些,这是后端里比较新的路由方式了。传值方式如下,data 如果有需要,仍然正常传输,如果不需要,可以直接把参数传入 data,写成 ${data.start} 和 ${data.limit} 这种格式,第三行的 data 就可以删去了。
// dashboard.js
import request from './request'

const dashboardApi = {
getDashboardData(data, start, limit) {
return request({
url: `/api/leju/admin/dashboard/baseInfo/${start}/${limit}`,
data
})
}
}

export default dashboardApi

登录认证的两种方式

session

  1. 客户端访问服务器,服务器会在必要的时候,创建一个 session 对象(和前端的 sessionStorage 区分,这里的是服务器的),该 session 对象有一个唯一的 sessionId,该 session 对象存储在服务器的 session 集合中。
  2. 服务端一旦创建了 session 对象,会通过请求的 response 对象中的 set-cookies 方法,把当前的 sessionId 存入客户端浏览器的 cookies 中。
  3. 根据 http 协议,客户端之后的每次请求都会携带 cookie
  4. 服务器可以根据客户端的请求是否携带了 sessionId 来判断是否登录
  5. 服务端 session 有默认过期时间,但每次访问将会刷新过期时间。这个过期时间可以通过程序修改过期时间,过期后的 session 其客户端可以被识别。

特点:session 登录,大部分功能都是后台实现的,客户端几乎无感知。但是,如果客户端发送一个 ajax 的跨域请求,那么很多第三方库都是默认不携带 cookie 的。比如 jQuery、axios 等,但可以通过它们上面的方法,设置 withCredentials = true 来使其发送时携带 cookie

问题:

  1. 不适合分布式部署,当访问量很大时,可能有 Ngix 反向代理让多台服务器负载均衡,此时 session 只存在于你访问过的服务器上,但其他的服务器上不存在,如果上次代理的服务器和这次代理的服务器不一样,又需要重新登录。除非在服务器上统一 session 的状态,但这个在后台实现起来很麻烦。
  2. 移动端如 App、小程序,它们不支持 cookie

token

  1. 用户登录成功,服务器会根据客户信息,依据特定算法(可以看看 JWT),生成一个对应该客户端的唯一的标识,这个标识我们称之为 token,一般是一个字符串,并返回给客户端。
  2. 客户端需要自行实现 token 的保存,并且在之后的每次请求都携带token(一般在请求拦截器里添加特定的 token 键名与 token 值即可)
  3. 服务器会收到客户端的 token,并且根据生成算法验证当前 token 是否有效,因此如果在不知道生成算法的情况下篡改 token 并发送,将会被识别为无效 token,并返回 token 无效的信息。
  4. token 有过期时间,过期后页面的任何请求都将失效,并且后台应该返回一个 token 失效的信息,前端拿到信息后,需要重新发送请求获取新的 token,当然此时肯定不能让用户退出重新登陆,因此需要保存客户之前的登录信息,用什么方式转码存储就是另外的问题了,为了安全,必然需要与后台协调算法。
  5. 客户端退出时,token 并不会立即失效,直到 token 自身的过期时间到了。

特点:客户端需要手动实现 token 的保存和发送。token 无法防止丢失,但是能防止篡改,而且适合分布式部署,服务器不需要存储任务内容,仅需根据生成算法验证当前 token 是否有效即可。

这是算力换空间的一种体现。

分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理