如何用不那么奇怪的承诺包装事件发射器?



我正在尝试用Promise包装http.get。这是我得到的:

import Http from 'http';
export function get(url) {
let req;
return new Promise((resolve, reject) => {
req = Http.get(url, async res => {
if(res.statusCode !== 200) {
return reject(new Error(`Request failed, got status ${res.statusCode}`));
}
let contentLengthStr = res.headers['content-length'];
if(contentLengthStr) {
let contentLength = parseInt(contentLengthStr, 10);
if(contentLength >= 0 && contentLength <= 2*1024**3) {
let buf = Buffer.allocUnsafe(contentLength);
let offset = 0;
res.on('data', chunk => {
if(chunk.length + offset > contentLength) {
return reject(new Error(`Received too much data, expected ${contentLength} bytes`));
}
chunk.copy(buf, offset);
offset += chunk.length;
});
res.on('end', () => {
if(offset === contentLength) {
resolve(buf);
} else {
return reject(new Error(`Expected ${contentLength} bytes, received ${offset}`));
}
})
} else {
return reject(new Error(`Bad Content-Length header: ${contentLengthStr}`));
}
} else {
return reject(new Error(`Missing Content-Length header not supported`));
}
});
}).catch(err => {
req.abort();
throw err;
})
}

它似乎工作正常,但感觉有点笨拙。

首先,async/await在这里似乎没有帮助。我不能扔也不能回到res.on('end'里面.返回只是从end回调中返回,但我没有真正的方法从内部中断Http.get(url, res => {函数。throwing不会"冒泡"到我创建的承诺,因为data/end事件不会同步触发。我必须手动呼叫reject

真正困扰我的部分是,如果服务器向我发送的数据比他们所说的通过Content-Length标头发送给我的数据多,我想中止请求并停止侦听事件。为此,我拒绝了 Promise,然后立即捕获它,以便我可以中止请求,然后重新抛出错误,以便调用方可以处理它。为此,我必须在 Promise 上方声明req变量,然后在 Promise 中初始化它,以便我可以在 Promise 之后访问它。

一切的流程感觉真的很笨拙。有没有更好的方法来写这个?(假设我有可用的所有 ES6/ES2017+/下一个功能)

以下是我处理该问题的方法:

  • "promisify"Http.get,意思是将API转换为使用承诺。
  • 将流逻辑分离到其自己的单独承诺中。
  • 在清晰的平面异步函数中检测错误逻辑。

这看起来像下面这样:

import Http from 'http';
// promisify Http.get
const http = (url, abort) => new Promise((resolve, reject) => {
let req = Http.get(url, res => {
if(res.statusCode !== 200) reject(new Error(`Request failed, got status ${res.statusCode}`));
else resolve(res);
});
abort(() => req.abort()); // allow aborting via passed in function
});
export async function get(url) {
let abort = null;
let res = await http(url, (onabort) => { abort = onabort; });
let contentLengthStr = res.headers['content-length'];
if(!contentLengthStr) new Error(`Missing Content-Length header not supported`);
if(contentLengthStr < 0 || contentLengthStr > (2*1024**3 +1)) {
throw new Error(`Bad Content-Length header: ${contentLengthStr}`);
}
try { 
return await read(res, contentLength);
} catch (e) {
abort();
throw e;
}
}
function read(res, contentLength) {
let buf = Buffer.allocUnsafe(contentLength), offset = 0;
return new Promise((resolve, reject) => {
res.on('data', chunk => {
if(chunk.length + offset > contentLength) {
return reject(new Error(`Received too much data, expected ${contentLength} bytes`));
}
chunk.copy(buf, offset);
offset += chunk.length;
});
res.on('end', () => {
if(offset === contentLength) resolve(buf);
else return reject(new Error(`Expected ${contentLength} bytes, received ${offset}`));
});
});
}

从技术上讲,这应该在代码审查中,但它是一个不同的受众。

第一个观察。如果我错了,请纠正我,但HTTP.get没有记录回调中返回值的用法。因此,除非函数代码使用await关键字,否则使用async关键字定义的回调会令人困惑(您确实提到它没有帮助)。

读者感到困惑的第二个来源是return reject( ...... );结构的使用。reject不返回值,并且我不知道正在使用的事件侦听器的返回值。因此,可以通过将return语句(如果实际需要)放在reject调用之后来颠倒顺序。

中止请求可以在 promise 执行器的作用域内完成,方法是将let req;移回执行器并根据需要显式调用.abort。不应再要求兑现承诺。重构后,事件发射器回调可能看起来像

res.on('data', chunk => {
if(chunk.length + offset > contentLength) {
reject(new Error(`Received too much data, expected ${contentLength} bytes`));
req.abort();
} else {
chunk.copy(buf, offset);
offset += chunk.length;
}
});
res.on('end', () => {
if(offset === contentLength) {
resolve(buf);
} else {
reject( new Error(`Expected ${contentLength} bytes, received ${offset}`));
req.abort();
}
});

这只剩下最后两个return reject....用法。您可以在每种情况下中止请求,也可以设置一个稍后检查的error变量:

req = Http.get(url, res => {
let error = null;
// ....
} else {
error = new Error(`Bad Content-Length header: ${contentLengthStr}`);
}
} else {
error = new Error(`Missing Content-Length header not supported`));
}
if( error) {
reject( error);
req.abort();
}
});

在此阶段,不再需要捕获承诺拒绝并重新抛出错误。当然,这是未经测试的,但希望能提供一些有用的反馈。

最新更新