[译] 2017 年成为更好的 Node 开发人员的 10 个技巧

925 阅读10分钟
原文链接: coyee.com

A person in a traditional cross-legged meditation pose, focusing on becoming a better Node developer

2017 年成为更好的 Node 开发者的 10 个技巧出自作者 Azat Mardan 之手。SitePoint 的客户帖子主要为你提供 Web 社区知名作家及演讲者的优秀内容。

注意: 这篇文章之前的标题是 “来自平台大神的最佳实践分享”。文章内容是经过实际的测试和试验写出,但并非 2017 年最新技巧。 虽然,Node 大神分享的最佳经典实践在 2017 年,2018 年甚至 2019 年都会受用,但一些尖端功能,像 async/await, promises 并没有涵盖在内。因为这些新特性不在 Node 核心代码中,也不如 npm 和 Express 流行。文章第二部分,我将介绍此文章的性质。

我开始作为全职Node开发是在2012年,当我加入了Storify公司。 从那时起,我从没有回头或者觉得挂念Python,Ruby,Java和PHP——这些贯穿我之前十年web开发工作的语言。

Storify对我来说是一份有趣的工作,因为不像大多数公司, Storify所有的项目运行(也许今后依旧)都是使用Javascript。你看,大多数公司,尤其是大公司如贝宝(PayPal)、沃尔玛(Walmart)、或第一资本(Capital One),只是使用Node作为他们堆栈的某部分。通常他们使用它作为API途径或一个业务流程层。这也是不错的,但是对于一个软件工程师来说,没有什么比完全沉浸在一个Node环境中更棒的了。

我这篇博客中,我会列出 2017 年成为更好 Node 开发者的技巧的大纲。这些技巧一部分是我在具体实践中总结得出的,一部分借鉴了 Node 和 npm 优秀模块开发者的经验。主要内容如下:

  1. 避免复杂性 — 尽可能将代码组织成较小的块,在非常小的基础上看看还能不能更加小。
  2. 使用异步代码 — 避免使用与 plague 类似的同步代码。
  3. 避免阻止 require — 将所有的 require 语句放在文件的顶部,因为它们是同步的,会阻止执行中的程序。
  4. 了解 require 可被缓存 — 它可以是代码中的新特性,也可能成为代码中的 Bug。
  5. 时刻检查错误 — 错误不是足球,不要将它留给别人处理,也不要忽视它。
  6. 旨在同步代码中使用 try…catch — try...catch 对异步代码无效,而且 V8 也不能像优化普通代码一样优化 try...catch。
  7. 返回回调或使用 if … else — 确定返回回调阻止继续执行程序。
  8. 监听错误事件 — 几乎所有 Node 的类/对象都从 EventEmitter 继承(观察者模式),并触发错误事件。请确认监听了这些事件。
  9. 了解 npm — 安装模块时使用 -S 或 -D,而不是 --save 或 --save-dev
  10. 在 package.json 中使用确定的版本:npm 会在你使用 -S 的时候选用默认版本,所以你需要手工修改版本号。不要相信 semver,但在开源模块中需要这样做。
  11. 附加 — 使用不同依赖。将项目所需要的东西放至 devDependencies 中开发,然后使用 npm i --production。多余的依赖越多,带来的风险也越大。

下面,让我们对每项内容都进行一下了解吧~

避免复杂性

npm 的作者 Isaac Z. Schlueter 写了一些模板,我们可以看下。例如, 用 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)

我相信大多数的读者都更喜欢第二个示例,尤其是在名称可以自解释时。当然,在你写代码时,你能理解它的运行方式。或许,你还会为能将如此多的方法用在一行代码中而沾沾自喜。但是,代码不会说话啊。如果你把代码写的复杂而严谨,当你时隔六个月再来看,或者某天你睡迷糊了或喝醉了来看,理解起它们来会非常困难,更不要说根本不了解其算法和复杂性的同事了。保持简单,这一准则在使用 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 有缓存。因此,只要解析的域名没有变化(在 nmp 模板中是没有的), 模块中的代码会只执行一次并将结果保存在一个变量中(同一进程内)。这是一个很不错的优化。然而,即使在缓存的情况下,你也最好把 require 语句放在前面。 看看下面的代码,在真正进入路由的时候才加载 axios 模块。/connect 路由出乎预料的慢,因为它在导入模块的时候才开始请求文件:

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 可被缓存

我在上面提到过 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 库。 你可以使用以下代码来避免在无数回调中出现手动检查的误差。 (Hello, callback hell).

var ok = require('okay')

request.get('/accounts', ok(console.error, (response)=>{
  try {
    data = JSON.parse(response)
  } catch(error) {
    console.error(error)
  }
}))

返回回调或使用 if … else

Node 是并发的。因此它有个特点就是,如果不加以注意,就会出现 bug。安全起见,我们使用 return 语句终止执行:

let error = true
if (error) return callback(error)
console.log('I will never run - good.')

避免由于错误的控制流导致的一些无意的并发(和失败操作)。

let error = true
if (error) callback(error)
console.log('I will run. Not 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)
})

了解 npm

很多 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 创建一个包含依赖的具体版本的新文件。

结语

这篇博客只是第一部分,其中已经涵盖了很多内容,从回调函数和异步代码的使用,到错误的检查和依赖的锁定。希望这篇文章能给你带来新的启示。

同时,如若文章有所遗漏或你有更好的做法,欢迎在下方留言,告诉我你的想法。