一杯茶的时间,上手 Node.js

Node.js 太火了,火到几乎全部前端工程师都想学,几乎全部后端工程师也想学。一说到 Node.js,咱们立刻就会想到“异步”、“事件驱动”、“非阻塞”、“性能优良”这几个特色,可是你真的理解这些词的含义吗?这篇教程将带你快速入门 Node.js,为后续的前端学习或是 Node.js 进阶打下坚实的基础。javascript

此教程属于Node.js 后端工程师学习路线的一部分,点击可查看所有内容。html

起步

什么是 Node?

简单地说,Node(或者说 Node.js,二者是等价的)是 JavaScript 的一种运行环境。在此以前,咱们知道 JavaScript 都是在浏览器中执行的,用于给网页添加各类动态效果,那么能够说浏览器也是 JavaScript 的运行环境。那么这两个运行环境有哪些差别呢?请看下图:前端

两个运行环境共同包含了 ECMAScript,也就是剥离了全部运行环境的 JavaScript 语言标准自己。如今 ECMAScript 的发展速度很是惊人,几乎可以作到每一年发展一个版本。java

提示node

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。在平常场合,这两个词是能够互换的。更多背景知识可参考阮一峰的《JavaScript语言的历史》webpack

另外一方面,浏览器端 JavaScript 还包括了:git

  • 浏览器对象模型(Browser Object Model,简称 BOM),也就是 window 对象
  • 文档对象模型(Document Object Model,简称 DOM),也就是 document 对象

而 Node.js 则是包括 V8 引擎。V8 是 Chrome 浏览器中的 JavaScript 引擎,通过多年的发展和优化,性能和安全性都已经达到了至关的高度。而 Node.js 则进一步将 V8 引擎加工成能够在任何操做系统中运行 JavaScript 的平台。程序员

预备知识

在正式开始这篇教程以前,咱们但愿你已经作好了如下准备:es6

  • 了解 JavaScript 语言的基础知识,若是有过浏览器 JS 开发经验就更好了
  • 已经安装了 Node.js,配置好了适合本身的编辑器或 IDE
  • 了解相对路径和绝对路径

学习目标

这篇教程将会让你学到:github

  • 浏览器 JavaScript 与 Node.js 的关系与区别
  • 了解 Node.js 有哪些全局对象
  • 掌握 Node.js 如何导入和导出模块,以及模块机制的原理
  • 了解如何用 Node.js 开发简单的命令行应用
  • 学会利用 npm 社区的力量解决开发中遇到的难题,避免“重复造轮子”
  • 了解 npm scripts 的基本概念和使用
  • 初步了解 Node.js 的事件机制

运行 Node 代码

运行 Node 代码一般有两种方式:1)在 REPL 中交互式输入和运行;2)将代码写入 JS 文件,并用 Node 执行。

提示

REPL 的全称是 Read Eval Print Loop(读取-执行-输出-循环),一般能够理解为交互式解释器,你能够输入任何表达式或语句,而后就会马上执行并返回结果。若是你用过 Python 的 REPL 必定会以为很熟悉。

使用 REPL 快速体验

若是你已经安装好了 Node,那么运行如下命令就能够输出 Node.js 的版本:

$ node -v
v12.10.0
复制代码

而后,咱们还能够进入 Node REPL(直接输入 node),而后输入任何合法的 JavaScript 表达式或语句:

$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> 1 + 2
3
> var x = 10;
undefined
> x + 20
30
> console.log('Hello World');
Hello World
undefined
复制代码

有些行的开头是 >,表明输入提示符,所以 > 后面的都是咱们要输入的命令,其余行则是表达式的返回值或标准输出(Standard Output,stdout)。运行的效果以下:

编写 Node 脚本

REPL 一般用来进行一些代码的试验。在搭建具体应用时,更多的仍是建立 Node 文件。咱们先建立一个最简单的 Node.js 脚本文件,叫作 timer.js,代码以下:

console.log('Hello World!');
复制代码

而后用 Node 解释器执行这个文件:

$ node timer.js
Hello World!
复制代码

看上去很是平淡无奇,可是这一行代码却凝聚了 Node.js 团队背后的心血。咱们来对比一下,在浏览器和 Node 环境中执行这行代码有什么区别:

  • 在浏览器运行 console.log 调用了 BOM,实际上执行的是 window.console.log('Hello World!')
  • Node 首先在所处的操做系统中建立一个新的进程,而后向标准输出打印了指定的字符串, 实际上执行的是 process.stdout.write('Hello World!\n')

简而言之,Node 为咱们提供了一个无需依赖浏览器、可以直接与操做系统进行交互的 JavaScript 代码运行环境!

Node 全局对象初探

若是你有过编写 JavaScript 的经验,那么你必定对全局对象不陌生。在浏览器中,咱们有 documentwindow 等全局对象;而 Node 只包含 ECMAScript 和 V8,不包含 BOM 和 DOM,所以 Node 中不存在 documentwindow;取而代之,Node 专属的全局对象是 process。在这一节中,咱们将初步探索一番 Node 全局对象。

JavaScript 全局对象的分类

在此以前,咱们先看一下 JavaScript 各个运行环境的全局对象的比较,以下图所示:

能够看到 JavaScript 全局对象能够分为四类:

  1. 浏览器专属,例如 windowalert 等等;
  2. Node 专属,例如 processBuffer__dirname__filename 等等;
  3. 浏览器和 Node 共有,可是实现方式不一样,例如 console(第一节中已提到)、setTimeoutsetInterval 等;
  4. 浏览器和 Node 共有,而且属于 ECMAScript 语言定义的一部分,例如 DateStringPromise 等;

Node 专属全局对象解析

process

process 全局对象能够说是 Node.js 的灵魂,它是管理当前 Node.js 进程状态的对象,提供了与操做系统的简单接口。

首先咱们探索一下 process 对象的重要属性。打开 Node REPL,而后咱们查看一下 process 对象的一些属性:

  • pid:进程编号
  • env:系统环境变量
  • argv:命令行执行此脚本时的输入参数
  • platform:当前操做系统的平台

提示

能够在 Node REPL 中尝试一下这些对象。像上面说的那样进入 REPL(你的输出颇有可能跟个人不同):

$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> process.pid
3
> process.platform
'darwin'
复制代码

Buffer

Buffer 全局对象让 JavaScript 也可以轻松地处理二进制数据流,结合 Node 的流接口(Stream),可以实现高效的二进制文件处理。这篇教程不会涉及 Buffer

__filename__dirname

分别表明当前所运行 Node 脚本的文件路径和所在目录路径。

警告

__filename__dirname 只能在 Node 脚本文件中使用,在 REPL 中是没有定义的。

使用 Node 全局对象

接下来咱们将在刚才写的脚本文件中使用 Node 全局对象,分别涵盖上面的三类:

  • Node 专属:process
  • 实现方式不一样的共有全局对象:consolesetTimeout
  • ECMAScript 语言定义的全局对象:Date

提示

setTimeout 用于在必定时间后执行特定的逻辑,第一个参数为时间到了以后要执行的函数(回调函数),第二个参数是等待时间。例如:

setTimeout(someFunction, 1000);
复制代码

就会在 1000 毫秒后执行 someFunction 函数。

代码以下:

setTimeout(() => {
  console.log('Hello World!');
}, 3000);

console.log('当前进程 ID', process.pid);
console.log('当前脚本路径', __filename);

const time = new Date();
console.log('当前时间', time.toLocaleString());
复制代码

运行以上脚本,在我机器上的输出以下(Hello World! 会延迟三秒输出):

$ node timer.js
当前进程 ID 7310
当前脚本路径 /Users/mRc/Tutorials/nodejs-quickstart/timer.js
当前时间 12/4/2019, 9:49:28 AM
Hello World!
复制代码

从上面的代码中也能够一瞥 Node.js 异步的魅力:在 setTimeout 等待的 3 秒内,程序并没有阻塞,而是继续向下执行,这就是 Node.js 的异步非阻塞!

提示

在实际的应用环境中,每每有不少 I/O 操做(例如网络请求、数据库查询等等)须要耗费至关多的时间,而 Node.js 可以在等待的同时继续处理新的请求,大大提升了系统的吞吐率。

在后续教程中,咱们会出一篇深刻讲解 Node.js 异步编程的教程,敬请期待!

理解 Node 模块机制

Node.js 相比以前的浏览器 JavaScript 的另外一个重点改变就是:模块机制的引入。这一节内容很长,但倒是入门 Node.js 最为关键的一步,加油吧💪!

JavaScript 的模块化之路

Eric Raymond 在《UNIX编程艺术》中定义了模块性(Modularity)的规则:

开发人员应使用经过定义明确的接口链接的简单零件来构建程序,所以问题是局部的,能够在未来的版本中替换程序的某些部分以支持新功能。 该规则旨在节省调试复杂、冗长且不可读的复杂代码的时间。

“分而治之”的思想在计算机的世界很是广泛,可是在 ES2015 标准出现之前(不了解不要紧,后面会讲到), JavaScript 语言定义自己并无模块化的机制,构建复杂应用也没有统一的接口标准。人们一般使用一系列的 <script> 标签来导入相应的模块(依赖):

<head>
  <script src="fileA.js"></script>
  <script src="fileB.js"></script>
</head>
复制代码

这种组织 JS 代码的方式有不少问题,其中最显著的包括:

  • 导入的多个 JS 文件直接做用于全局命名空间,很容易产生命名冲突
  • 导入的 JS 文件之间不能相互访问,例如 fileB.js 中没法访问 fileA.js 中的内容,很不方便
  • 导入的 <script> 没法被轻易去除或修改

人们渐渐认识到了 JavaScript 模块化机制的缺失带来的问题,因而两大模块化规范被提出:

  1. AMD(Asynchronous Module Definition)规范,在浏览器中使用较为广泛,最经典的实现包括 RequireJS
  2. CommonJS 规范,致力于为 JavaScript 生态圈提供统一的接口 API,Node.js 所实现的正是这一模块标准。

提示

ECMAScript 2015(也就是你们常说的 ES6)标准为 JavaScript 语言引入了全新的模块机制(称为 ES 模块,全称 ECMAScript Modules),并提供了 importexport 关键词,若是感兴趣可参考这篇文章。可是截止目前,Node.js 对 ES 模块的支持还处于试验阶段,所以这篇文章不会讲解、也不提倡使用。

什么是 Node 模块

在正式分析 Node 模块机制以前,咱们须要明肯定义什么是 Node 模块。一般来讲,Node 模块可分为两大类:

  • 核心模块:Node 提供的内置模块,在安装 Node 时已经被编译成二进制可执行文件
  • 文件模块:用户编写的模块,能够是本身写的,也能够是经过 npm 安装的(后面会讲到)。

其中,文件模块能够是一个单独的文件(以 .js.node.json 结尾),或者是一个目录。当这个模块是一个目录时,模块名就是目录名,有两种状况:

  1. 目录中有一个 package.json 文件,则这个 Node 模块的入口就是其中 main 字段指向的文件;
  2. 目录中有一个名为 index 的文件,扩展名为 .js.node.json,此文件则为模块入口文件。

一会儿消化不了不要紧,能够先阅读后面的内容,忘记了模块的定义能够再回过来看看哦。

Node 模块机制浅析

知道了 Node 模块的具体定义后,咱们来了解一下 Node 具体是怎样实现模块机制的。具体而言,Node 引入了三个新的全局对象(仍是 Node 专属哦):1)require;2) exports 和 3)module。下面咱们逐一讲解。

require

require 用于导入其余 Node 模块,其参数接受一个字符串表明模块的名称或路径,一般被称为模块标识符。具体有如下三种形式:

  • 直接写模块名称,一般是核心模块或第三方文件模块,例如 osexpress
  • 模块的相对路径,指向项目中其余 Node 模块,例如 ./utils
  • 模块的绝对路径(不推荐!),例如 /home/xxx/MyProject/utils

提示

在经过路径导入模块时,一般省略文件名中的 .js 后缀。

代码示例以下:

// 导入内置库或第三方模块
const os = require('os');
const express = require('express');

// 经过相对路径导入其余模块
const utils = require('./utils');

// 经过绝对路径导入其余模块
const utils = require('/home/xxx/MyProject/utils');
复制代码

你也许会好奇,经过名称导入 Node 模块的时候(例如 express),是从哪里找到这个模块的?实际上每一个模块都有个路径搜索列表 module.paths,在后面讲解 module 对象的时候就会一清二楚了。

exports

咱们已经学会了用 require 导入其余模块中的内容,那么怎么写一个 Node 模块,并导出其中内容呢?答案就是用 exports 对象。

例如咱们写一个 Node 模块 myModule.js:

// myModule.js
function add(a, b) {
  return a + b;
}

// 导出函数 add
exports.add = add;
复制代码

经过将 add 函数添加到 exports 对象中,外面的模块就能够经过如下代码使用这个函数。在 myModule.js 旁边建立一个 main.js,代码以下:

// main.js
const myModule = require('./myModule');

// 调用 myModule.js 中的 add 函数
myModule.add(1, 2);
复制代码

提示

若是你熟悉 ECMAScript 6 中的解构赋值,那么能够用更优雅的方式获取 add 函数:

const { add } = require('./myModule');
复制代码

module

经过 requireexports,咱们已经知道了如何导入、导出 Node 模块中的内容,可是你可能仍是以为 Node 模块机制有一丝丝神秘的感受。接下来,咱们将掀开这神秘的面纱,了解一下背后的主角——module 模块对象。

咱们能够在刚才的 myModule.js 文件的最后加上这一行代码:

console.log('module myModule:', module);
复制代码

在 main.js 最后加上:

console.log('module main:', module);
复制代码

运行后会打印出来这样的内容(左边是 myModule,右边是 module):

能够看到 module 对象有如下字段:

  • id:模块的惟一标识符,若是是被运行的主程序(例如 main.js)则为 .,若是是被导入的模块(例如 myModule.js)则等同于此文件名(即下面的 filename 字段)
  • pathfilename:模块所在路径和文件名,没啥好说的
  • exports:模块所导出的内容,实际上以前的 exports 对象是指向 module.exports 的引用。例如对于 myModule.js,刚才咱们导出了 add 函数,所以出如今了这个 exports 字段里面;而 main.js 没有导出任何内容,所以 exports 字段为空
  • parentchildren:用于记录模块之间的导入关系,例如 main.js 中 require 了 myModule.js,那么 main 就是 myModule 的 parent,myModule 就是 main 的 children
  • loaded:模块是否被加载,从上图中能够看出只有 children 中列出的模块才会被加载
  • paths:这个就是 Node 搜索文件模块的路径列表,Node 会从第一个路径到最后一个路径依次搜索指定的 Node 模块,找到了则导入,找不到就会报错

提示

若是你仔细观察,会发现 Node 文件模块查找路径(module.paths)的方式实际上是这样的:先找当前目录下的 node_modules,没有的话再找上一级目录的 node_modules,还没找到的话就一直向上找,直到根目录下的 node_modules。

深刻理解 module.exports

以前咱们提到,exports 对象本质上是 module.exports 的引用。也就是说,下面两行代码是等价的:

// 导出 add 函数
exports.add = add;

// 和上面一行代码是同样的
module.exports.add = add;
复制代码

实际上还有第二种导出方式,直接把 add 函数赋给 module.exports 对象:

module.exports = add;
复制代码

这样写和第一种导出方式有什么区别呢?第一种方式,在 exports 对象上添加一个属性名为 add,该属性的值为 add 函数;第二种方式,直接令 exports 对象为 add 函数。可能有点绕,可是请必定要理解这二者的重大区别!

require 时,二者的区别就很明显了:

// 第一种导出方式,须要访问 add 属性获取到 add 函数
const myModule = require('myModule');
myModule.add(1, 2);

// 第二种导出方式,能够直接使用 add 函数
const add = require('myModule');
add(1, 2);
复制代码

警告

直接写 exports = add; 没法导出 add 函数,由于 exports 本质上是指向 moduleexports 属性的引用,直接对 exports 赋值只会改变 exports,对 module.exports 没有影响。若是你以为难以理解,那咱们用 appleprice 类比 moduleexports

apple = { price: 1 };   // 想象 apple 就是 module
price = apple.price;    // 想象 price 就是 exports
apple.price = 3;        // 改变了 apple.price
price = 3;              // 只改变了 price,没有改变 apple.price
复制代码

咱们只能经过 apple.price = 1 设置 price 属性,而直接对 price 赋值并不能修改 apple.price

重构 timer 脚本

在聊了这么多关于 Node 模块机制的内容后,是时候回到咱们以前的定时器脚本 timer.js 了。咱们首先建立一个新的 Node 模块 info.js,用于打印系统信息,代码以下:

const os = require('os');

function printProgramInfo() {
  console.log('当前用户', os.userInfo().username);
  console.log('当前进程 ID', process.pid);
  console.log('当前脚本路径', __filename);
}

module.exports = printProgramInfo;
复制代码

这里咱们导入了 Node 内置模块 os,并经过 os.userInfo() 查询到了系统用户名,接着经过 module.exports 导出了 printProgramInfo 函数。

而后建立第二个 Node 模块 datetime.js,用于返回当前的时间,代码以下:

function getCurrentTime() {
  const time = new Date();
  return time.toLocaleString();
}

exports.getCurrentTime = getCurrentTime;
复制代码

上面的模块中,咱们选择了经过 exports 导出 getCurrentTime 函数。

最后,咱们在 timer.js 中经过 require 导入刚才两个模块,并分别调用模块中的函数 printProgramInfogetCurrentTime,代码以下:

const printProgramInfo = require('./info');
const datetime = require('./datetime');

setTimeout(() => {
  console.log('Hello World!');
}, 3000);

printProgramInfo();
console.log('当前时间', datetime.getCurrentTime());
复制代码

再运行一下 timer.js,输出内容应该与以前彻底一致。

读到这里,我想先恭喜你渡过了 Node.js 入门最难的一关!若是你已经真正地理解了 Node 模块机制,那么我相信接下来的学习会无比轻松哦。

命令行开发:接受输入参数

Node.js 做为能够在操做系统中直接运行 JavaScript 代码的平台,为前端开发者开启了无限可能,其中就包括一系列用于实现前端自动化工做流的命令行工具,例如 GruntGulp 还有大名鼎鼎的 Webpack

从这一步开始,咱们将把 timer.js 改形成一个命令行应用。具体地,咱们但愿 timer.js 能够经过命令行参数指定等待的时间(time 选项)和最终输出的信息(message 选项):

$ node timer.js --time 5 --message "Hello Tuture"
复制代码

经过 process.argv 读取命令行参数

以前在讲全局对象 process 时提到一个 argv 属性,可以获取命令行参数的数组。建立一个 args.js 文件,代码以下:

console.log(process.argv);
复制代码

而后运行如下命令:

$ node args.js --time 5 --message "Hello Tuture"
复制代码

输出一个数组:

[
  '/Users/mRc/.nvm/versions/node/v12.10.0/bin/node',
  '/Users/mRc/Tutorials/nodejs-quickstart/args.js',
  '--time',
  '5',
  '--message',
  'Hello Tuture'
]
复制代码

能够看到,process.argv 数组的第 0 个元素是 node 的实际路径,第 1 个元素是 args.js 的路径,后面则是输入的全部参数。

实现命令行应用

根据刚才的分析,咱们能够很是简单粗暴地获取 process.argv 的第 3 个和第 5 个元素,分别能够获得 timemessage 参数。因而修改 timer.js 的代码以下:

const printProgramInfo = require('./info');
const datetime = require('./datetime');

const waitTime = Number(process.argv[3]);
const message = process.argv[5];

setTimeout(() => {
  console.log(message);
}, waitTime * 1000);

printProgramInfo();
console.log('当前时间', datetime.getCurrentTime());
复制代码

提醒一下,setTimeout 中时间的单位是毫秒,而咱们指定的时间参数单位是秒,所以要乘 1000。

运行 timer.js,加上刚才说的全部参数:

$ node timer.js --time 5 --message "Hello Tuture"
复制代码

等待 5 秒钟后,你就看到了 Hello Tuture 的提示文本!

不过很显然,目前这个版本有很大的问题:输入参数的格式是固定的,很不灵活,好比说调换 timemessage 的输入顺序就会出错,也不能检查用户是否输入了指定的参数,格式是否正确等等。若是要亲自实现上面所说的功能,那可得花很大的力气,说不定还会有很多 Bug。有没有更好的方案呢?

npm:洪荒之力,都赐予你

从这一节开始,你将再也不是一我的写代码。你的背后将拥有百万名 JavaScript 开发者的支持,而这一切仅须要 npm 就能够实现。npm 包括:

  • npm 命令行工具(安装 node 时也会附带安装)
  • npm 集中式依赖仓库(registry),存放了其余 JavaScript 开发者分享的 npm 包
  • npm 网站,能够搜索须要的 npm 包、管理 npm 账户等

npm 初探

咱们首先打开终端(命令行),检查一下 npm 命令是否可用:

$ npm -v
6.10.3
复制代码

而后在当前目录(也就是刚才编辑的 timer.js 所在的文件夹)运行如下命令,把当前项目初始化为 npm 项目:

$ npm init
复制代码

这时候 npm 会提一系列问题,你能够一路回车下去,也能够仔细回答,最终会建立一个 package.json 文件。package.json 文件是一个 npm 项目的核心,记录了这个项目全部的关键信息,内容以下:

{
  "name": "timer",
  "version": "1.0.0",
  "description": "A cool timer",
  "main": "timer.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mRcfps/nodejs-quickstart.git"
  },
  "author": "mRcfps",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/mRcfps/nodejs-quickstart/issues"
  },
  "homepage": "https://github.com/mRcfps/nodejs-quickstart#readme"
}

复制代码

其中大部分字段的含义都很明确,例如 name 项目名称、 version 版本号、description 描述、author 做者等等。不过这个 scripts 字段你可能会比较困惑,咱们会在下一节中详细介绍。

安装 npm 包

接下来咱们将讲解 npm 最最最经常使用的命令—— install。没错,绝不夸张地说,一个 JavaScript 程序员用的最多的 npm 命令就是 npm install

在安装咱们须要的 npm 包以前,咱们须要去探索一下有哪些包能够为咱们所用。一般,咱们能够在 npm 官方网站 上进行关键词搜索(记得用英文哦),好比说咱们搜 command line:

出来的第一个结果 commander 就很符合咱们的须要,点进去就是安装的说明和使用文档。咱们还想要一个“加载中”的动画效果,提升用户的使用体验,试着搜一下 loading 关键词:

第二个结果 ora 也符合咱们的须要。那咱们如今就安装这两个 npm 包:

$ npm install commander ora
复制代码

少量等待后,能够看到 package.json 多了一个很是重要的 dependencies 字段:

"dependencies": {
  "commander": "^4.0.1",
  "ora": "^4.0.3"
}
复制代码

这个字段中就记录了咱们这个项目的直接依赖。与直接依赖相对的就是间接依赖,例如 commander 和 ora 的依赖,咱们一般不用关心。全部的 npm 包(直接依赖和间接依赖)所有都存放在项目的 node_modules 目录中。

提示

node_modules 一般有不少的文件,所以不会加入到 Git 版本控制系统中,你从网上下载的 npm 项目通常也只会有 package.json,这时候只需运行 npm install(后面不跟任何内容),就能够下载并安装全部依赖了。

整个 package.json 代码以下所示:

{
  "name": "timer",
  "version": "1.0.0",
  "description": "A cool timer",
  "main": "timer.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mRcfps/nodejs-quickstart.git"
  },
  "author": "mRcfps",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/mRcfps/nodejs-quickstart/issues"
  },
  "homepage": "https://github.com/mRcfps/nodejs-quickstart#readme",
  "dependencies": {
    "commander": "^4.0.1",
    "ora": "^4.0.3"
  }
}
复制代码

关于版本号

在软件开发中,版本号是一个很是重要的概念,不一样版本的软件存在或大或小的差别。npm 采用了语义版本号(Semantic Versioning,简称 semver),具体规定以下:

  • 版本格式为:主版本号.次版本号.修订号
  • 主版本号的改变意味着不兼容的 API 修改
  • 次版本号的改变意味着作了向下兼容的功能性新增
  • 修订号的改变意味着作了向下兼容的问题修正

提示

向下兼容的简单理解就是功能只增不减

所以在 package.json 的 dependencies 字段中,能够经过如下方式指定版本:

  • 精确版本:例如 1.0.0,必定只会安装版本为 1.0.0 的依赖
  • 锁定主版本和次版本:能够写成 1.01.0.x~1.0.0,那么可能会安装例如 1.0.8 的依赖
  • 仅锁定主版本:能够写成 11.x^1.0.0npm install 默认采用的形式),那么可能会安装例如 1.1.0 的依赖
  • 最新版本:能够写成 *x,那么直接安装最新版本(不推荐)

你也许注意到了 npm 还建立了一个 package-lock.json,这个文件就是用来锁定所有直接依赖和间接依赖的精确版本号,或者说提供了关于 node_modules 目录的精确描述,从而确保在这个项目中开发的全部人都能有彻底一致的 npm 依赖。

站在巨人的肩膀上

咱们在大体读了一下 commander 和 ora 的文档以后,就能够开始用起来了,修改 timer.js 代码以下:

const program = require('commander');
const ora = require('ora');
const printProgramInfo = require('./info');
const datetime = require('./datetime');

program
  .option('-t, --time <number>', '等待时间 (秒)', 3)
  .option('-m, --message <string>', '要输出的信息', 'Hello World')
  .parse(process.argv);

setTimeout(() => {
  spinner.stop();
  console.log(program.message);
}, program.time * 1000);

printProgramInfo();
console.log('当前时间', datetime.getCurrentTime());
const spinner = ora('正在加载中,请稍后 ...').start();
复制代码

此次,咱们再次运行 timer.js:

$ node timer.js --message "洪荒之力!" --time 5
复制代码

转起来了!

尝鲜 npm scripts

在本教程的最后一节中,咱们将简单地介绍一下 npm scripts,也就是 npm 脚本。以前在 package.json 中提到,有个字段叫 scripts,这个字段就定义了所有的 npm scripts。咱们发如今用 npm init 时建立的 package.json 文件默认就添加了一个 test 脚本:

"test": "echo \"Error: no test specified\" && exit 1"
复制代码

那一串命令就是 test 脚本将要执行的内容,咱们能够经过 npm test 命令执行该脚本:

$ npm test

> timer@1.0.0 test /Users/mRc/Tutorials/nodejs-quickstart
> echo "Error: no test specified" && exit 1

Error: no test specified
npm ERR! Test failed.  See above for more details.
复制代码

在初步体验了 npm scripts 以后,咱们有必要了解一下 npm scripts 分为两大类:

  • 预约义脚本:例如 teststartinstallpublish 等等,直接经过 npm <scriptName> 运行,例如 npm test,全部预约义的脚本可查看文档
  • 自定义脚本:除了以上自带脚本的其余脚本,须要经过 npm run <scriptName> 运行,例如 npm run custom

如今就让咱们开始为 timer 项目添加两个 npm scripts,分别是 startlint。第一个是预约义的,用于启动咱们的 timer.js;第二个是静态代码检查,用于在开发时检查咱们的代码。首先安装 ESLint npm 包:

$ npm install eslint --save-dev
$ # 或者
$ npm install eslint -D
复制代码

注意到咱们加了一个 -D--save-dev 选项,表明 eslint 是一个开发依赖,在实际项目发布或部署时不须要用到。npm 会把全部开发依赖添加到 devDependencies 字段中。而后分别添加 startlint 脚本,代码以下:

{
  "name": "timer",
  "version": "1.0.0",
  "description": "A cool timer",
  "main": "timer.js",
  "scripts": {
    "lint": "eslint **/*.js",
    "start": "node timer.js -m '上手了' -t 3",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mRcfps/nodejs-quickstart.git"
  },
  "author": "mRcfps",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/mRcfps/nodejs-quickstart/issues"
  },
  "homepage": "https://github.com/mRcfps/nodejs-quickstart#readme",
  "dependencies": {
    "commander": "^4.0.1",
    "ora": "^4.0.3"
  },
  "devDependencies": {
    "eslint": "^6.7.2"
  }
}
复制代码

ESLint 的使用须要一个配置文件,建立 .eslintrc.js 文件(注意最前面有一个点),代码以下:

module.exports = {
    "env": {
        "es6": true,
        "node": true,
    },
    "extends": "eslint:recommended",
};
复制代码

运行 npm start,能够看到成功地运行了咱们的 timer.js 脚本;而运行 npm run lint,没有输出任何结果(表明静态检查经过)。

npm scripts 看上去平淡无奇,可是却能为项目开发提供很是便利的工做流。例如,以前构建一个项目须要很是复杂的命令,可是若是你实现了一个 build npm 脚本,那么当你的同事拿到这份代码时,只需简单地执行 npm run build 就能够开始构建,而无需关心背后的技术细节。在后续的 Node.js 或是前端学习中,咱们会在实际项目中使用各类 npm scripts 来定义咱们的工做流,你们慢慢就会领会到它的强大了。

下次再见:监听 exit 事件

在这篇教程的最后一节中,咱们将让你简单地感觉 Node 的事件机制。Node 的事件机制是比较复杂的,足够讲半本书,但这篇教程但愿能经过一个很是简单的实例,让你对 Node 事件有个初步的了解。

提示

若是你有过在网页(或其余用户界面)开发中编写事件处理(例如鼠标点击)的经验,那么你必定会以为 Node 中处理事件的方式似曾相识而又符合直觉。

咱们在前面简单地提了一下回调函数。实际上,回调函数和事件机制共同组成了 Node 的异步世界。具体而言,Node 中的事件都是经过 events 核心模块中的 EventEmitter 这个类实现的。EventEmitter 包括两个最关键的方法:

  • on:用来监听事件的发生
  • emit:用来触发新的事件

请看下面这个代码片断:

const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();

// 监听 connect 事件,注册回调函数
emitter.on('connect', function (username) {
  console.log(username + '已链接');
});

// 触发 connect 事件,而且加上一个参数(即上面的 username)
emitter.emit('connect', '一只图雀');
复制代码

运行上面的代码,就会输出如下内容:

一只图雀已链接
复制代码

能够说,Node 中不少对象都继承自 EventEmitter,包括咱们熟悉的 process 全局对象。在以前的 timer.js 脚本中,咱们监听 exit 事件(即 Node 进程结束),并添加一个自定义的回调函数打印“下次再见”的信息:

const program = require('commander');
const ora = require('ora');
const printProgramInfo = require('./info');
const datetime = require('./datetime');

program
  .option('-t, --time <number>', '等待时间 (秒)', 3)
  .option('-m, --message <string>', '要输出的信息', 'Hello World')
  .parse(process.argv);

setTimeout(() => {
  spinner.stop();
  console.log(program.message);
}, program.time * 1000);

process.on('exit', () => {
  console.log('下次再见~');
});

printProgramInfo();
console.log('当前时间', datetime.getCurrentTime());
const spinner = ora('正在加载中,请稍后 ...').start();
复制代码

运行后,会在程序退出后打印“下次再见~”的字符串。你可能会问,为啥不能在 setTimeout 的回调函数中添加程序退出的逻辑呢?由于除了正常运行结束(也就是等待了指定的时间),咱们的程序颇有可能会由于其余缘由退出(例如抛出异常,或者用 process.exit 强制退出),这时候经过监听 exit 事件,就能够在确保全部状况下都能执行 exit 事件的回调函数。若是你以为仍是不能理解的话,能够看下面这张示意图:

提示

process 对象还支持其余经常使用的事件,例如 SIGINT(用户按 Ctrl+C 时触发)等等,可参考这篇文档

这篇 Node.js 快速入门教程到这里就结束了,但愿可以成为你进一步探索 Node.js 或是前端开发的基石。exit 事件已经触发,那咱们也下次再见啦~

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

相关文章
相关标签/搜索