Node.js:應對非同步執行的挑戰
Node.js 允许您快速轻松地创建应用程序。但由于其异步特性,可能很难编写可读且可管理的代码。在本文中,我将向您展示一些如何实现这一目标的技巧。
回调地狱或末日金字塔
Node.js 的构建方式强制您使用异步函数。这意味着回调,回调,甚至更多回调。您可能已经看到过,甚至自己编写过这样的代码:
app.get('/login', function (req, res) { sql.query('SELECT 1 FROM users WHERE name = ?;', [ req.param('username') ], function (error, rows) { if (error) { res.writeHead(500); return res.end(); } if (rows.length < 1) { res.end('Wrong username!'); } else { sql.query('SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ], function (error, rows) { if (error) { res.writeHead(500); return res.end(); } if (rows.length < 1) { res.end('Wrong password!'); } else { sql.query('SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ], function (error, rows) { if (error) { res.writeHead(500); return res.end(); } req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); }); } }); } }); });
这实际上是直接来自我的第一个 Node.js 应用程序的一个片段。如果您在 Node.js 中做过一些更高级的事情,您可能会理解所有内容,但这里的问题是,每次使用某些异步函数时,代码都会向右移动。它变得更难阅读,更难调试。幸运的是,有一些解决方案可以解决这个问题,因此您可以为您的项目选择合适的解决方案。
解决方案1:回调命名和模块化
最简单的方法是命名每个回调(这将帮助您调试代码)并将所有代码拆分为模块。只需几个简单的步骤即可将上面的登录示例变成一个模块。
结构
让我们从一个简单的模块结构开始。为了避免上述情况,当你只是将混乱分成更小的混乱时,让我们将其作为一个类:
var util = require('util'); function Login(username, password) { function _checkForErrors(error, rows, reason) { } function _checkUsername(error, rows) { } function _checkPassword(error, rows) { } function _getData(error, rows) { } function perform() { } this.perform = perform; } util.inherits(Login, EventEmitter);
该类由两个参数构造:用户名
和密码
。看示例代码,我们需要三个函数:一个检查用户名是否正确(_checkUsername
),另一个检查密码(_checkPassword
),还有一个返回用户相关数据(_getData
)并通知应用程序登录成功。还有一个 _checkForErrors
帮助器,它将处理所有错误。最后,有一个 perform
函数,它将启动登录过程(并且是类中唯一的公共函数)。最后我们继承EventEmitter
来简化该类的使用。
帮助者
_checkForErrors
函数将检查是否发生任何错误或 SQL 查询是否未返回任何行,并发出相应的错误(以及提供的原因):
function _checkForErrors(error, rows, reason) { if (error) { this.emit('error', error); return true; } if (rows.length < 1) { this.emit('failure', reason); return true; } return false; }
它还会返回 true
或 false
,具体取决于是否发生错误。
执行登录
perform
函数只需执行一个操作:执行第一个 SQL 查询(检查用户名是否存在)并分配适当的回调:
function perform() { sql.query('SELECT 1 FROM users WHERE name = ?;', [ username ], _checkUsername); }
我假设您的 SQL 连接可以在 sql
变量中全局访问(只是为了简化,讨论这是否是一个好的实践超出了本文的范围)。这就是这个函数的全部内容。
检查用户名
下一步是检查用户名是否正确,如果正确,则触发第二个查询 - 检查密码:
function _checkUsername(error, rows) { if (_checkForErrors(error, rows, 'username')) { return false; } else { sql.query('SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ username, password ], _checkPassword); } }
与混乱示例中的代码几乎相同,但错误处理除外。
检查密码
这个函数与前一个函数几乎完全相同,唯一的区别是调用的查询:
function _checkPassword(error, rows) { if (_checkForErrors(error, rows, 'password')) { return false; } else { sql.query('SELECT * FROM userdata WHERE name = ?;', [ username ], _getData); } }
获取用户相关数据
此类中的最后一个函数将获取与用户相关的数据(可选步骤)并用它触发成功事件:
function _getData(error, rows) { if (_checkForErrors(error, rows)) { return false; } else { this.emit('success', rows[0]); } }
最后的修饰和使用
最后要做的事情是导出类。在所有代码后面添加这一行:
module.exports = Login;
这将使 Login
类成为该模块将导出的唯一内容。稍后可以像这样使用它(假设您已将模块文件命名为 login.js
并且它与主脚本位于同一目录中):
var Login = require('./login.js'); ... app.get('/login', function (req, res) { var login = new Login(req.param('username'), req.param('password)); login.on('error', function (error) { res.writeHead(500); res.end(); }); login.on('failure', function (reason) { if (reason == 'username') { res.end('Wrong username!'); } else if (reason == 'password') { res.end('Wrong password!'); } }); login.on('success', function (data) { req.session.username = req.param('username'); req.session.data = data; res.redirect('/userarea'); }); login.perform(); });
这里又多了几行代码,但是代码的可读性增加了,非常明显。此外,该解决方案不使用任何外部库,这使得如果有新人加入您的项目,它会变得完美。
这是第一种方法,让我们继续第二种方法。
解决方案 2:Promise
使用 Promise 是解决此问题的另一种方法。承诺(正如您可以在提供的链接中阅读的那样)“表示从操作的单个完成中返回的最终值”。实际上,这意味着您可以链接调用以压平金字塔并使代码更易于阅读。
我们将使用 NPM 存储库中提供的 Q 模块。
Q简而言之
在开始之前,我先向您介绍一下Q。对于静态类(模块),我们主要使用 Q.nfcall
函数。它帮助我们将遵循 Node.js 回调模式(其中回调的参数是错误和结果)的每个函数转换为 Promise。它的使用方式如下:
Q.nfcall(http.get, options);
它非常像 Object.prototype.call
。您还可以使用 Q.nfapply
,它类似于 Object.prototype.apply
:
Q.nfapply(fs.readFile, [ 'filename.txt', 'utf-8' ]);
此外,当我们创建 Promise 时,我们使用 then(stepCallback)
方法添加每个步骤,使用 catch(errorCallback)
捕获错误,并使用 done()
结束。
在这种情况下,由于 sql
对象是一个实例,而不是静态类,所以我们必须使用 Q.ninvoke
或 Q.npost
,它们与上面类似。不同之处在于,我们将方法的名称作为字符串传递到第一个参数中,并将我们想要使用的类的实例作为第二个参数传递,以避免方法从实例。
准备承诺
首先要做的是执行第一步,使用Q.nfcall
或Q.nfapply
(使用你更喜欢的,下面没有区别):
var Q = require('q'); ... app.get('/login', function (req, res) { Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ?;', [ req.param('username') ]) });
请注意该行末尾缺少分号 - 函数调用将被链接起来,因此它不能在那里。我们只是像混乱的示例中那样调用 sql.query
,但我们省略了回调参数 - 它由 Promise 处理。
检查用户名
现在我们可以为 SQL 查询创建回调,它将与“厄运金字塔”示例中的回调几乎相同。在 Q.ninvoke
调用后添加以下内容:
.then(function (rows) { if (rows.length < 1) { res.end('Wrong username!'); } else { return Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ]); } })
如您所见,我们使用 then
方法附加回调(下一步)。另外,在回调中我们省略了 error
参数,因为我们稍后会捕获所有错误。我们正在手动检查查询是否返回某些内容,如果是,我们将返回下一个要执行的承诺(同样,由于链接而没有分号)。
检查密码
与模块化示例一样,检查密码与检查用户名几乎相同。这应该在最后一次 then
调用之后进行:
.then(function (rows) { if (rows.length < 1) { res.end('Wrong password!'); } else { return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); } })
获取用户相关数据
最后一步是将用户数据放入会话中。再一次,回调与混乱的示例没有太大区别:
.then(function (rows) { req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); })
检查错误
使用 Promise 和 Q 库时,所有错误均由使用 catch
方法的回调集处理。在这里,无论错误是什么,我们都只发送 HTTP 500,如上面的示例所示:
.catch(function (error) { res.writeHead(500); res.end(); }) .done();
之后,我们必须调用 done
方法来“确保,如果错误在结束之前没有得到处理,它将被重新抛出并报告”(来自库的 README)。现在我们漂亮的扁平化代码应该如下所示(并且行为就像混乱的代码一样):
var Q = require('q'); ... app.get('/login', function (req, res) { Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ?;', [ req.param('username') ]) .then(function (rows) { if (rows.length < 1) { res.end('Wrong username!'); } else { return Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ]); } }) .then(function (rows) { if (rows.length < 1) { res.end('Wrong password!'); } else { return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); } }) .then(function (rows) { req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); }) .catch(function (error) { res.writeHead(500); res.end(); }) .done(); });
代码更加简洁,并且比模块化方法涉及的重写更少。
解决方案 3:步骤库
此解决方案与上一个解决方案类似,但更简单。 Q 有点重,因为它实现了整个 Promise 的想法。 Step 库的存在只是为了消除回调地狱。使用起来也更简单,因为您只需调用从模块导出的唯一函数,将所有回调作为参数传递,并使用 this
代替每个回调。因此,可以使用 Step 模块将这个混乱的示例转换成这样:
var step = require('step'); ... app.get('/login', function (req, res) { step( function start() { sql.query('SELECT 1 FROM users WHERE name = ?;', [ req.param('username') ], this); }, function checkUsername(error, rows) { if (error) { res.writeHead(500); return res.end(); } if (rows.length < 1) { res.end('Wrong username!'); } else { sql.query('SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ], this); } }, function checkPassword(error, rows) { if (error) { res.writeHead(500); return res.end(); } if (rows.length < 1) { res.end('Wrong password!'); } else { sql.query('SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ], this); } }, function (error, rows) { if (error) { res.writeHead(500); return res.end(); } req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); } ); });
这里的缺点是没有通用的错误处理程序。尽管在一个回调中引发的任何异常都会作为第一个参数传递给下一个回调(因此脚本不会因为未捕获的异常而停止运行),但在大多数情况下,为所有错误使用一个处理程序是很方便的。
选择哪一个?
这很大程度上是个人选择,但为了帮助您选择正确的选择,以下列出了每种方法的优缺点:
模块化:
优点:
- 没有外部库
- 有助于提高代码的可重用性
缺点:
- 更多代码
- 如果您要转换现有项目,则需要进行大量重写
承诺(Q):
优点:
- 更少的代码
- 如果应用于现有项目,只需稍微重写
缺点:
- 您必须使用外部库
- 需要一些学习
步骤库:
优点:
- 易于使用,无需学习
- 如果转换现有项目,则几乎可以进行复制和粘贴
缺点:
- 没有通用的错误处理程序
- 正确缩进
step
函数有点困难
结论
正如您所看到的,Node.js 的异步特性是可以管理的,并且可以避免回调地狱。我个人使用模块化方法,因为我喜欢让我的代码结构良好。我希望这些技巧将帮助您编写更具可读性的代码并更轻松地调试脚本。
以上是Node.js:應對非同步執行的挑戰的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。
