我会在这篇文章中简述 10 个技巧,帮助你在 2017 年成为更好的 Node 开发者。这些技巧有一些是我在实践中学习和领悟的,还有一些从优秀的 Node 和 npm 模块作者那里借鉴而来。本文的内容包含如下一些:
避免过于复杂— 把代码按尽可能小的块来组织。
使用异步代码— 避免像灾难一样的同步代码。
避免阻塞请求— 把所有 require 语句放在文件的顶部,因为它们是同步的,会阻塞执行中的程序。
了解 require 会被缓存 — 这会在你的代码中起到正面或负面的作用。
总是检查错误— 错误不是足球,不要抛弃错误,也不要跳过错误检查。
只在同步代码中使用 try…catch — try...catch 对异步代码无效,而且 V8 也不能像优化普通代码一样优化 try...catch。
返回回调或使用 if … else — 确定返回回调阻止继续执行程序。
监听错误事件— 几乎所有 Node 的类/对象都从 EventEmitter 继承(观察者模式),并触发错误事件。请确认监听了这些事件。
了解 npm — 安装模块时使用 -S 或 -D,而不是 --save 或 --save-dev
在 package.json 中使用确定的版本 :npm 会在你使用 -S 的时候选用默认版本,这很愚蠢。所以你需要手工修改版本号。不要相信 semver[译者注:语义化版本标准],但在开源模块中需要这样做。
加分 — 使用不同的依赖。把项目在开发阶段需要的东西放在 devDependencies 中,记得使用 npm i --production。多余的依赖越多,出现问题的风险就越大。
我们应该分别看待上述每一条,不是吗?
Isaac Z. Schlueter,是 npm 的创造者,我们来看看他写的代码。例如,用 use-strict 在模块中强制实施 JavaScript 严格模式,只需要三行代码:
var module = require('module') module.wrapper[0] += '"use strict";' Object.freeze(module.wrap)
为什么要避免复杂性呢?美国海军传说中,有一句短语很著名:化繁为简,返璞归真。(或者“保持简单,蠢货!”)。因为事实证明,人的大脑在同一时刻只能记忆 5 到 7 项内容。
把代码模块化为更小的部分,你和其它开发者才能更好的理解它。这样你也可以更好的测试它。看看这个例子:
app.use(function(req, res, next) { if (req.session.admin === true) return next() else return next(new Error('Not authorized')) }, function(req, res, next) { req.db = db next() })
或者这个:
const auth = require('./middleware/auth.js') const db = require('./middleware/db.js')(db) app.use(auth, db)
我相信大多数人会更喜欢第2个例子,尤其是在名称可以自解释的时候。当然,如果你在写代码,你会想知道它的工作方式。有时,你可能会耍耍小聪明,将多个方法在同一行里进行链式调用。不过,请按最简单的方式编写代码。因为在时隔 6 个月,或在你喝醉或极度兴奋的时候,再来看你写的这些代码,可能你自己都难以理解,更不用说那些不了解它的算法和复杂性的同事了。简单做事,尤其是在使用 Node 的异步方式的时候。
有一种 left-pad 事件 ,不过它只影响了依赖公共注册表的项目,并在 11 分钟之后重新发布。最小化所带来的益处远大于其缺点。而且,npm 已经 修改了它的发布策略 ,任何重要的项目都应该使用缓存或私有注册中心(作为临时解决方案)。
同步代码确实在 Node 中有一个(低的)位置。 它主要用于编写 CLI 命令或与 Web 应用程序无关的其他脚本。Node 开发者主要构建 Web 应用程序,因此他们使用异步代码,以避免阻塞线程。
例如,如果我们只是构建数据库脚本,而不是用来处理并发/并行任务的系统,这样就行了:
let data = fs.readFileSync('./acconts.json') db.collection('accounts').insert(data, (results))=>{ fs.writeFileSync('./accountIDs.json', results, ()=>{process.exit(1)}) })
但是在构建 Web 应用时下面写法会更佳:
app.use('/seed/:name', (req, res) => { let data = fs.readFile(`./${req.params.name}.json`, ()=>{ db.collection(req.params.name).insert(data, (results))=>{ fs.writeFile(`./${req.params.name}IDs.json`, results, ()={res.status(201).send()}) }) }) })
区别在于你是否在写并发(通常是长时间运行)还是非并发(短时间运行)的系统。 根据经验,我们总是在 Node 中使用异步代码。
Node 有一个简单的模块加载系统,使用 CommonJS 模块规范。它内建的 require 函数很容易把另外单独存放的的模块包含进来。与 AMD/requirejs 不同,Node/CommonJS 采用同步的方式加载模块。require工作方式是: 导入在模块或文件中导出的内容 。
const react = require('react')
大多数开发知道 require 有缓存。因此,只要解析出来的文件名没什么变化(在 npm 模块中是没有的),模块中的代码会只执行一次并将结果保存在一个变量中(同一进程内)。这个优化非常棒。然而,在有缓存的情况下,你仍然最好把 require 语句放在前面。看看下面的代码,在真正进入路由的时候才加载 axios 模块。/connect 路由出乎预料的慢,因为它在导入模块的时候才开始请求文件[译者注:IO 操作比 CPU 运算慢很多]:
app.post('/connect', (req, res) => { const axios = require('axios') axios.post('/api/authorize', req.body.auth) .then((response)=>res.send(response)) })
更好更高效的方式是服务器启动后就加载模块,而不是在路由中:
const axios = require('axios') const express = require('express') app = express() app.post('/connect', (req, res) => { axios.post('/api/authorize', req.body.auth) .then((response)=>res.send(response)) })
我在上一节提到过 require 缓存,但有趣的是,我们可以有 module.exports 之外的代码。例如,
console.log('I will not be cached and only run once, the first time') module.exports = () => { console.log('I will be cached and will run every time this module is invoked') }
知道一些代码可能仅运行一次,你可以使用这种此功能作为优势。
Node 不是 Java。在 Java 中,你可以抛出错误,因为多数时候你会在这些错误发生时中止应用程序的执行。在 Java 中,你可以通过一个单独的 try ... catch 处理多个错误。
在 Node 中不是这样。Node 使用 事件循环 和异步执行,错误发生时会不属于与处理代码(比如 try...catch)不同的上下文中。下面的作法在 Node 中无效:
try { request.get('/accounts', (error, response)=>{ data = JSON.parse(response) }) } catch(error) { // Will NOT be called console.error(error) }
不过 try...catch 仍然可以用于同步的 Node 代码。对上面的代码进行重构之后就好多了:
request.get('/accounts', (error, response)=>{ try { data = JSON.parse(response) } catch(error) { // Will be called console.error(error) } })
如果我们不能将 request 调用放在 try...catch 块中,我们就不能处理来自 request 的错误。Node 开发者采用提供包含 error 参数的回调来解决这个问题。这样你需要在每个回调中手工处理错误。你需要检查 error(确保它不是 null),然后将相应的错误消息显示给用户或者客户端,并记录下来。也可以通过调用栈中的 callback 往回传(如果你有回调,而且调用栈上还有另一个函数)。
request.get('/accounts', (error, response)=>{ if (error) return console.error(error) try { data = JSON.parse(response) } catch(error) { console.error(error) } })
你还可以使用 okay 库。你可以像下面的代码那样使用它来避免在无数的回调中手工检查错误(你好, 回调地狱 )。
var ok = require('okay') request.get('/accounts', ok(console.error, (response)=>{ try { data = JSON.parse(response) } catch(error) { console.error(error) } }))
Node 是并发的。所以,如果不加注意,它可能会变成一个错误。 为了安全起见,我们使用 return 语句终止执行:
let error = true if (error) return callback(error) console.log('I will never run - good.')
确保返回一个回调,以防止继续执行。
几乎所有的 Node 类/对象都扩展了事件发射器(观察者模式)并抛出错误事件。在错位被破坏之前,这给开发人员提供了捕获错误并处理的机会。
使用 .on() 为错误创建事件侦听器是个好习惯:
var req = http.request(options, (res) => { if (('' + res.statusCode).match(/^2/d/d$/)) { // Success, process response } else if (('' + res.statusCode).match(/^5/d/d$/)) // Server error, not the same as req error. Req was ok. } }) req.on('error', (error) => { // Can't even make a request: general error, e.g. ECONNRESET, ECONNREFUSED, HPE_INVALID_VERSION console.log(error) })
很多 Node 开发者甚至前端开发者都知道 --save(npm install 的参数)可以安装一个模块并在 package.json 中记录模块的版本。另外,还有 --save-dev,用于在 devDependencies 添加记录(记录那些不需要在发布时的模块)。不过你知道可以用 -S 和 -D 代替 --save 和 --save-dev 吗?你可以尝试这样做。
在安装模块的时候,去删除 -S 和 -D 为你添加的那些 ^ 记号。它们非常危险,因为它们允许 npm install(或简写为 npm i)从 npm 库中拉取最新的小版本(语义化的版本号中的第2个数)。比如从 v6.1.0 到 v6.2.0 就是一个小版本发布。
npm 团队信任 semver ,但你不应该信任它。我的意思是他们加上 ^ 符号是因为他们相信开源作者不会在小版本中引入打破接口的改变。任何理智的人都不应该相信它。锁定你的版本号。甚至最好使用 shrinkwrap :npm shrinkwrap 为会依赖的版本创建新的文件。
以上是文章的第一部分。 我们已经涵盖了很多东西,从使用回调函数和异步代码,到检查错误和锁定依赖。 我希望你在这里找到了一些新的或有用的东西。