重构 node.js(第一部分) 1. 使用 async/await 2. 避免在循环中使用 await 3. 使用 async fs 模块 4. 使用 util.promisify 将回调转换为 promise 5. 使用描述性错误类型 有什么想法吗?💬

2025-05-26

重构 node.js(第一部分)

1. 使用 async/await

2. 避免在循环中使用 await

3. 使用异步 fs 模块

4. 使用util.promisify将回调转换为承诺

5. 使用描述性错误类型

有什么想法吗?💬

这是系列文章的第一部分,我将在其中分享编写更清晰、更有效的 node.js代码的技巧。

1. 使用 async/await

因此,在 Javascript 中编写异步代码有 3 种方法:回调、promise 和 async/await。

(如果您还没有逃脱回调地狱,我建议您查看另一篇 dev.to 文章:@amberjones撰写的如何使用 JavaScipt Promises 逃脱回调地狱

Async/await 允许我们以比承诺更清晰、更易读的语法构建异步非阻塞代码👍。

让我们看一个例子,下面的代码执行myFunction(),返回结果并处理函数可能抛出的任何错误:

// Promises
myFunction()
    .then(data => {
        doStuff(data);
    })
    .catch(err => {
        handle(err);
    });
Enter fullscreen mode Exit fullscreen mode
// async/await
try {
    const data = await myFunction();
    doStuff(data);
}
catch (err) {
    handle(err);
}
Enter fullscreen mode Exit fullscreen mode

它不是更干净、更容易阅读吗async/await

关于 async/await 的一些额外提示:

  • 任何返回 Promise 的函数都可以被等待。
  • await关键字只能在异步函数中使用。
  • 您可以使用并行执行异步函数await Promise.all([asyncFunction1, asyncFunction2])

2. 避免在循环中使用 await

由于 async/await 非常干净且易读,我们可能会倾向于做这样的事情:

const productsToUpdate = await productModel.find({ outdated: true });

for (const key in productsToUpdate) {
    const product = productsToUpdate[key];

    product.outdated = false;
    await product.save();
}
Enter fullscreen mode Exit fullscreen mode

上面的代码使用 检索产品列表find,然后遍历它们并逐一更新它们。它应该可以正常工作,但我们应该可以做得更好🤔。考虑以下替代方案:

选项 A:编写单个查询

我们可以轻松编写一个查询,一次性查找并更新所有产品,从而将责任委托给数据库,并将 N 个操作减少到1 个。方法如下:

await productModel.update({ outdated: true }, {
    $set: {
        outdated: false
    }
 });
Enter fullscreen mode Exit fullscreen mode

选项 B:Promise.all

需要明确的是,在这个例子中,选项 A肯定是可行的方法,但如果异步操作不能合并为一个(也许它们不是数据库操作,而是对外部 REST API 的请求),您应该考虑使用以下方法并行运行所有操作Promise.all

const firstOperation = myAsyncFunction();
const secondOperation = myAsyncFunction2('test');
const thirdOperation = myAsyncFunction3(5);

await Promise.all([ firstOperation, secondOperation, thirdOperation ]);
Enter fullscreen mode Exit fullscreen mode

这种方法将执行所有异步函数,并等待所有函数都解析完毕。该方法仅在操作彼此之间没有依赖关系时有效。

3. 使用异步 fs 模块

Node 的fs模块允许我们与文件系统进行交互。fs模块中的每个操作都包含同步和异步选项。

这是用于读取文件的异步和同步代码的示例👇

// Async
fs.readFile(path, (err, data) => {
    if (err)
        throw err;

    callback(data);
});

// Sync 
return fs.readFileSync(path);

Enter fullscreen mode Exit fullscreen mode

同步选项(通常以 结尾Sync,例如readFileSync)看起来更简洁,因为它不需要回调,但实际上可能会损害应用程序的性能。为什么?因为同步操作是阻塞的,所以当应用程序同步读取文件时,它会阻止任何其他代码的执行。

但是,如果能找到一种fs异步使用模块避免回调的方法就好了,对吧?查看下一个技巧来了解如何操作。

4. 使用util.promisify将回调转换为承诺

promisify是来自 node.js 模块的一个函数。它接受一个遵循标准回调结构的函数,并将其转换为 Promise。这也允许在回调函数中util使用。await

我们来看一个例子。node 模块中的函数readFile都遵循回调函数式的结构,因此我们将对它们进行 Promises 化,以便在 的异步函数中使用它们accessfsawait

这是回调版本:

const fs = require('fs');

const readFile = (path, callback) => {
    // Check if the path exists.
    fs.stat(path, (err, stats) => {
        if (err)
            throw err;

        // Check if the path belongs to a file.
        if (!stats.isFile())
            throw new Error('The path does not belong to a file');

        // Read file.
        fs.readFile(path, (err, data) => {
            if (err)
                throw err;

            callback(data);
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

这是“promisified”+异步版本👌:

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

const readFilePromise = util.promisify(fs.readFile);
const statPromise = util.promisify(fs.stat);

const readFile = async (path) => {
    // Check if the path exists.
    const stats = await statPromise(path);

    // Check if the path belongs to a file.
    if (!stats.isFile())
        throw new Error('The path does not belong to a file');

    // Read file.
    return await readFilePromise(path);
}
Enter fullscreen mode Exit fullscreen mode

5. 使用描述性错误类型

假设我们正在构建一个 REST API 端点,该端点通过 ID 返回产品。服务将处理逻辑,控制器将处理请求、调用服务并构建响应:

/* --- product.service.js --- */

const getById = async (id) => {
    const product = await productModel.findById(id);

    if (!product)
        throw new Error('Product not found');

    return product;
}

/* --- product.controller.js --- */

const getById = async (req, res) => {
    try {
        const product = await productService.getById(req.params.id);

        return product;
    }
    catch (err) {
        res.status(500).json({ error: err.message });
    }
}
Enter fullscreen mode Exit fullscreen mode

那么,这里的问题是什么?想象一下,我们服务的第一行(productModel.findById(id))抛出了一个数据库或网络相关的错误,在前面的代码中,该错误的处理方式与“未找到”错误完全相同。这会使客户端处理错误变得更加复杂。

此外,还有一个更大的问题:出于安全原因,我们不希望任何错误返回给客户端(我们可能会泄露敏感信息)。

我们该如何解决这个问题?

处理这个问题的最佳方法是针对每种情况使用不同的 Error 类实现。这可以通过构建我们自己的自定义实现,或者安装一个已经包含我们需要的所有 Error 实现的库来实现。

对于 REST API,我喜欢使用throw.js。这是一个非常简单的模块,包含与最常见的 HTTP 状态码匹配的错误。该模块定义的每个错误都包含状态码作为属性。

让我们看看前面的例子如何使用throw.js

/* --- product.service.js --- */
const error = require('throw.js');

const getById = async (id) => {
    const product = await productModel.findById(id);

    if (!product)
        throw new error.NotFound('Product not found');

    return product;
}

/* --- product.controller.js --- */
const error = require('throw.js');

const getById = async (req, res) => {
    try {
        const product = await productService.getById(req.params.id);

        return product;
    }
    catch (err) {
        if (err instanceof error.NotFound)
            res.status(err.statusCode).json({ error: err.message });
        else
            res.status(500).json({ error: 'Unexpected error' });
    }
}
Enter fullscreen mode Exit fullscreen mode

在第二种方法中,我们实现了两件事:

  • 我们的控制器现在有足够的信息来了解错误并采取相应的行动。
  • REST API 客户端现在还将收到一个状态代码,这也能帮助他们处理错误。

我们甚至可以更进一步,构建一个全局错误处理程序或中间件来处理所有错误,这样我们就可以从控制器中清除该代码。不过,这是另一篇文章要讨论的内容。

这是另一个实现最常见错误类型的模块:node-common-errors

有什么想法吗?💬

这些提示有用吗?

您希望我在本系列的下一篇文章中撰写有关其他 node.js 的主题吗?

您有什么技巧可以编写有效/干净的 node.js 代码?

我很乐意听取您的反馈

文章来源:https://dev.to/paulasantamaria/refactoring-node-js-part-1-42fe
PREV
你的 DEV 之年 - 查看你的数据!好奇你的数据?快来试试吧!我的 DEV 之年
NEXT
掌握 NPM 脚本简介内置脚本和别名执行多个脚本理解错误静默或大声运行脚本从文件引用脚本前后访问环境变量传递参数命名约定文档结论