【问题标题】:Node.js how to wait on asynchronous call (readdir and stat)Node.js 如何等待异步调用(readdir 和 stat)
【发布时间】:2019-05-31 09:37:03
【问题描述】:

我正在使用服务器端的 post 方法来检索请求目录中的所有文件(非递归),下面是我的代码。

在不使用 setTimeout 的情况下,我无法使用更新的 pathContent 发回回复 (res.json(pathContent);)。

我了解这是由于所使用的文件系统方法(readdirstat)的异步行为造成的,因此需要使用某种回调、异步或承诺技术。

我尝试将async.waterfallreaddir 的整个主体一起用作一个函数,将res.json(pathContent) 作为另一个函数,但它没有将更新后的数组发送到客户端。

我知道关于这个异步操作有成千上万个问题,但在阅读了许多帖子后,我不知道如何解决我的问题。

任何 cmets 将不胜感激。谢谢。

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');

var pathName = '';
const pathContent = [];

app.post('/api/files', (req, res) => {
    const newPath = req.body.path;
    fs.readdir(newPath, (err, files) => {
        if (err) {
            res.status(422).json({ message: `${err}` });
            return;
        }
        // set the pathName and empty pathContent
        pathName = newPath;
        pathContent.length = 0;

        // iterate each file
        const absPath = path.resolve(pathName);
        files.forEach(file => {
            // get file info and store in pathContent
            fs.stat(absPath + '/' + file, (err, stats) => {
                if (err) {
                    console.log(`${err}`);
                    return;
                }
                if (stats.isFile()) {
                    pathContent.push({
                        path: pathName,
                        name: file.substring(0, file.lastIndexOf('.')),
                        type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
                    })
                } else if (stats.isDirectory()) {
                    pathContent.push({
                        path: pathName,
                        name: file,
                        type: 'Directory',
                    });
                }
            });
        });
    });    
    setTimeout(() => { res.json(pathContent); }, 100);
});

【问题讨论】:

  • 看看这个帖子,貌似他们用的是同步方法stackoverflow.com/questions/44019316/…
  • 感谢您的快速回复!根据参考资料,我使用了同步方法(readdirSync 和 statSync)并让它工作。

标签: javascript node.js express asynchronous


【解决方案1】:

最简单和最干净的方法是使用await/async,这样您就可以使用promise,并且代码几乎看起来像同步代码。

因此,您需要readdirstat 的承诺版本,可以由utils 核心库的promisify 创建。

const { promisify } = require('util')

const readdir = promisify(require('fs').readdir)
const stat = promisify(require('fs').stat)

async function getPathContent(newPath) {
  // move pathContent otherwise can have conflicts with concurrent requests
  const pathContent = [];

  let files = await readdir(newPath)

  let pathName = newPath;
  // pathContent.length = 0;  // not needed anymore because pathContent is new for each request

  const absPath = path.resolve(pathName);

  // iterate each file

  // replace forEach with (for ... of) because this makes it easier 
  // to work with "async" 
  // otherwise you would need to use files.map and Promise.all
  for (let file of files) {
    // get file info and store in pathContent
    try {
      let stats = await stat(absPath + '/' + file)
      if (stats.isFile()) {
        pathContent.push({
          path: pathName,
          name: file.substring(0, file.lastIndexOf('.')),
          type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
        })
      } else if (stats.isDirectory()) {
        pathContent.push({
          path: pathName,
          name: file,
          type: 'Directory',
        });
      }
    } catch (err) {
      console.log(`${err}`);
    }
  }

  return pathContent;
}

app.post('/api/files', (req, res, next) => {
  const newPath = req.body.path;
  getPathContent(newPath).then((pathContent) => {
    res.json(pathContent);
  }, (err) => {
    res.status(422).json({
      message: `${err}`
    });
  })
})

并且您不应该使用+ (absPath + '/' + file) 连接路径,而是使用path.join(absPath, file)path.resolve(absPath, file)

而且您永远不应该以这样一种方式编写代码:为请求执行的代码依赖于全局变量,例如 var pathName = '';const pathContent = [];。这可能适用于您的测试环境,但肯定会导致生产中的问题。其中两个请求同时处理变量“同时”

【讨论】:

  • 如果您 map 使用异步函数和 Promise.all 处理文件,您会获得一点性能提升,因为所有统计信息都可以并行运行,而不是顺序运行。 (pathContent = await Promise.all(files.map(async (file) =>{await stat(…);return {path: pathname …}})))
  • @GarrettMotzner 这取决于用例,如果您有一堆并行请求访问您的系统,那么您可能希望保留每个请求的顺序解决方案。
  • 这是一个有趣的问题,但我认为最好通过其他地方的速率限制来解决。对我来说,在大多数情况下,让 stat 调用顺序是没有意义的,因为那时调用之间没有依赖关系。在某些极端情况下,您最终会执行过多的并行系统调用,但如果是这种情况,您可能会遇到更大的问题。
  • @GarrettMotzner 我不想与您最初所说的相矛盾。是的,然后必须在其他地方添加速率限制。但是如果它是连续的,那么与当前活动请求的数量相关的负载更容易预测,并且可以更容易地处理速率限制。但是哪种解决方案更好取决于用例。如果这是一个罕见的动作,那么看看 map 和 Promise.all 可能会更好。
  • 您是否介意解释一下为什么 readdir 不需要包含在 try-catch 块中而只需包含在 stat 中?谢谢!
【解决方案2】:

这里有一些选项:

  • 使用同步文件方法(查看文档,但它们通常以Sync 结尾)。速度较慢,但​​代码更改相当简单,而且非常容易理解。
  • 使用promises(或util.promisify)为每个统计数据创建一个promise,并使用Promise.all等待所有统计数据完成。之后,您可以使用异步函数和等待,以便更容易阅读代码和更简单的错误处理。 (可能是最大的代码更改,但它会使异步代码更容易理解)
  • 记录您已完成的统计数量,如果该数量是您期望的大小,则在统计回调中调用res.json 表单(代码更改最小,但很容易出错)

【讨论】:

    【解决方案3】:

    有不同的方法:

    1. 您可以首先使用 new Promise() 对函数进行承诺,然后使用 async/await 或 .then()
    2. 可以使用Bluebird包的ProsifyAll()函数(https://www.npmjs.com/package/bluebird)
    3. 您可以使用同步版本的 fs 函数

    【讨论】:

      【解决方案4】:

      根据我收到的初始评论和参考,我改用了 readdirSync 和 statSync 并且能够使其工作。我还将查看其他答案并了解其他实现方式。

      感谢大家的热心投入。

      这是我的解决方案。

      const express = require('express');
      const bodyParser = require('body-parser');
      const fs = require('fs');
      const path = require('path');
      
      var pathName = '';
      const pathContent = [];
      
      app.post('/api/files', (req, res) => {
          const newPath = req.body.path;
      
          // validate path
          let files;
          try {
              files = fs.readdirSync(newPath);
          } catch (err) {
              res.status(422).json({ message: `${err}` });
              return;
          }
      
          // set the pathName and empty pathContent
          pathName = newPath;
          pathContent.length = 0;
      
          // iterate each file
          let absPath = path.resolve(pathName);
          files.forEach(file => {
              // get file info and store in pathContent
              let fileStat = fs.statSync(absPath + '/' + file);
              if (fileStat.isFile()) {
                  pathContent.push({
                      path: pathName,
                      name: file.substring(0, file.lastIndexOf('.')),
                      type: file.substring(file.lastIndexOf('.') + 1).concat(' File'),
                  })
              } else if (fileStat.isDirectory()) {
                  pathContent.push({
                      path: pathName,
                      name: file,
                      type: 'Directory',
                  });
              }
          });
          res.json(pathContent);
      });
      

      【讨论】:

      • 你真的应该把 var pathName const pathContent 移到你的 (req, res) => { ... } 中,这样它们就不再是全局的了,否则你会得到 "random" 意想不到的错误结果,如果您同时有两个或多个请求。
      • 在这种情况下(这是针对学校作业),一次只会有一个请求。服务器需要将这些作为其他请求(即 get)的全局变量来获取相同的信息。 (除非有更好的方法)顺便说一句,我非常感谢您的宝贵意见。我刚刚测试了您的代码并验证了它是否有效。接受您的回复作为答案。
      猜你喜欢
      • 2013-08-03
      • 1970-01-01
      • 2018-12-13
      • 2020-08-01
      • 2021-03-26
      • 1970-01-01
      • 1970-01-01
      • 2016-02-14
      • 2019-04-22
      相关资源
      最近更新 更多