【问题标题】:Promisfy writing file to filesystem承诺将文件写入文件系统
【发布时间】:2020-02-17 20:24:13
【问题描述】:

我有兴趣了解如何 Promisfy 这段代码:

const http = require('http');
const fs = require('fs');

const download = function(url, dest, cb) {
  let file = fs.createWriteStream(dest);
  const request = http.get(url, function(response) {
    response.pipe(file);
    file.on('finish', function() {
      file.close(cb);  // close() is async, call cb after close completes.
    });
  }).on('error', function(err) { // Handle errors
    fs.unlink(dest); // Delete the file async. (But we don't check the result)
    if (cb) cb(err.message);
  });
};

我对此的第一个看法是:

const http = require('http');
const fs = require('fs');

const download = async (url, dest, cb) => {
  let file = fs.createWriteStream(dest);
  const request = http.get(url, function(response) {
    response.pipe(file);
    file.on('finish', function() {
      const closed = await file.close(cb);  // close() is async, await here?
      if (closed) {
          // handle cleanup and retval
      }
    });
  }).on('error', function(err) { // Handle errors
    const deleted = await fs.unlink(dest); // Delete the file async. 
    if (!deleted) { ... }
  });
};

上面的实现显然是错误的。什么是正确的方法来删除回调并只使用 async/await?

【问题讨论】:

  • 两个提示。首先,在最新版本的 node.js 中,使用fs.promises 接口到fs 模块。这就是您获得对fs 模块的承诺支持的地方。像 fs.unlink() 这样的常规函数​​根本不支持 Promise,因此将 await 与它们一起使用没有任何用处。其次,流媒体没有内置的 Promise 支持。如果您想承诺使用流媒体的东西,您必须手动将其放入 new Promise(...) 并在流媒体完成或出现错误时手动调用 resolve()reject()
  • @jfriend00 streaming has no built-in promise support -- 不完全正确,请参阅可读流接口上的Symbol.asyncIterator
  • @PatrickRoberts - 好吧,我猜这很新(至少对我来说是新的)。您能否发布一个最能说明如何在这种情况下使用它的答案?
  • &jfriend00 我可以,但现在不行。如果你想对我进行 FGITW,我不会被冒犯
  • @PatrickRoberts - 老实说,我还没有深入了解 asyncIterators 或了解您如何使用它们来知道 .pipe() 何时完成,这是我认为需要知道的。

标签: javascript node.js promise async-await


【解决方案1】:

这是一种在 Promise 中手动包装管道操作的方法。不幸的是,其中大部分只是错误处理,以涵盖所有可能发生错误的地方:

const http = require('http');
const fs = require('fs');

const download = function(url, dest) {
  return new Promise((resolve, reject) => {
      const file = fs.createWriteStream(dest);

      // centralize error cleanup function
      function cleanup(err) {
          reject(err);
          // cleanup partial results when aborting with an error
          file.on('close', () => {
            fs.unlink(dest);
          });
          file.end();
      }

      file.on('error', cleanup).on('finish', resolve);

      const request = http.get(url, function(response) {
        if (response.status < 200 || response.status >= 300) {
            cleanup(new Error(`Unexpected Request Status Code: ${response.status}`);
            return;
        }
        response.pipe(file);
        response.on('error', cleanup);

      }).on('error', cleanup);
  });
};

download(someURL, someDest).then(() => {
  console.log("operation complete");
}).catch(err => {
  console.log(err);
});

这不会等待文件在错误条件下被关闭或删除,然后再拒绝(认为如果这些清理操作无论如何都存在错误,通常没有任何建设性可做)。如果需要,只需从这些清理操作的异步回调中调用 reject(err) 或使用这些函数的 fs.promises 版本并等待它们即可轻松添加。


有几点需要注意。这主要是错误处理,因为在三个可能的地方可能会出现错误,并且某些错误需要进行一些清理工作。

  1. 添加了必需的错误处理。

  2. 在 OP 的原始代码中,他们调用 file.close(),但 file 是一个流,并且 writeStream 上没有 .close() 方法。你调用.end()关闭写流。

  3. 您可能还需要检查适当的 response.status,因为即使状态为 4xx 或 5xx,http.get() 仍会返回响应对象和流。

【讨论】:

  • @Bergi nope,该规则的少数例外之一
  • 顺便说一句,我更愿意在http.get() 回调中声明file,这样我就不必在@987654335 返回的请求对象的error 处理程序中使用unlink() @.
  • @PatrickRoberts - 你的意思是在请求处理程序中调用file.createWriteStream(),所以如果请求立即失败,那么你不必清理文件?
  • @PatrickRoberts - 你们中的任何一个人都知道在管道时是否需要读取流和写入流上的错误处理程序?或者,是否有一个地方可以捕获任一类型的错误?
  • @Bergi - 好的,我通过将错误处理集中在一个清理函数中来简化错误处理。我知道您需要http.get().on('error' ...),因为某些失败(如错误的 URL)可能会立即失败。我知道您需要在 writestream 上使用错误处理程序,因为您可以获得完整磁盘或磁盘写入错误之类的信息。我假设您需要能够检测到开始流动的请求,但连接断开并因此出现错误。
【解决方案2】:

以下是我如何将您的节点样式回调 API 重写为异步函数:

const http = require('http');
const fs = require('fs');

async function download (url, dest) {
  const response = await new Promise((resolve, reject) => {
    http.get(url, resolve).once('error', reject);
  });

  if (response.status < 200 || response.status >= 300) {
    throw new Error(`${responses.status} ${http.STATUS_CODES[response.status]}`);
  }

  const file = await fs.promises.open(dest, 'w');

  try {
    for await (const data of response) {
      await file.write(data);
    }
  } catch (error) {
    await file.close();
    await fs.promises.unlink(dest);
    throw error;
  }

  await file.close();
}

请注意,此方法使用fs.promises 命名空间中的FileHandle 类,以及在Readable 流类上定义的Symbol.asyncIterator 接口,它允许您使用@ 的data 事件987654332@ 和 for await...of 循环,并通过隐式拒绝底层异步迭代器返回的承诺,将错误处理从 responseerror 事件传播到 catch 块。

【讨论】:

  • 这很酷。我认为您需要检查 response.status === 200,因为这是原始的 http.get(),不会为您检查。
  • 而且,我认为有一个隐藏的陷阱(我不确定我的答案是否正确处理)如果您在代码完成阅读所有 http.get() 响应之前保释,它可能会泄漏记忆。我在examples in the doc 中看到了一些暗示这种效果的东西。 nodejs 中似乎缺少一些更高级别的构造,因为这种类型的代码是 5% 的函数和 95% 的错误处理。我想你可以说,为了简单起见,流并没有真正完全承诺。
  • @jfriend00 究竟以什么方式保释?另外我可以说明一下,这里没有处理响应状态,但这甚至可能不是用户要求,我不喜欢假设,特别是如果这意味着进一步复杂化功能。
  • 假设您在 file.write(data) 上遇到写入错误。传入的http.get() 响应可能未完全使用。我添加了一个链接到我之前在文档中看到的评论。
  • @jfriend00 啊,如果循环提前退出,for await...of 会隐式调用异步迭代器对象上的 throwreturn 方法。那些应该已经在响应对象上实现了适当的清理方法,但如果你愿意,我可以仔细检查源代码。
猜你喜欢
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
  • 2017-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-22
相关资源
最近更新 更多