Node.js 完全手册(上)

308 阅读48分钟

The definitive Node.js handbook

注意:你可以得到这本手册的 [PDF、ePub 或 Mobi][1] 版本,以方便参考,或在你的 Kindle 或平板电脑上阅读。

Node.js 简介

本手册是 Node.js 的入门指南,它是服务器端 JavaScript 运行环境。

概况

Node.js 是一个 服务器 上的 JavaScript 运行环境

Node.js 是开源的、跨平台的,自从 2009 年推出以来,它大受欢迎,现在在 Web 开发领域发挥着重要作用。如果 GitHub 的星星是一个流行的指示因素,那么拥有 58000 多颗星星就意味着非常流行。

Node.js 在浏览器之外运行 V8 JavaScript 引擎,这是 Chrome 浏览器的核心。Node.js 能够利用那些使 Chrome 浏览器的 JavaScript 运行变得非常快的成果,这使得 Node.js 能够从 V8 执行的巨大性能改进和即时编译中受益。得益于此,在 Node.js 中运行的 JavaScript 代码可以变得非常有性能。

一个 Node.js 应用程序是由一个单一的进程运行的(single process),不需要为每个请求创建一个新的线程(new thread)。Node 在其标准库中提供了一套异步 I/O 原生语法,这将防止 JavaScript 代码被阻塞,一般来说,Node.js 中的库是使用非阻塞范式编写的,使阻塞行为成为例外,而不是正常的。

当 Node.js 需要执行 I/O 操作时,比如从网络中读取数据、访问数据库或文件系统,而不是阻塞线程,Node.js 会在响应回来时恢复操作,而不是浪费 CPU 来等待。

这使得 Node.js 可以用一台服务器处理成千上万的并发连接,而不需要引入管理线程并发的负担,这将是错误的主要来源。

Node.js 有一个独特的优势,因为数百万为浏览器编写 JavaScript 的前端开发人员现在能够运行服务器端代码和前端代码,而不需要学习一种完全不同的语言。

在 Node.js 中,新的 ECMAScript 标准可以顺利使用,因为你不必等待所有用户更新他们的浏览器--你通过改变 Node.js 的版本来决定使用哪个 ECMAScript 版本,你还可以通过运行带有标志(flags)的 Node 来启用特定的实验性功能。

Node.js 有大量的库

凭借其简单的结构,Node 包管理器([NPM][2])帮助 Node.js 的生态系统激增。现在,[NPM registry][3] 托管了近 50 万个开源包,你可以自由使用。

一个 Node.js 应用程序的例子

Node.js 最常见的例子 Hello World 是一个网络服务器:

const http = require('http')
const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req, res) => {  
    res.statusCode = 200  
    res.setHeader('Content-Type', 'text/plain')  
    res.end('Hello World\n')
})

server.listen(port, hostname, () => {  
  console.log(`Server running at http://${hostname}:${port}/`)
})

要运行这个片段,将其保存为 server.js 文件,并在终端运行 node server.js

这段代码首先包括 Node.js [http模块][4]。

Node.js 有一个惊艳的 [标准库][5],包括对网络的一流支持。

httpcreateServer() 方法创建一个新的 HTTP 服务器并返回。

该服务器被设置为在指定的端口和主机名上监听。当服务器准备好时,回调函数被调用,在这种情况下,通知我们服务器正在运行。

每当收到一个新的请求,[request event][6] 被调用,提供两个对象:一个请求(一个 [http.IncomingMessage][7] 对象)和一个响应(一个 [http.ServerResponse][8] 对象)。

这两个对象对于处理 HTTP 调用是必不可少的。

第一个对象提供请求的细节。在这个简单的例子中,这个没有被使用,但是你可以访问请求头和请求数据。

第二个对象是用来返回数据给调用者的。

在这种情况下:

res.statusCode = 200

我们将 "statusCode" 属性设置为 "200",以表示成功响应。

我们设置 Content-Type 头:

res.setHeader('Content-Type', 'text/plain')

……我们最后关闭响应,将内容作为一个参数添加到 end():

res.end('Hello World\n')

Node.js 框架和工具

Node.js 是一个低代码(low-level)平台。为了让开发者更容易、更有趣,成千上万的库被建立在Node.js 之上。

其中许多人随着时间的推移成为了流行的选择。这里有一个不全面的列表,列出了我认为非常相关和值得学习的那些:

  • [Express][9]
    创建一个网络服务器的最简单而强大的方法之一。它的极简方法和对服务器核心功能的无偏见关注是其成功的关键。
  • [Meteor][10]
    一个令人难以置信的强大的全栈框架,赋予你用 JavaScript 构建应用程序的同构方法,在客户端和服务器上共享代码。曾经是一个提供一切的现成工具,现在它与前端库如[React][11]、[Vue][12] 和 [Angular][13] 集成。Meteor 也可以用来创建移动应用程序。
  • [Koa][14]
    由 Express 背后的同一个团队建立,Koa 旨在更简单和更小,建立在多年的知识之上。这个新项目的诞生是由于需要在不破坏现有社区的情况下,创造不兼容的变化。
  • [Next.js][15]
    这是一个用于渲染服务器端的 [React][16] 应用程序的框架。
  • [Micro][17]
    这是一个非常轻量级的服务器,用于创建异步的 HTTP 微服务。
  • [Socket.io][18]
    这是一个实时通信引擎,用于构建网络应用。

Node.js 的简史

回顾 2009 年到今天的 Node.js 的历史

信不信由你,Node.js 只有 9 年的历史。

相比之下,JavaScript 有 23 年的历史,而我们所知的网络(在引入 Mosaic 之后)有 25 年的历史。

对于一项技术来说,9 年的时间实在是太短了,但 Node.js 似乎已经存在了很久。

我有幸从 Node.js 的早期就开始工作,当时它只有 2 年的历史,尽管信息很少,但你已经可以感觉到它是一个巨大的东西。

在这一节中,我想画出 Node.js 在历史上的大图景,把事情看清楚。

一段小的历史

JavaScript 是一种编程语言,是在网景公司创建的,作为一种脚本工具,在他们的浏览器 [Netscape Navigator][19] 中操作网页。

网景公司的部分商业模式是销售网络服务器,其中包括一个名为 "Netscape LiveWire" 的环境,它可以使用服务器端的 JavaScript 创建动态页面。因此,服务器端 JavaScript 的想法并不是由 Node.js 引入的,它就像 JavaScript 一样古老--但在当时它并不成功。

导致 Node.js 崛起的一个关键因素是时机。几年前,JavaScript 开始被认为是一种严肃的语言,这要归功于 "Web 2.0 "应用程序,它们向世界展示了网络上的现代体验是什么样的(想想谷歌地图或 GMail)。

由于浏览器的竞争,JavaScript 引擎的性能标准大大提高了,这种竞争仍在继续。每个主要浏览器背后的开发团队每天都在努力工作,为我们提供更好的性能,这对 JavaScript 这个平台来说是一个巨大的胜利。Chrome V8,即 Node.js 背后使用的引擎,就是其中之一,特别是它的 Chrome JavaScript 引擎。

但当然,Node.js 的流行并不只是因为纯粹的运气或时机。它引入了许多关于如何在服务器上用 JavaScript 编程的创新思维。

2009

Node.js 的诞生

第一种形式的 [npm][20] 的诞生

2010

[Express][21] 诞生

[Socket.io][22] 诞生

2011

npm 达到 1.0 版本

公司开始采用 Node。[LinkedIn][23], [Uber][24]

[Hapi][25] 诞生

2012

被采用的速度非常快

2013

第一个使用 Node.js 的大型博客平台: [Ghost][26]

[Koa][27] 诞生

2014

大事件: [IO.js][28] 是 Node.js 的一个重要分叉,目标是引入 ES6 支持,并快速推进。

2015

[Node.js 基金会][29] 诞生

IO.js 回归到 Node.js 中

npm 引入了私有模块

[Node 4][30] 发布 (之前没有发布过 1、2、3 版本)

2016

[leftpad 事件][31]

[Yarn][32] 诞生: Node 6 发布

2017

npm 更专注于安全: Node 8 发布

[HTTP/2][33]

[V8][34] 在其测试套件中引入了 Node,正式将 Node 作为除 Chrome 之外的 JavaScript 引擎的目标。

每周 30 亿次 npm 下载

2018

Node 10 发布

[ES modules][35].

[mjs][36] 实验性支持

如何安装 Node.js

如何在你的系统上安装 Node.js:使用软件包管理器、官方网站安装程序或 nvm

Node.js 可以通过不同的方式进行安装。这篇文章强调了最常见和最方便的方式。

所有主要平台的官方软件包都可以使用[这里][37]。

安装 Node.js 的一个非常方便的方法是通过包管理器。在这种情况下,每个操作系统都有自己的。

在 macOS 上,[Homebrew][38] 是事实上的标准,而且一旦安装,就可以通过在 CLI 中运行这个命令,非常容易地安装 Node.js:

brew install node

其他用于 Linux 和 Windows 的软件包管理器被列出 [这里][39]。

[nvm][40] 是运行 Node.js 的一种流行方式。它允许你轻松地切换 Node.js 的版本,并安装新的版本来尝试,并在发生故障时轻松回滚,例如。

它对于用旧的 Node.js 版本测试你的代码也非常有用。

我的建议是,如果你刚刚开始,而且你还没有使用 Homebrew,就使用官方安装程序。否则,Homebrew 是我最喜欢的解决方案。

使用 Node.js,你需要知道多少 JavaScript?

如果你刚刚开始学习 JavaScript,你需要对这门语言有多深的了解?

作为一个初学者,你很难达到对自己的编程能力有足够信心的程度。

在学习编程的过程中,你可能也会迷惑不解,到底哪里是 JavaScript 的终点,哪里是 Node.js 的起点,反之亦然。

我建议你在深入学习 Node.js 之前,先对主要的 JavaScript 概念有一个很好的掌握:

  • Lexical Structure(语法结构)
  • Expressions(表示式)
  • Types (类型)
  • Variables (变量)
  • Functions (函数)
  • this
  • Arrow Functions (箭头函数)
  • Loops (循环)
  • Loops and Scope (循环和作用域)
  • Arrays (数组)
  • Template Literals (模板文字)
  • Semicolons (分号)
  • Strict Mode (严格模式)
  • ECMAScript 6, 2016, 2017 (ES6 ES2016 ES2017 标准)

有了这些概念,你就可以在浏览器和 Node.js 中成为一名熟练的 JavaScript 开发者了。

以下概念也是理解异步编程的关键,这也是 Node.js 的一个基本部分:

  • 异步编程(Asynchronous programming)和回调(callbacks)
  • 定时器(Timers)
  • Promises
  • Async and Await
  • 闭包(Closures)
  • 事件循环(The Event Loop)

幸运的是,我写了一本免费的电子书,解释了所有这些主题,它叫做 [JavaScript 基础知识][41]。这是你能找到的学习所有这些的最紧凑的资源。

Node.js 和浏览器之间的差异

在 Node.js 中编写 JavaScript 应用程序与在浏览器内为网络编程有什么不同。

浏览器和 Node 都使用 JavaScript 作为其编程语言。

构建在浏览器中运行的应用程序与构建 Node.js 应用程序是完全不同的事情。

尽管它始终是 JavaScript,但有一些关键的区别,使体验有了根本的不同。

编写 Node.js 应用程序的前端开发者有一个巨大的优势--语言仍然是一样的。

你有一个巨大的机会,因为我们知道全面、深入地学习一门编程语言是多么困难。通过使用相同的语言来执行你在网络上的所有工作--无论是在客户端还是在服务器上--你就处于一个独特的优势地位。

生态系统的变化。

在浏览器中,大多数时候你所做的是与 DOM 或其他网络平台 API(如 Cookies )进行交互。当然,这些并不存在于 Node.js 中。你没有 documentwindow 和所有其他由浏览器提供的对象。

而且在浏览器中,我们没有 Node.js 通过其模块提供的所有好用的 API,如文件系统访问功能。

另一个很大的区别是,在 Node.js 中你可以控制环境。除非你正在构建一个任何人都可以在任何地方部署的开源应用程序,否则你知道你将在哪个版本的 Node.js 上运行该应用程序。与浏览器环境相比,你不能奢侈地选择你的访问者将使用什么浏览器。

这意味着你可以编写所有你的 Node 版本支持的现代 ES6-7-8-9 的 JavaScript。

由于 JavaScript 的发展如此之快,但浏览器可能有点慢,用户的升级也有点慢--有时在网络上,你只能使用旧的 JavaScript/ECMAScript 版本。

你可以使用 Babel 将你的代码转换为兼容 ES5 的版本,然后再运到浏览器上,但在 Node.js 中,你就不需要这样了。

另一个区别是,Node.js 使用 [CommonJS][42] 模块系统,而在浏览器中,我们开始看到 ES 模块标准被实施。

在实践中,这意味着你暂时在 Node.js 中使用 require(),而在浏览器中使用 import(译者注:Node 13.2.0 起开始正式支持 ES Modules,可以在 Node 中使用 import :)。

V8 JavaScript 引擎

V8 是谷歌浏览器的 JavaScript 引擎的名字。在使用 Chrome 浏览器浏览时,它能接收我们的 JavaScript 并执行它。

V8 提供了运行时环境,在其中执行 JavaScript。DOM 和其他网络平台 API 是由浏览器提供的。

最酷的是,JavaScript 引擎是独立于它所承载的浏览器的。这一关键特征使 Node.js 的崛起成为可能。V8 早在 2009 年就被 Node.js 选择为引擎,随着 Node.js 的普及,V8 成为现在为大量用 JavaScript 编写的服务器端代码提供动力的引擎。

Node.js 的生态系统是巨大的,由于它的存在,V8也为桌面应用程序提供了动力,比如 [Electron][43] 等项目。

其他 JS 引擎

其他浏览器有自己的 JavaScript 引擎:

  • Firefox 使用 [Spidermonkey][44]
  • Safari 使用 [JavaScriptCore][45] (也叫 Nitro)
  • Edge 使用 [Chakra][46](译者注: 现在 Edge 放弃自己的引擎,使用 chrome 一样的引擎,即 V8)

还有很多 JavaScript 引擎。

所有这些引擎都实现了 ECMA ES-262 标准,也叫 ECMAScript,即 JavaScript 使用的标准。

对性能的追求

V8 是用 C++ 编写的,而且它在不断改进。它是可移植的,可以在 Mac、Windows、Linux 和其他一些系统上运行。

在这个 V8 介绍中,我将忽略 V8 的实现细节。它们可以在更权威的网站上找到,包括 [V8 官方网站][47],而且它们随着时间的推移而变化,往往是很大的变化。

V8 一直在发展,就像周围的其他 JavaScript 引擎一样,以加快网络和 Node.js 生态系统的发展。

在网络上,有一场多年来一直在进行的性能竞赛,我们(作为用户和开发者)从这场竞争中获益良多,因为我们年复一年地得到更快和更优化的机器。

编译(Compilation)

一般来说,JavaScript 被认为是一种解释语言,但现代的 JavaScript 引擎不再只是解释JavaScript,而是对其进行编译。

这发生在 2009 年,当时 SpiderMonkey JavaScript 编译器被添加到 Firefox 3.5 中,每个人都遵循这个想法。

JavaScript 由 V8 内部编译,采用实时制(JIT)编译,以加快执行速度。

这可能看起来违反直觉,。但自从 2004 年谷歌地图问世以来,JavaScript 已经从一般执行几十行代码的语言发展到在浏览器中运行的几千到几十万行的完整应用程序。

我们的应用程序现在可以在浏览器中运行数小时,而不仅仅是一些表单验证规则或简单的脚本。

在这个新世界里,编译 JavaScript 是非常有意义的,因为虽然可能需要多花一点时间来让 JavaScript就绪,但一旦完成,它的性能就会比纯粹的解释代码高得多。

如何退出 Node.js 程序

终止一个 Node.js 应用程序有多种方法。

在控制台中运行程序时,你可以用 ctrl-C 关闭它,但我想在这里讨论的是以编程方式退出。

让我们从最激烈的一个开始,看看为什么你最好使用它。

process 核心模块提供了一个方便的方法,允许你以编程方式退出 Node.js 程序:process.exit()

当 Node.js 运行这一行时,进程会立即被强制终止。

这意味着任何正在等待的回调,任何仍在发送的网络请求,任何文件系统访问,或写到 stdoutstderr 的进程--都将立即被强制地终止。

如果这对你来说是好的,你可以传递一个整数,向操作系统发出退出代码的信号:

process.exit(1)

默认情况下,退出代码是`0',这意味着成功。不同的退出代码有不同的含义,你可能想在自己的系统中使用这些代码,让程序与其他程序沟通。

你可以阅读更多关于退出代码的内容 [这里][48]。

你也可以设置 process.exitCode 属性:

process.exitCode = 1

而当程序后来结束时,Node.js 将返回该退出代码。

当所有的处理完成后,一个程序会优雅地退出。

很多时候,我们用 Node.js 启动服务器,比如这个 HTTP 服务器:

const express = require('express')
const app = express()
app.get('/', (req, res) => {  res.send('Hi!')})
app.listen(3000, () => console.log('Server ready'))

这个程序永远不会结束。如果你调用 process.exit(),任何正在等待或运行的请求都会被终止。这是不好的

在这种情况下,你需要给命令发送一个 `SIGTERM' 信号,并通过进程信号处理程序来处理:

注意: process 不需要 require,它是自带的。

const express = require('express')
const app = express()

app.get('/', (req, res) => {  res.send('Hi!')})

app.listen(3000, () => console.log('Server ready'))

process.on('SIGTERM', () => {  
  app.close(() => {
      console.log('Process terminated')  
  })
})

什么是信号?信号是一种便携式操作系统接口(POSIX)的互通系统:为了通知一个进程发生的事件而向其发送的通知。

SIGKILL 是告诉一个进程立即终止的信号,最好是像 process.exit() 那样。

SIGTERM 是告诉一个进程优雅地终止的信号。它是进程管理器发出的信号,如 upstartsupervisord 和其他的。

你可以从程序内部,在另一个函数中发送这个信号:

process.kill(process.pid, 'SIGTERM')

或者从另一个正在运行的 Node.js 程序,或者在你的系统中运行的任何其他应用程序,知道你想终止的进程的 PID。

如何从 Node.js 读取环境变量

Node 的 process 核心模块提供了 env 属性,它承载了所有在进程启动时设置的环境变量。

下面是一个访问 NODE_ENV 环境变量的例子,该变量默认设置为 development

process.env.NODE_ENV // "development"

在脚本运行前将其设置为 production 将告诉 Node.js,这是一个生产环境。

以同样的方式,你可以访问你设置的任何自定义环境变量。

在哪里托管一个 Node.js 应用程序

一个 Node.js 应用程序可以被托管在很多地方,这取决于你的需求。

下面是一个非详尽的清单,当你想部署你的应用程序并使其可以公开访问时,你可以探索这些选项。

我将列出从最简单和受限制到更复杂和强大的选项。

最简单的选择:本地隧道

即使你有一个动态 IP,或者你在一个 NAT 下,你也可以部署你的应用程序,并使用本地隧道从你的计算机上提供请求。

这个选项适合于一些快速测试、演示产品或与一小部分人分享应用程序。

一个在所有平台上都可用的非常好的工具是 [ngrok][49]。

使用它,你只需输入 ngrok PORT,你想要的端口就会暴露在互联网上。你会得到一个 ngrok.io 的域名,但如果付费订阅,你可以得到一个自定义的 URL 以及更多的安全选项(记住,你的机器是向公共互联网开放的)。

你可以使用的另一项服务是 [localtunnel][50]。

零配置部署

Glitch

[Glitch][51] 是一个 playground,是一种比以往任何时候都更快地建立你的应用程序的方式,并看到它们在自己的 glitch.com 子域上运行。你目前不能拥有一个自定义域名,而且还有一些 [限制][52],但它真的很适合做原型。它看起来很有趣(这是个优点),而且它不是一个傻瓜式的环境--你得到了 Node.js 的所有功能,一个 CDN,安全的证书存储,GitHub 导入/导出和更多。

由 FogBugz 和 Trello 背后的公司(以及 Stack Overflow 的共同创建者)提供。

我经常使用它来做演示。

Codepen

[Codepen][53] 是一个了不起的平台和社区。你可以创建一个有多个文件的项目,并以自定义域名进行部署。

Serverless

无服务器(Serverless)是一种发布应用的方式,而且完全没有服务器需要管理。无服务器是一种范式,你把你的应用发布为功能,它们在网络端点上做出响应(也叫 FAAS--功能即服务)。

非常受欢迎的解决方案有:

  • [Serverless Framework][54]
  • [Standard Library][55]

它们都为在 AWS Lambda 和其他基于 Azure 或谷歌云的 FAAS 解决方案上发布,提供了一个抽象层。

PAAS

PAAS 是 Platform As A Service 的缩写。这些平台解决了很多你在部署应用时应该担心的事情。

Zeit Now

[Zeit][56] 是一个有趣的选择。你只需在终端输入 now,它就会负责部署你的应用程序。有一个有限制的免费版本,而付费版本则更强大。你只需忘记有一个服务器,你只需部署应用程序。

Nanobox

[Nanobox][57]

Heroku

[Heroku][58] 是一个神奇的平台。

这是一篇好文章,[在 Heroku 上开始使用 Node.js][59].

Microsoft Azure

[Azure][60] 是微软的云产品。

查看 [在 Azure 中创建一个 Node.js Web 应用][61].

Google Cloud Platform

[Google Cloud][62] 是你的应用程序的一个了不起的结构。

他们有一个很好的[Node.js 文档部分][63].

Virtual Private Server(虚拟私有服务器)

在本节中,你会找到常见的选择,从友好到更不友好的顺序排列:

  • [Digital Ocean][64]
  • [Linode][65]
  • [Amazon Web Services][66], 我特别提到 Amazon Elastic Beanstalk,因为它抽象了一点 AWS 的复杂性。

因为他们提供了一个空的 Linux 机器,你可以在上面工作,所以这些没有具体的教程。

在 VPS 类别中还有很多选择,这些只是我使用的和我推荐的。

Bare metal(裸金属)

另一个解决方案是获得一个 [裸机金属服务器][67],安装一个 Linux 发行版,把它连接到互联网上(或者每月租一个,比如你可以使用 [虚拟裸金属][68]服务)。

如何使用 Node.js REPL

REPL 是 Read-Evaluate-Print-Loop 的缩写,它是一种快速探索 Node.js 功能的好方法。

node 命令是我们用来运行 Node.js 脚本的命令:

node script.js

如果我们省略文件名,我们在 REPL 模式下使用它:

node

如果你现在在你的终端尝试一下,会发生这样的情况:

node

该命令保持在空闲模式,等待我们输入什么。

提示:如果你不确定如何打开你的终端,谷歌 “How to open terminal on ”。

REPL 正在等待我们输入一些 JavaScript 代码。

从简单的开始,然后按下 enter 键:

> console.log('test')
> test
> undefined

第一个值 test,是我们告诉控制台要打印的输出,然后我们得到 undefined,这是运行 console.log() 的返回值。

现在我们可以输入一行新的 JavaScript 了。

通过使用 tab 键完成自动补全

REPL 最酷的地方是它是互动的。

当你写代码时,如果你按下 tab 键,REPL 将尝试自动完成你写的内容,以匹配你已经定义的变量或预定义的变量。

探索 JavaScript 对象

试着输入一个 JavaScript 类的名称,如 Number,加一个点,然后按 tab

REPL将打印出你可以访问该类的所有属性和方法:

探索全局对象(global objects)

你可以通过输入 "global. "并按 "tab "来检查你可以访问的 globals 对象:

The _ special variable (特殊变量)

如果在一些代码之后,你输入_,这将会打印出最后一个操作的结果。

点命令(Dot commands)

REPL 有一些特殊的命令,都以点.开头。它们是

  • .help: 显示点命令的帮助。
  • .editor: 启用更多的编辑器,可以轻松地编写多行 JavaScript 代码。一旦你进入这个模式,输入 ctrl-D 就可以运行你写的代码。
  • .break: 当输入一个多行表达式时,输入.break 命令将中止继续输入。与按下 ctrl-C 相同。
  • .clear: 将 REPL 上下文重置为空对象,并清除当前正在输入的任何多行表达式。
  • .load: 加载一个 JavaScript 文件,相对于当前工作目录。
  • .save: 将你在 REPL 会话中输入的所有内容保存到一个文件(指定文件名)
  • .exit: 退出 repl(与按两次 ctrl-C 相同)

REPL 知道你什么时候在输入一个多行语句,而不需要调用.editor

例如,如果你开始键入一个迭代,像这样:

[1, 2, 3].forEach(num =>; {

并按下 "enter",REPL 将转到一个以 3 个点开始的新行,表明你现在可以继续在该块上工作。

... console.log(num)... })

如果你在一行的结尾处输入 .break,多行模式将停止,语句不会被执行。

Node.js,接受来自命令行的参数

如何在 Node.js 程序中接受从命令行传递的参数

在调用 Node.js 程序时,你可以使用以下方式传递任意数量的参数:

node app.js

参数可以是独立的,也可以有一个键和一个值。

例如:

node app.js flavio

或者

node app.js name=flavio

这改变了你在 Node.js 代码中检索该值的方式。

你检索它的方式是使用 Node.js 内置的 process 对象。

它暴露了一个 argv 属性,这是一个包含所有命令行调用参数的数组。

第一个参数是 node 命令的完整路径。

第二个元素是正在执行的文件的完整路径。

所有的附加参数都是从第三个位置开始往前出现。

你可以用一个循环遍历所有的参数(包括节点路径和文件路径):

process.argv.forEach((val, index) => {  
  console.log(`${index}: ${val}`)
})

你可以通过创建一个排除前两个参数的新数组,只获得额外的参数:

const args = process.argv.slice(2)

如果你有一个没有索引名称的参数,像这样:

node app.js flavio

你可以通过以下方式访问它

const args = process.argv.slice(2)
args[0]

在这种情况下:

node app.js name=flavio

args[0] 的值是 name=flavio,而你需要对它进行解析。最好的方法是使用 [minimist][69],它有助于处理参数:

const args = require('minimist')(process.argv.slice(2))
args['name'] //flavio

使用 Node.js 输出到命令行

如何使用 Node.js 打印到命令行控制台,从基本的 console.log 到更复杂的情况。

使用控制台模块的基本输出

Node.js 提供了一个 [console 模块][70],它提供了大量非常有用的方法来与命令行进行交互。

它基本上与你在浏览器中找到的 console 对象相同。

最基本和最常用的方法是 console.log(),它将你传递给它的字符串打印到控制台。

如果你传递一个对象,它将把它渲染成一个字符串。

你可以向 console.log 传递多个变量,例如:

const x = 'x'
const y = 'y'
console.log(x, y)

而 Node.js 会同时打印。

我们还可以通过传递变量和格式指定器来格式化短语,让它看起来更漂亮。

例如:

console.log('My %s has %d years', 'cat', 2)
  • %s 格式化变量为字符串
  • %d 或者 %i 格式化变量为整数
  • %f 格式化一个变量为浮点数
  • %O 用于打印一个对象的表示方法

比如:

console.log('%O', Number)

清空控制台

console.clear() 清空控制台(其行为可能取决于所使用的控制台)

元素统计

console.count() 是一个很方便的方法。

以此代码为例:

const x = 1
const y = 2
const z = 3 
console.count(  'The value of x is ' + x + ' and has been checked .. how many times?')
console.count(  'The value of x is ' + x + ' and has been checked .. how many times?')
console.count(  'The value of y is ' + y + ' and has been checked .. how many times?')

发生的情况是,count 将计算一个字符串被打印的次数,并在其旁边打印出计数。

你可以只统计 apples 和 oranges:

const oranges = ['orange', 'orange']
const apples = ['just one apple'] 
oranges.forEach(fruit => {console.count(fruit)}) 
apples.forEach(fruit => {console.count(fruit)})

打印堆栈跟踪

在有些情况下,打印一个函数的调用堆栈跟踪是很有用的,也许是为了回答这个问题。"你是如何到达代码的这一部分的?”

你可以使用 console.trace():

const function2 = () => console.trace()
const function1 = () => function2()
function1()

这将打印出堆栈跟踪。如果我在 Node REPL 中尝试这样做,会打印出以下内容:

Trace    
at function2 (repl:1:33)    
at function1 (repl:1:25)    
at repl:1:1    
at ContextifyScript.Script.runInThisContext (vm.js:44:33)    
at REPLServer.defaultEval (repl.js:239:29)    
at bound (domain.js:301:14)    
at REPLServer.runBound [as eval] (domain.js:314:12)    
at REPLServer.onLine (repl.js:440:10)    
at emitOne (events.js:120:20)    
at REPLServer.emit (events.js:210:7)

计算花费的时间

你可以使用 time()timeEnd() 轻松计算出一个函数的运行时间。

const doSomething = () => console.log('test')
const measureDoingSomething = () => {
    console.time('doSomething()')     //do something, and measure the time it takes doSomething() 
    doSomething()
    console.timeEnd('doSomething()')
}

measureDoingSomething()

stdout 和 stderr

正如我们所看到的,console.log 很适合在控制台中打印信息。这就是所谓的标准输出(stdout)。

console.error 打印到 stderr 流(stream)。

它不会出现在控制台,但会出现在错误日志中。

输出颜色

你可以通过使用转义序列给控制台中的文本输出着色。转义序列是一组标识颜色的字符。

Example:

console.log('\x1b[33m%s\x1b[0m', 'hi!')

你可以在 Node REPL 中尝试这样做,它将以黄色打印 hi!

然而,这只是低级别的方法。为控制台输出着色的最简单方法是使用一个库。[Chalk][71] 就是这样一个库,除了着色之外,它还可以帮助其他的样式设计,比如将文本加粗、斜体或下划线。

你用 npm install chalk 安装它,然后你就可以使用它:

const chalk = require('chalk')
console.log(chalk.yellow('hi!'))

使用 chalk.yellow 比记住转义代码要方便得多,而且代码的可读性也很强。

查看我在上面发布的 Chalk 项目链接,了解更多的使用实例。

创建一个进度条

[Progress][72] 是一个很棒的软件包,可以在控制台中创建一个进度条。使用 npm install progress 来安装它。

这个片段创建了一个 10 步的进度条,每 100 毫秒完成一步。当进度条完成后,我们会清除间隔时间:

const ProgressBar = require('progress')
const bar = new ProgressBar(':bar', { total: 10 })
const timer = setInterval(() => {
    bar.tick()
    if (bar.complete) { 
      clearInterval(timer) 
    }
}, 100)

在 Node.js 中接受来自命令行的输入

如何使 Node.js CLI 程序具有交互性?

Node 从第 7 版开始就提供了 [readline 模块][73] 来执行这个任务:从一个可读流中获取输入,比如 process.stdin 流,在 Node 程序的执行过程中,它就是终端输入,一次一个行。

const readline = require('readline')
                .createInterface({
                  input: process.stdin, output: process.stdout 
                })
readline.question(`What's your name?\n`, (name) => {
    console.log(`Hi ${name}!`)
    readline.close()
})

这段代码询问用户名,一旦输入文本并且用户按了回车键,我们就会发送一个问候语。

question() 方法显示第一个参数(一个问题)并等待用户输入。一旦回车键被按下,它就会调用回调函数。

在这个回调函数中,我们关闭 readline 接口。

readline 提供了其他几个方法,我将让你在我上面 [链接][73] 文档中查看它们。

如果你需要要求一个密码,现在最好是回显它,而是显示一个*符号。

最简单的方法是使用 [readline-sync][74],它在 API 方面非常相似,可以开箱即用。

一个更完整和抽象的解决方案是由 [Inquirer.js][75] 提供。

你可以用 npm install inquirer 来安装它,然后你可以像这样复制上面的代码:

const inquirer = require('inquirer')
const questions = [{ type: 'input', name: 'name', message: "What's your name?", }]
inquirer.prompt(questions).then(answers => { 
    console.log(`Hi ${answers['name']}!`) 
})

Inquirer.js 可以让你做很多事情,比如问多个选择,有单选按钮,确认选项等等。

值得了解所有的替代方案,特别是 Node.js 提供的内置方案,但如果你打算将 CLI 输入提高到一个新的水平,Inquirer.js 是一个最佳选择。

使用 exports 从 Node.js 文件中暴露功能(Expose functionality)

如何使用 module.exports API 将数据暴露给你的应用程序中的其他文件,或者也暴露给其他应用程序。

Node.js 有一个内置的模块系统。

一个 Node.js 文件可以导入其他 Node.js 文件所暴露的功能。

当你想导入一些东西时,你可以使用:

const library = require('./library')

导入驻扎在当前文件夹中的 library.js 文件中所暴露的功能。

在这个文件中,功能必须在被其他文件导入之前被公开。

文件中定义的任何其他对象或变量默认为私有,不向外界公开。

这就是 [module 系统][76]提供的 module.exports API所允许我们做的。

当你把一个对象或一个函数指定为新的 exports 属性时,这就是被暴露的东西。因此,它可以被导入到你的应用程序的其他部分,或者其他应用程序中。

你可以通过 2 种方式做到这一点。

首先是给 module.exports 指定一个对象,这是一个由模块系统提供的开箱即用的对象,这将使你的文件只导出那个对象:

const car = {  brand: 'Ford',  model: 'Fiesta'}
module.exports = car
//..in the other file
const car = require('./car')

第二种方式是将导出的对象作为 exports 的一个属性。这种方式允许你导出多个对象、函数或数据:

const car = {  brand: 'Ford',  model: 'Fiesta'}
exports.car = car

或直接

exports.car = {  brand: 'Ford',  model: 'Fiesta'}

而在另一个文件中,你将通过引用你导入的一个属性来使用它:

const items = require('./items')items.car

或者

const car = require('./items').car

module.exportsexports 之间有什么区别?

前者暴露了 它所指向的对象。后者暴露了它所指向的对象的 属性

npm 简介

npmnode 软件包管理器

2017 年 1 月,超过 35 万个软件包被报告列在 npm registry 中,使其成为地球上最大的单一语言代码库,你可以肯定有一个软件包用于(几乎!)一切。

它开始时是一种下载和管理 Node.js 包的依赖关系的方式,但后来它也成为了一个用于前端 JavaScript 的工具。

npm 做了很多事情。

下载

npm 管理你项目的依赖项下载。

安装所有的依赖项

如果一个项目有一个 packages.json 文件,通过运行

npm install

它将在 node_modules 文件夹中安装项目所需的一切,如果它不存在,则创建它。

安装单个软件包

你也可以通过运行以下命令来安装一个特定的软件包

npm install <package-name>

通常你会看到在这个命令中加入更多的标志(flags):

  • --save 安装并添加条目到 package.json 文件中 dependencies
  • --save-dev 安装并添加条目到 package.json 文件 devDependencies 中。

区别主要在于,devDependencies 通常是开发工具,如测试库,而 dependencies 是与生产中的应用捆绑在一起的。

更新软件包

更新也很容易,通过运行

npm update

npm 将检查所有软件包是否有满足你的版本约束的较新版本。

你可以指定一个单独的软件包来更新:

npm update <package-name>

版本管理

除了普通的下载,npm 还管理版本,所以你可以指定一个包的任何特定版本,或者要求比你所需要的版本高或低。

很多时候,你会发现一个库只与另一个库的主要版本兼容。

或者一个库的最新版本中的一个 bug,仍未被修复,导致了一个问题。

指定一个库的明确版本也有助于让每个人都处于同一个确切的软件包版本上,这样整个团队就会运行同一个版本,直到 package.json 文件被更新。

在所有这些情况下,版本管理都有很大帮助,npm 遵循语义版本管理(semver)标准。

运行任务

package.json 文件支持一种指定命令行任务的格式,可以通过使用

npm <task-name>

例如:

{
    "scripts": {
        "start-dev": "node lib/server-development",
        "start": "node lib/server-production"
    }
}

使用这个功能来运行 Webpack 是非常普遍的:

{
    "scripts": {
        "watch": "webpack --watch --progress --colors --config webpack.conf.js",
        "dev": "webpack --progress --colors --config webpack.conf.js",
        "prod": "NODE_ENV=production webpack -p --config webpack.conf.js",
    }
}

因此,与其输入那些容易忘记或打错的长命令,你可以运行

npm watch 
npm dev 
npm prod

npm 在哪里安装软件包

当你使用npm(或 [yarn][77])安装一个软件包时,你可以执行 2 种类型的安装:

  • a local install (本地安装)
  • a global install (全局安装)

默认情况下,当你输入一个 "npm install "命令时,例如:

npm install lodash

包被安装在当前文件树下的 node_modules 子文件夹中。

在这种情况下,npm 也会在当前文件夹中的 package.json 文件的 dependencies 属性中添加 lodash 条目。

使用 -g 标志进行全局安装:

npm install -g lodash

当这种情况发生时,npm 不会将软件包安装在本地文件夹下,而是会使用一个全局位置。

具体在哪里?

npm root -g 命令将告诉你该位置在你的机器上的确切位置。

在 MacOS 或 Linux 上,这个位置可以是 /usr/local/lib/node_modules。 在 Windows 上应该是C:\Users\YOU\AppData\Roaming\npm\node_modules

然而,如果你使用 nvm 来管理 Node.js 的版本,这个位置会有所不同。

例如,我使用 nvm,我的软件包位置显示为 /Users/flavio/.nvm/versions/node/v8.9.0/lib/node_modules

如何使用或执行一个用 npm 安装的软件包

如何在你的代码中包含并使用安装在 node_modules 文件夹中的软件包

当你使用 npm 安装一个包到你的 node_modules 文件夹,或者全局安装,你如何在你的 Node 代码中使用它?

假设你安装了 lodash,一个流行的 JavaScript 工具库,使用

npm install lodash

这将在本地的 node_modules 文件夹中安装该软件包

要在你的代码中使用它,你只需要用 require 将它导入你的程序:

const _ = require('lodash')

如果你的软件包是一个可执行文件呢?

在这种情况下,它将把可执行文件放在 node_modules/.bin/ 文件夹下。

一个简单的演示, 使用 [cowsay][78]。

cowsay 软件包提供了一个命令行程序,执行该程序可以让一头牛说些什么(也可以是其他动物)。

当你使用 npm install cowsay 来安装这个包时,它将自己和一些依赖项安装在 node_modules 文件夹:

有一个隐藏的.bin 文件夹,它包含 cowsay 二进制文件的符号链接。

你如何执行这些?

你当然可以输入 ./node_modules/.bin/cowsay 来运行它,它也可以工作,但是 [npx][79],包括在最近版本的 npm 中(从 5.2 开始),是一个更好的选择。你只需运行:

npx cowsay

而 npx 会找到软件包的位置。

package.json 指南

package.json 文件是很多基于 Node.js 生态系统的应用代码库中的一个关键元素。

如果你用 JavaScript 工作,或者你曾经与一个 JavaScript 项目、Node.js 或前端项目互动,你肯定见过 package.json 文件。

那是做什么用的?你应该知道些什么,你可以用它做哪些很酷的事情?

package.json 文件是你项目的清单。它可以做很多事情,完全不相关。例如,它是一个工具配置的中央仓库。它也是 [npm][80] 和 [yarn][81] 存储它所安装的软件包的名称和版本的地方。

文件结构

下面是一个 package.json 文件的例子:

{}

它是空的! 对于一个应用程序来说,package.json 文件中应该包含什么并没有固定的要求。唯一的要求是遵循 JSON 格式,否则它不能被试图以编程方式访问其属性的程序读取。

如果你正在构建一个 Node.js 包,你想通过 npm 发布,事情就会发生根本性的变化,你必须有一套属性来帮助其他人使用它。我们将在后面看到更多关于这方面的内容。

这是另一个 package.json:

{  "name": "test-project"}

它定义了一个 name 属性,它告诉了这个文件所在的同一文件夹中包含的应用程序或包的名称。

这里有一个更复杂的例子,我从一个 Vue.js 应用样本中提取了这个例子:

{
    "name": "test-project",
    "version": "1.0.0",
    "description": "A Vue.js project",
    "main": "src/main.js",
    "private": true,
    "scripts": {
        "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
        "start": "npm run dev",
        "unit": "jest --config test/unit/jest.conf.js --coverage",
        "test": "npm run unit",
        "lint": "eslint --ext .js,.vue src test/unit",
        "build": "node build/build.js"
    },
    "dependencies": {
        "vue": "^2.5.2"
    },
    "devDependencies": {
        "autoprefixer": "^7.1.2",
        "babel-core": "^6.22.1",
        "babel-eslint": "^8.2.1",
        "babel-helper-vue-jsx-merge-props": "^2.0.3",
        "babel-jest": "^21.0.2",
        "babel-loader": "^7.1.1",
        "babel-plugin-dynamic-import-node": "^1.2.0",
        "babel-plugin-syntax-jsx": "^6.18.0",
        "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
        "babel-plugin-transform-runtime": "^6.22.0",
        "babel-plugin-transform-vue-jsx": "^3.5.0",
        "babel-preset-env": "^1.3.2",
        "babel-preset-stage-2": "^6.22.0",
        "chalk": "^2.0.1",
        "copy-webpack-plugin": "^4.0.1",
        "css-loader": "^0.28.0",
        "eslint": "^4.15.0",
        "eslint-config-airbnb-base": "^11.3.0",
        "eslint-friendly-formatter": "^3.0.0",
        "eslint-import-resolver-webpack": "^0.8.3",
        "eslint-loader": "^1.7.1",
        "eslint-plugin-import": "^2.7.0",
        "eslint-plugin-vue": "^4.0.0",
        "extract-text-webpack-plugin": "^3.0.0",
        "file-loader": "^1.1.4",
        "friendly-errors-webpack-plugin": "^1.6.1",
        "html-webpack-plugin": "^2.30.1",
        "jest": "^22.0.4",
        "jest-serializer-vue": "^0.3.0",
        "node-notifier": "^5.1.2",
        "optimize-css-assets-webpack-plugin": "^3.2.0",
        "ora": "^1.2.0",
        "portfinder": "^1.0.13",
        "postcss-import": "^11.0.0",
        "postcss-loader": "^2.0.8",
        "postcss-url": "^7.2.1",
        "rimraf": "^2.6.0",
        "semver": "^5.3.0",
        "shelljs": "^0.7.6",
        "uglifyjs-webpack-plugin": "^1.1.1",
        "url-loader": "^0.5.8",
        "vue-jest": "^1.0.2",
        "vue-loader": "^13.3.0",
        "vue-style-loader": "^3.0.1",
        "vue-template-compiler": "^2.5.2",
        "webpack": "^3.6.0",
        "webpack-bundle-analyzer": "^2.9.0",
        "webpack-dev-server": "^2.9.1",
        "webpack-merge": "^4.1.0"
    },
    "engines": {
        "node": ">= 6.0.0",
        "npm": ">= 3.0.0"
    },
    "browserslist": ["> 1%", "last 2 versions", "not ie &lt;= 8"]
}

这里有很多的事情要做:

  • name 设置应用程序/包的名称
  • version 设置应用程序/包的名称
  • description 是对应用程序/包的简要描述
  • main 设置应用程序的入口点
  • private 如果设置为 true 可以防止应用程序/软件包被意外地发布到 npm
  • scripts 定义了一组你可以运行的 node 脚本
  • dependencies 设置一个作为依赖项安装的 npm 包的列表
  • devDependencies 设置一个作为开发依赖的 npm 包的列表
  • engines 设置该软件包/应用程序适用于哪些版本的 Node
  • browserslist 用于告诉你要支持哪些浏览器(以及它们的版本)

所有这些属性都被 npm 或其他我们可以使用的工具所使用。

属性分类

本节详细描述了你可以使用的属性。我指的是 包(package),但同样的事情也适用于你不作为包使用的本地应用程序。

这些属性大多只在 npm[网站][82] 上使用,其他的由与你的代码交互的脚本使用,如 npm 或其他。

name

设置软件包的名称。

例如:

{"name": "test-project"}

该名称必须少于 214 个字符,不能有空格,只能包含小写字母、连字符(-)或下划线(_)。

这是因为当一个软件包在 npm 上发布时,它会根据这个属性获得自己的 URL。

author

列出软件包作者的名字

例如:

{
    "author": "Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)"
}

也可与此格式一起使用:

{
    "author": {
        "name": "Flavio Copes",
        "email": "flavio@flaviocopes.com",
        "url": "https://flaviocopes.com"
    }
}

contributors

除了作者之外,该项目还可以有一个或多个贡献者。这个属性是一个数组,列出他们。

例如:

{
    "contributors": ["Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)"]
}

也可与此格式一起使用:

{
    "contributors": [{
        "name": "Flavio Copes",
        "email": "flavio@flaviocopes.com",
        "url": "https://flaviocopes.com"
    }]
}

bugs

链接到软件包的 issues 跟踪器,很可能是 GitHub issues 页面

比如:

{  "bugs": "https://github.com/flaviocopes/package/issues"}

homepage

设置软件包的主页

例子:

{  "homepage": "https://flaviocopes.com/package"}

version

表示软件包的当前版本。

例子:

{"version": "1.0.0"}

这个属性遵循版本的语义版本(semver)符号,这意味着版本总是用 3 个数字表示。x.x.x

第一个数字是主要版本,第二个是次要版本,第三个是补丁版本。

这些数字是有意义的:一个只修复 bug 的版本是补丁版本,一个引入了向后兼容的变化的版本是次要版本,一个主要版本可以有突破性的变化。

license

表示该软件包的许可证。

例如:

{"license": "MIT"}

keywords

这个属性包含一个与你的包所做的事情相关联的关键词。

例如:

{"keywords": [  "email",  "machine learning",  "ai"]}

这有助于人们在浏览类似的软件包或浏览 npm 官网时找到你的软件包。

description

这个属性包含了对软件包的简要描述.

例如:

{"description": "A package to work with strings"}

如果你决定将你的软件包发布到npm上,这样人们就能发现软件包的内容,这就特别有用。

repository

这个属性指定了这个软件包仓库的位置。

例如:

{"repository": "github:flaviocopes/testing"}

注意github前缀。 还有其他受欢迎的服务::

{"repository": "gitlab:flaviocopes/testing"}
{"repository": "bitbucket:flaviocopes/testing"}

你可以明确地设置所使用的版本控制系统:

{"repository": {  "type": "git",  "url": "https://github.com/flaviocopes/testing.git"}}

你可以使用不同的版本控制系统:

{"repository": {  "type": "svn",  "url": "..."}}

main

设置包的入口点。

当你在一个应用程序中导入这个包时,应用程序就会在这里搜索模块的出口。

例如:

{"main": "src/main.js"}

private

如果设置为 "true",可以防止应用程序/软件包被意外地发布在 "npm" 上

例如:

{"private": true }

scripts

定义了一组你可以运行的 node 脚本

例如:

{
  "scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "unit": "jest --config test/unit/jest.conf.js --coverage",
    "test": "npm run unit",
    "lint": "eslint --ext .js,.vue src test/unit",
    "build": "node build/build.js"
  }
}

这些脚本是命令行应用程序。你可以通过调用 npm run XXXXyarn XXXX 来运行它们,其中 XXXX 是命令名称。

例如:
npm run dev

你可以为命令使用任何你想要的名字,而且脚本可以做任何你想要的事情。

dependencies

设置一个作为依赖项安装的 npm 包的列表。

当你使用 npm 或 yarn 安装一个包时:

npm install <PACKAGENAME>
yarn add <PACKAGENAME>

该软件包会自动插入该列表中。

例如:

 {"dependencies": {  "vue": "^2.5.2"}}

devDependencies

设置一个作为开发依赖的 npm 包的列表。

它们与 dependencies 不同,因为它们只能安装在开发机器上,不需要在生产中运行代码。

当你使用 npmyarn 安装一个包时:

npm install --dev <PACKAGENAME>
yarn add --dev <PACKAGENAME>

该软件包会自动插入该列表中。

比如:

{"devDependencies": {  "autoprefixer": "^7.1.2",  "babel-core": "^6.22.1"}}

engines

设置该软件包/应用程序适用于哪些版本的 Node.js 和其他命令。

例如:

{"engines": {  "node": ">= 6.0.0",  "npm": ">= 3.0.0",  "yarn": "^0.13.0"}}

browserslist

是用来告诉你要支持哪些浏览器(以及它们的版本)。它被 Babel、Autoprefixer 和其他工具引用,只为你的目标浏览器添加所需的 polyfills(降级方案)和 fallbacks(回退方案)。

例如:

{"browserslist": [  "> 1%",  "last 2 versions",  "not ie <= 8"]}

这种配置意味着你要支持所有至少有 1%使用量的浏览器的最后两个主要版本(来自 [CanIUse.com][83]统计),但 IE8 和更低版本除外([见更多][84] 浏览器列表)。

Command-specific properties

package.json 文件也可以承载特定命令的配置,例如 Babel、ESLint 等等。

每一个都有一个特定的属性,如 eslintConfigbabel 和其他。这些都是特定的命令,你可以在各自的命令/项目文档中找到如何使用它们。

Package versions

你在上面的描述中看到了这样的版本号。~3.0.0^0.13.0。它们是什么意思,你还可以使用哪些其他的版本指定符?

该符号指定了你的软件包接受哪些更新,来自该依赖关系。

鉴于使用 semver(语义版本管理),所有的版本都有 3 位数字,第一位是主要版本,第二位是次要版本,第三位是补丁版本,你有这些规则:

  • ~: 如果你写 ~0.13.0, 你想只更新补丁版本。0.13.1可以,但0.14.0不可以。
  • ^: 如果你写 ^0.13.0, 你想更新补丁和次要版本。0.13.1, 0.14.0等等。
  • *: 如果你写 *, 这意味着你接受所有的更新,包括主要版本的升级。
  • >: 你接受比你指定的版本高的任何版本。
  • >=: 你接受任何等于或高于你指定的版本。
  • <=: 你接受任何等于或低于你指定的版本。
  • <: 你接受比你指定的版本低的任何版本。

也有其他规则:

  • no symbol:你只接受你指定的那个特定版本
  • latest:你想使用最新的可用版本

你可以将上述大部分的范围结合起来,就像这样。1.0.0 || >=1.1.0 < 1.2.0,以使用 1.0.0 或 1.1.0 以上的一个版本,但低于 1.2.0。

package-lock.json 文件

package-lock.json 文件是在安装 Nodo包时自动生成的。

在版本 5 中,NPM 引入了 package-lock.json 文件。

那是什么?你可能知道 package.json 文件,它更常见,存在的时间也更长。

该文件的目的是跟踪每一个安装的软件包的确切版本,这样,即使软件包被维护者更新,产品也能以同样的方式 100%重现。

这解决了 package.json 未解决的一个非常具体的问题。在 package.json 中,你可以使用 semver 注解来设置你想升级到哪个版本(补丁或小版本),例如:

  • 如果你写 ~0.13.0, 你想只更新补丁版本。0.13.1可以,但 0.14.0 不行。
  • 如果你写 ^0.13.0, 你想更新补丁和次要版本。0.13.1, 0.14.0 等等。
  • 如果你写 0.13.0, 这就是将被使用的确切版本,永远都是 0.13.0

你不会向 Git 提交你的 node_modules 文件夹,它通常是巨大的,当你试图通过使用 npm install 命令在另一台机器上复制项目时,如果你指定了~语法,并且一个包的补丁版本已经发布,那就会被安装。对于 ^ 和次要版本也是如此。

如果你指定了准确的版本,如例子中的 "0.13.0",你就不会受到这个问题的影响。

可能是你,也可能是另一个人在世界的另一端试图通过运行 npm install 来初始化这个项目。

所以你的原始项目和新初始化的项目实际上是不同的。即使一个补丁或小版本不应该引入破坏性的变化,我们都知道 bug 可以(所以,他们会)潜入。

package-lock.json 将你当前安装的每个软件包的版本in stone上,npm 在运行 npm install 时将使用这些确切的版本。

这个概念并不新鲜,其他编程语言的包管理器(如 PHP 中的 Composer)多年来也使用类似的系统。

package-lock.json 文件需要提交到你的 Git 仓库,如果项目是公开的或者你有合作者,或者你使用 Git 作为部署的来源,那么它可以被其他人取走。

当你运行 npm update 时,依赖的版本将在 package-lock.json 文件中更新。

一个例子

这是一个 package-lock.json 文件的结构示例,当我们在一个空的文件夹中运行 npm install cowsay 时,我们得到了这个文件:

{
    "requires": true, "lockfileVersion": 1, "dependencies": {
        "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz", "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "minimist": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "optimist": {
            "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",

                "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" }
        }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" } }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" }
    }
}

我们安装了 cowsay,它依赖于:

  • get-stdin
  • optimist
  • string-width
  • strip-eof

反过来,这些软件包需要其他的软件包,我们可以从 "requires "属性中看到,有些软件包有:

  • ansi-regex
  • is-fullwidth-code-point
  • minimist
  • wordwrap
  • strip-eof

它们按字母顺序被添加到文件中,每一个都有一个 version 字段,一个指向软件包位置的 resolved 字段,以及一个 integrity 字符串,我们可以用来验证该软件包。

查找一个 npm 包的安装版本

查看所有安装的 npm 包的最新版本,包括它们的依赖关系:

npm list

例如:

❯ npm list/Users/flavio/dev/node/cowsay
  └─┬ cowsay@1.3.1  
    ├── get-stdin@5.0.1  
    ├─┬ optimist@0.6.1  
    │ ├── minimist@0.0.10  
    │ └── wordwrap@0.0.3  
    ├─┬ string-width@2.1.1  
      ├── is-fullwidth-code-point@2.0.0  
      │ └─┬ strip-ansi@4.0.0  
      │   └── ansi-regex@3.0.0  
      └── strip-eof@1.0.0

你也可以直接打开 package-lock.json 文件,但这涉及一些用眼睛查看。

npm list -g 也是一样的,只是针对全局安装的软件包。

要想只得到你的顶层软件包(基本上是你告诉 npm 安装的和你在 package.json 中列出的那些),运行 npm list --depth=0:

❯ npm list --depth=0/Users/flavio/dev/node/cowsay
   └── cowsay@1.3.1

你可以通过指定名称来获得一个特定软件包的版本:

❯ npm list cowsay/Users/flavio/dev/node/cowsay
  └── cowsay@1.3.1

这也适用于你安装的软件包的依赖性:

❯ npm list minimist/Users/flavio/dev/node/cowsay
  └─┬ cowsay@1.3.1  
    └─┬ optimist@0.6.1    
      └── minimist@0.0.10

如果你想查看 npm 仓库中软件包的最新可用版本,运行npm view [package_name] version:

❯ npm view cowsay version
1.3.1

安装一个旧版本的 npm 包

安装一个旧版本的 npm 包可能对解决兼容性问题有帮助。

你可以使用 @ 语法来安装一个 npm 包的旧版本:

npm install <package>@<;version>

例如:

npm install cowsay

安装 1.3.1 版本(在撰写本文时)。

安装 1.2.0 版本:

npm install cowsay@1.2.0

同样的情况也可以用全局包来做:

npm install -g webpack@4.16.4

你也可能对列出一个包的所有以前的版本感兴趣。你可以用 npm view <package> versions 来做:

❯ npm view cowsay versions
[ '1.0.0',  '1.0.1',  '1.0.2',  '1.0.3',  '1.1.0',  '1.1.1',  '1.1.2',  '1.1.3',  '1.1.4',  '1.1.5',  '1.1.6',  '1.1.7',  '1.1.8',  '1.1.9',  '1.2.0',  '1.2.1',  '1.3.0',  '1.3.1' ]

将所有 Node 的依赖关系更新为最新版本

当你使用 npm install <packagename> 安装一个软件包时,该软件包的最新可用版本会被下载并放在 node_modules文件夹 中,并且在你当前文件夹中的package.jsonpackage-lock.json 文件中添加相应条目。

npm 会计算依赖关系,并安装那些最新的可用版本。

假设你安装了 [cowsay][85],一个很酷的命令行工具,可以让你让 cow(牛)说东西

当你 npm安装cowsay 时,这个条目被添加到 package.json 文件中:

{  "dependencies": {    "cowsay": "^1.3.1"  }}

这是 package-lock.json 的摘录,为了清晰起见,我把嵌套的依赖关系去掉了:

{
    "requires": true,
    "lockfileVersion": 1,
    "dependencies": {
        "cowsay": {
            "version": "1.3.1",
            "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz",
            "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==",
            "requires": {
                "get-stdin": "^5.0.1",
                "optimist": "~0.6.1",
                "string-width": "~2.1.1",
                "strip-eof": "^1.0.0"
            }
        }
    }
}

现在这 2 个文件告诉我们,我们安装了 cowsay 的1.3.1版本,而我们的更新规则是 ^1.3.1,对于 npm 的版本规则(后面会解释)意味着 npm 可以更新到补丁和小版本。0.13.10.14.0,以此类推。

如果有一个新的次要版本或补丁版本,我们输入 npm update,安装的版本就会被更新,package-lock.json 文件就会勤奋地填上新的版本。

package.json 保持不变。

为了发现新发布的软件包,你可以运行 npm outdated

下面是我很久没有更新的一个软件库中的一些过时的软件包的列表:

其中一些更新是主要版本。运行 npm update 不会更新这些版本。主要版本从不以这种方式更新,因为它们(顾名思义)会带来破坏性的变化,而 npm 想为你省去麻烦。

要将所有软件包更新到新的主要版本,请在全局安装 npm-check-updates 包:

npm install -g npm-check-updates

然后运行:

ncu -u

这将升级 package.json 文件中的所有版本提示,到 dependenciesdevDependencies,所以 npm 可以安装新的主要版本(major version)。

现在你已经准备好运行更新:

npm update

如果你刚下载的项目没有 node_modules 的依赖,你想先安装全新版本,只要运行

npm install

使用 npm 进行语义版本管理

语义版本管理(Semantic Versioning)是一种用来为版本提供意义的惯例。

如果说 Node.js 包中有什么了不起的东西,那就是所有的人都同意使用语义版本控制(Semantic Versioning)来进行版本编号。

语义版本控制的概念很简单:所有的版本都有 3 位数字。x.y.z

  • 第一个数字是主版本(major version)
  • 第二位数字是次要版本 (minor version)
  • 第三位数字是补丁版本 (patch version)

当你制作一个新的版本时,你不会随心所欲地提高一个数字,而是有规则的:

  • 当你对 API 进行不兼容的修改时,你要提高主版本的等级
  • 当你以向后兼容的方式增加功能时,你要提高次要版本的数量
  • 当你进行向后兼容的 bug 修复时,你要提高补丁版本

这个惯例在所有的编程语言中都被采用,而且每个 npm 包都要遵守这个惯例,这一点非常重要,因为整个系统都依赖于此。

为什么这么重要?

因为 npm 设置了一些规则,我们可以在 [package.json 文件][87] 中使用,以便在运行 npm update 时,选择它可以将我们的软件包更新到哪些版本。

规则使用这些符号:

  • ^
  • ~
  • =
  • >=
  • <
  • <=
  • =
  • -
  • ||

让我们看看这些规则的细节:

  • ^: 如果你写 ^0.13.0,当运行 npm update, 它可以更新到补丁(patch)和次要版本(minor version): 0.13.1, 0.14.0 等。
  • ~: 如果你写 ~0.13.0, 当运行 npm update,它可以更新到补丁(patch): 0.13.1是可以的, 但 0.14.0 不行。
  • >: 你接受比你指定的版本高的任何版本
  • >=: 你接受任何等于或高于你指定的版本
  • <=: 你接受任何等于或低于你指定的版本
  • <: 你接受比你指定的版本低的任何版本
  • =: 你接受确切的版本
  • -: 你接受一定范围的版本。例如: 2.1.0 - 2.6.2
  • ||: 你结合一组。例如: < 2.1 || > 2.6

你可以结合这些符号,例如使用 1.0.0 || >=1.1.0 <1.2.0 来表示使用 1.0.0 或 1.1.0 以上的一个版本,但低于 1.2.0。

也有其他规则:

  • no symbol:你只接受你指定的那个特定版本(1.2.1)。
  • latest:你想使用最新的可用版本

在本地或全球范围内卸载 npm 包

要卸载你以前本地安装的软件包(使用 npm install <package-name> 在 node_modules 文件夹下),请运行:

npm uninstall <package-name>

使用 -S 标志,或 --save,这个操作也将删除 [package.json 文件][89] 中的引用。

如果软件包是一个开发依赖,列在 package.json 文件的 devDependencies 中,你必须使用 -D/--save-dev 标志从文件中删除它:

npm uninstall -S <package-name>
npm uninstall -D <package-name>

如果软件包在全局范围内**安装,你需要添加 -g/--global 标志:

npm uninstall -g <package-name>

例如:

npm uninstall -g webpack

你可以在你系统的任何地方运行这个命令,因为你目前所在的文件夹并不重要。

全局或本地软件 npm 包

一个软件包最好在什么时候全局安装?以及为什么?

本地包和全局包的主要区别:

  • 本地包 安装在你运行 npm install <package-name> 的目录下,并且它们被放在这个目录下的 node_modules 文件夹中。
  • 全局包 都放在你系统中的一个地方(具体位置取决于你的设置),不管你在哪里运行 npm install -g <package-name>

在你的代码中,它们都是以同样的方式被导入:

require('package-name')

那么,你应该在什么时候以一种或另一种方式安装呢?

一般来说,所有的软件包都应该以本地包的方式安装。

这可以确保你的电脑中可以有几十个应用程序,如果需要的话,都可以运行每个软件包的不同版本。

更新全局软件包会使你的所有项目都使用新的版本,你可以想象这可能会在维护方面造成噩梦,因为一些软件包可能会破坏与其他依赖的兼容性,等等。

所有的项目都有自己的本地包,即使这看起来是一种资源的浪费,但与可能产生的负面影响相比,它是微不足道的。

当一个软件包提供了一个可执行的命令,你可以从 shell(CLI)中运行,并且在不同的项目中重复使用时,它应该被全局地安装。

你也可以在本地安装可执行命令,并使用 [npx][90] 运行它们,但有些软件包最好是全局安装。

你可能知道的流行的全局软件包的很好的例子:

  • npm
  • create-react-app
  • vue-cli
  • grunt-cli
  • mocha
  • react-native-cli
  • gatsby-cli
  • forever
  • nodemon

你的系统中可能已经有一些全局安装的软件包。你可以通过运行:

npm list -g --depth 0

在你的终端.

npm dependencies(依赖关系) 和 devDependencies(开发依赖关系)

一个包什么时候是依赖关系,什么时候是开发依赖关系?

当你使用 npm install <package-name> 安装一个 npm 包时,你是把它为一个依赖

该包会自动列在 package.json 文件的 dependencies 列表中(从 npm 5 开始:在你必须手动指定 --save 之前)。

当你添加 -D 标志,或 --save-dev 时,你就把它作为一个开发依赖项来安装,这就把它添加到 devDependencies 列表中。

开发依赖是指仅用于开发的软件包,在生产中不需要。例如,测试包、webpack 或 Babel。

当你在 生产环境 中时,如果你输入 npm install,并且该文件夹包含 package.json 文件,它们就会被安装(译者注: devDependencies 的 npm 包也会被安装),因为 npm 认为这是一个开发部署。

你需要设置 --production flag (npm install --production),以避免安装这些开发依赖项。

npx Node 包运行器

npx 是一种非常酷的运行 Node.js 代码的方式,并提供了许多有用的功能。

在本节中,我想介绍一个非常强大的命令,从 2017 年 7 月发布的 5.2 版本开始,npm 中就有这个命令,npx

如果你不想安装 npm,你可以将 npx 作为一个 [独立的包][91] 来安装。

npx 让你运行用 Node.js 构建并通过 npm 注册表发布的代码。

轻松地运行本地命令

Node.js 的开发者曾经将大多数可执行的命令作为全局包发布,以便让它们添加到系统的路径(path)中并可以执行。

这是一种痛苦,因为你无法真正安装同一命令的不同版本。

运行 npx commandname 会自动在项目的 node_modules 文件夹中找到该命令的正确引用,不需要知道确切的路径,也不需要在全局和用户的路径中安装包。

免安装的命令执行

npm 还有一个很好的功能,就是允许运行命令而不需要先安装它们。

这相当有用,主要是因为:

  1. 你不需要安装任何东西
  2. 你可以使用语法 @version 来运行同一命令的不同版本

使用 npx 的一个典型示范是通过 cowsay 命令。cowsay 将打印一头牛,说你在命令中写的东西。比如:

cowsay "Hello" 将打印出

< Hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

现在,这需要你之前从 npm 全局安装了 cowsay 命令,否则当你试图运行该命令时,你会得到一个错误。

npx 允许你在没有本地安装的情况下运行该 npm 命令:

npx cowsay "Hello"

现在,这是一个有趣的无用命令。其他情况包括:

  • 运行 vue CLI 工具来创建新的应用程序并运行它们:npx vue create my-vue-app
  • 使用 create-react-app 创建一个新的 React 应用:npx create-react-app my-react-app

以及更多。

一旦下载,下载的代码将被抹去。

使用不同的 Node.js 版本运行一些代码

使用 @ 来指定版本,并将其与 node npm 包相结合:

npx node@6 -v #v6.14.3
npx node@8 -v #v8.11.3

这有助于避免像 nvm 或其他 Node 版本管理工具的使用。

直接从 URL 中运行任意的代码片段

npx 并不仅能运行在 npm 注册表上发布的软件包。

你可以运行位于 [GitHub][92] gist 中的代码,例如:

npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

当然,在运行不受你控制的代码时,你需要小心,因为巨大的权力伴随着巨大的责任。