重构 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);
});
// async/await
try {
const data = await myFunction();
doStuff(data);
}
catch (err) {
handle(err);
}
它不是更干净、更容易阅读吗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();
}
上面的代码使用 检索产品列表find
,然后遍历它们并逐一更新它们。它应该可以正常工作,但我们应该可以做得更好🤔。考虑以下替代方案:
选项 A:编写单个查询
我们可以轻松编写一个查询,一次性查找并更新所有产品,从而将责任委托给数据库,并将 N 个操作减少到1 个。方法如下:
await productModel.update({ outdated: true }, {
$set: {
outdated: false
}
});
选项 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 ]);
这种方法将执行所有异步函数,并等待所有函数都解析完毕。该方法仅在操作彼此之间没有依赖关系时有效。
3. 使用异步 fs 模块
Node 的fs
模块允许我们与文件系统进行交互。fs
模块中的每个操作都包含同步和异步选项。
这是用于读取文件的异步和同步代码的示例👇
// Async
fs.readFile(path, (err, data) => {
if (err)
throw err;
callback(data);
});
// Sync
return fs.readFileSync(path);
同步选项(通常以 结尾Sync
,例如readFileSync
)看起来更简洁,因为它不需要回调,但实际上可能会损害应用程序的性能。为什么?因为同步操作是阻塞的,所以当应用程序同步读取文件时,它会阻止任何其他代码的执行。
但是,如果能找到一种fs
异步使用模块并避免回调的方法就好了,对吧?查看下一个技巧来了解如何操作。
4. 使用util.promisify将回调转换为承诺
promisify
是来自 node.js 模块的一个函数。它接受一个遵循标准回调结构的函数,并将其转换为 Promise。这也允许在回调函数中util
使用。await
我们来看一个例子。node 模块中的函数readFile
和都遵循回调函数式的结构,因此我们将对它们进行 Promises 化,以便在 的异步函数中使用它们。access
fs
await
这是回调版本:
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);
});
});
}
这是“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);
}
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 });
}
}
那么,这里的问题是什么?想象一下,我们服务的第一行(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' });
}
}
在第二种方法中,我们实现了两件事:
- 我们的控制器现在有足够的信息来了解错误并采取相应的行动。
- REST API 客户端现在还将收到一个状态代码,这也能帮助他们处理错误。
我们甚至可以更进一步,构建一个全局错误处理程序或中间件来处理所有错误,这样我们就可以从控制器中清除该代码。不过,这是另一篇文章要讨论的内容。
这是另一个实现最常见错误类型的模块:node-common-errors。
有什么想法吗?💬
这些提示有用吗?
您希望我在本系列的下一篇文章中撰写有关其他 node.js 的主题吗?
您有什么技巧可以编写有效/干净的 node.js 代码?
我很乐意听取您的反馈!
文章来源:https://dev.to/paulasantamaria/refactoring-node-js-part-1-42fe