为什么 node.js `fs.existsSync` 包裹在 promise 中时效果不佳?

Why node.js `fs.existsSync` doesn't work well when wrapped in promise?

我正在编写一个函数 createFile 来在目录中创建一个文件,除非它已经存在。我正在使用 Node.js 原生 fs 包来执行所有文件操作。我想让我的函数异步,所以我将所有 fs 函数包装在 promises:

function writeFilePromise(writePath, textContent) {
    return new Promise((resolve, reject) => {
      fs.writeFile(writePath, textContent, (err) => {
        reject();
      });
      resolve();
    });
  }

  function mkDirPromise(dir) {
    return new Promise(((resolve, reject) => {
      fs.mkdir(path.join(constants.FILES_STORAGE_DIR, dir), (err) => {
        reject(err);
      });
      resolve();
    }));
  }

然后我还想在 promise 中包装 fs.existsSync 以完成我的功能,但包装它会导致偶尔出现不正确的行为,即,如果文件的目录不存在而我想创建一个目录将在没有文件的情况下创建为空。通过调试我发现只有同步 fs.existsSync 会工作 always。 此函数代码:

function createFile(dir, fileName, httpMethod, textContent) {
    return new Promise(((resolve, reject) => {
      const searchPath = path.join(ROOT_DIR, dir, fileName);
      if (httpMethod === POST && fs.existsSync(searchPath)) {
        reject();
      } else {
        const fileExistsStatus = fs.existsSync(path.join(ROOT_DIR, dir));
        (async function fsOperations() {
          try {
            if (!fileExistsStatus) {
              await mkDirPromise(dir);
            }
            await writeFilePromise(searchPath, textContent);
            resolve();
          } catch (err) {
            reject(err);
          }
        }());
      }
    }));
  }

我缺少什么以及如何将我的函数变成真正的异步函数?

occasional incorrect behavior, namely, if a directory for the file didn't exist and I wanted to create one the directory would be created empty without the file

这可能是由于 writeFilePromise 尤其是 mkDirPromise 的不正确实现造成的。 fs.writeFilefs.mkdir 是异步的,但承诺是同步解决的。应该是:

  function writeFilePromise(writePath, textContent) {
    return new Promise((resolve, reject) => {
      fs.writeFile(writePath, textContent, (err) => {
        if (err)
          reject(err);
        else
          resolve();
      });
    });
  }

  function mkDirPromise(dir) {
    return new Promise(((resolve, reject) => {
      fs.mkdir(path.join(constants.FILES_STORAGE_DIR, dir), (err) => {
        if (err)
          reject(err);
        else
          resolve();
      });
    }));
  }

这就是 util.promisify 的用途:

const writeFilePromise = util.promisify(fs.writeFile);

即便如此,这也是一个轮子的重新发明,因为已经有第三方包可以做到这一点甚至更多,即 fs-extra.

createFile 控制流很差并且使用了 promise 构造反模式。既然用了async..await,应该是:

async function createFile(dir, fileName, httpMethod, textContent) {
  const searchPath = path.join(ROOT_DIR, dir, fileName);
  if (httpMethod === POST && fs.existsSync(searchPath)) {
    throw new Error();
  } else {
    const fileExistsStatus = fs.existsSync(path.join(ROOT_DIR, dir));
    if (!fileExistsStatus) {
      await mkDirPromise(dir);
    }
    await writeFilePromise(searchPath, textContent);
  }
}

值得一提的是existsSync是稳定的API方法,用它来检查文件是否存在是可以接受的。正如 the documentation 所述,

Note that fs.exists() is deprecated, but fs.existsSync() is not. (The callback parameter to fs.exists() accepts parameters that are inconsistent with other Node.js callbacks. fs.existsSync() does not use a callback.)

首先,您已经构建了 writeFilePromisemkDirPromise,因此它们将始终 resolve,而永远不会 reject。自 fs.writeFilefs.mkdir 是异步的,一旦它们开始,线程会立即移动到 resolve()。我想你的意思是...

function writeFilePromise(writePath, textContent) {
    return new Promise((resolve, reject) => {
        fs.writeFile(writePath, textContent, (err) => {
            if (err) reject();
            else resolve();
        });
    });
}

function mkDirPromise(dir) {
    return new Promise((resolve, reject) => {
        fs.mkdir(path.join(constants.FILES_STORAGE_DIR, dir), (err) => {
            if (err) reject();
            else resolve();
        });
    });
}

fs.exists而言,它已被贬低,所以我不推荐使用它。相反,尝试 fs.access:

function accessPromise(dir) {
    return new Promise((resolve, reject) => {
        fs.access(dir, (err) => {
            if (err) reject();
            else resolve();
        });
    });
}

最后,尝试调整使用 async 函数声明的位置,以确保正确同步代码:

async function createFile(dir, fileName, httpMethod, textContent) {
    const searchPath = path.join(ROOT_DIR, dir, fileName);
    if (httpMethod === POST && await accessPromise(searchPath)) {
        return false;
    } else {
        const fileExistsStatus = await accessPromise(path.join(ROOT_DIR, dir));
        try {
            if (!fileExistsStatus) {
                await mkDirPromise(dir);
            }
            await writeFilePromise(searchPath, textContent);
            return true;
        } catch (err) {
            return false;
        }
    }
}

调用该函数时记得使用await createFile(dir, fileName, httpMethod, textContent)

首先,考虑用有意义的东西拒绝,而不仅仅是 reject()

既然你在考虑 async 和 promise,我不建议使用 fs.xxxSync() 函数。此外,fs.exists 已弃用,请尝试使用 fs.stat().

我猜你只会在 HTTP 方法为 POST 时创建文件,但在当前的 if-else 逻辑中,当 HTTP 方法不是 POST 时,文件将始终被创建。

无需创建立即调用的异步函数。

试试这个:

function createFile(dir, fileName, httpMethod, textContent) {
    return new Promise((resolve, reject) => {
        const searchPath = path.join(ROOT_DIR, dir, fileName);
        if (httpMethod !== POST) {
            return reject(new Error('Invalid HTTP method'));
        }
        fs.exists(searchPath, (exists) => {
            if (exists) {
                return reject(new Error('Already exists'));
            }
            fs.exists(path.join(ROOT_DIR, dir), async (exists) => {
                try {
                    if (!exists) {
                        await mkDirPromise(dir);
                    }
                    await writeFilePromise(searchPath, textContent);
                    resolve();
                } catch (err) {
                    reject(err);
                }
            });
        });
    });
}