场景1.一个请求接着一个请求
案例:后一个请求依赖前一个请求,下面以爬取一个网页内的图片为例,使用了superagent请求模块,cheerio页面分析模块,图片的地址需要分析网页内容得出,所以必须按顺序进行请求。const request = require(‘superagent‘)
const cheerio = require(‘cheerio‘)
// 简单封装下请求,其他的类似
function getHTML(url) {
// 一些操作,比如设置一下请求头信息
return superagent.get(url).set(‘referer‘, referer).set(‘user-agent‘, userAgent)
}
// 下面就请求一张图片async function imageCrawler(url) {
let res = await getHTML(url)
let html = res.text
let $ = cheerio.load(html)
let $img = $(selector)[0]
let href = $img.attribs.src
res = await getImage(href)
retrun res.body
}
async function handler(url) {
let img = await imageCrawler(url)
console.log(img)
// buffer 格式的数据
// 处理图片}
handler(url)
其中await getHTML
是必须的,如果省略了await程序就不能按期得到结果。执行流程会先执行await后面
的表达式,其实际返回的是一个处于pending状态的promise
,等到这个promise处于已决议状态后才会执行await后面的操作,其中的代码执行会跳出async函数,继续执行函数外面的其他代码,所以并不会阻塞后续代码的执行。
场景2.并发请求
有时候我们并不需要等待一个请求回来才发出另一个请求,这样效率很低,所以这时候需要并发执行请求任务。下面以一个查询为例,先获取一个人的学校地址和家庭住址,再由这些信息获取详细的个人信息,学校地址和家庭住址是没有依赖关系的,后面的获取个人信息依赖于两者
async function infoCrawler(url, name) {
let [schoolAdr, homeAdr] = await Promise.all([getSchoolAdr(name), getHomeAdr(name)])
let info = await getInfo(url + ?schoolAdr=${schoolAdr}&homeAdr=${homeAdr}
)
return info
面使用的 Promise.all 里面的异步请求都会并发执行,并等到数据都准备后返回相应的按数据顺序返回的数组,这里最后处理获取信息的时间,由并发请求中最慢的请求决定,例如 getSchoolAdr 迟迟不返回数据,那么后续操作只能等待,就算 getHomeAdr 已经提前返回了,当然以上场景必须是这么做,但是有的时候我们并不需要这么做。
上面第一个场景中,我们只获取到一张图片,但是可能一个网页中不止一张图片,如果我们要把这些图片存储起来,其实是没有必要等待图片都并发请求回来后再处理,哪张图片早回来就存储哪张就行了
let imageUrls = [‘href1‘, ‘href2‘, ‘href3‘]
async function saveImages(imageUrls) {
await Promise.all(imageUrls.map(async imageUrl => {
let img = await getImage(imageUrl)
return await saveImage(img)
}))
console.log(‘done‘)
}
// 如果我们连存储是否全部完成也不关心,也可以这么写
let imageUrls = [‘href1‘, ‘href2‘, ‘href3‘]
// saveImages() 连 async 都省了
function saveImages(imageUrls) {
imageUrls.forEach(async imageUrl => {
let img = await getImage(imageUrl)
saveImage(img)
})
}可能有人会疑问 forEach 不是不能用于异步吗,这个说法我也在刚接触这个语法的时候就听说过,
很明显 forEach 是可以处理异步的,只是是并发处理,map 也是并发处理,这个怎么用主要看你的
实际场景
场景3.错误处理
一个请求发出,可以会遇到各种问题,报错是常有的事,所以处理错误有时很有必要,async/await处理错误也非常直观,使用try/catch直接捕获
async function imageCrawler(url) {
try {
let img = await getImage(url)
return img
} catch (error) {
console.log(error)
}
}
// imageCrawler 返回的是一个 promise 可以这样处理
async function imageCrawler(url) {
let img = await getImage(url)
return img
}
imageCrawler(url).catch(err => {
console.log(err)
})
可能有人会疑问,是不是要在每个请求中都try/catch一下,这个其实在最外层catch一下就好了,一些中间件的设计就喜欢在最外层捕获错误
async function ctx(next) {
try {
await next()
} catch (error) {
console.log(error)
}
}
超时处理
一个请求发出,我们是无法确定什么时候能返回的,也总不能一直傻等,设置超时处理有时候是很有必要的
function timeOut(delay){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(new Error(‘已超时‘))
})
})
}
async function imageCrawler(url,delay) {
try {
let img = await Promise.race([getImage(url), timeOut(delay)])
return img
} catch (error) {
console.log(error)
}
}
并发限制
在并发请求的场景中,如果需要大量并发,必须要进行并发限制,不然会被网站屏蔽或者造成进程奔溃
async function getImages(urls, limit) {
let running = 0
let r
let p = new Promise((resolve, reject) => {
r = resolve
})
function run() {
if (running < limit && urls.length > 0) {
running++
let url = urls.shift();
(async () => {
let img = await getImage(url)
running--
console.log(img)
if (urls.length === 0 && running === 0) {
console.log(‘done‘)
return r(‘done‘)
} else {
run()
}
})()
run() // 立即到并发上限
}
}
run()
return await p
}