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

Node.js 太火了,火到几乎所有前端工程师都想学,几乎所有后端工程师也想学。一说到 Node.js,我们马上就会想到“异步”、“事件驱动”、“非阻塞”、“性能优良”这几个特点,但是你真的理解这些词的含义吗?这篇教程将带你快速入门 Node.js,为后续的前端学习或是 Node.js 进阶打下坚实的基础。 此教程属于Node.js 后端工程师学习路线的一部分,点击可查看全部内容。 起步 什么是 Node? 简单地说,Node(或者说 Node.js,两者是等价的)是 JavaScript 的一种运行环境。在此之前,我们知道 JavaScript 都是在浏览器中执行的,用于给网页添加各种动态效果,那么可以说浏览器也是 JavaScript 的运行环境。那么这两个运行环境有哪些差异呢?请看下图: 两个运行环境共同包含了 ECMAScript,也就是剥离了所有运行环境的 JavaScript 语言标准本身。现在 ECMAScript 的发展速度非常惊人,几乎能够做到每年发展一个版本。 提示 ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。在日常场合,这两个词是可以互换的。更多背景知识可参考阮一峰的《JavaScript语言的历史》。 另一方面,浏览器端 JavaScript 还包括了: 浏览器对象模型(Browser Object Model,简称 BOM),也就是 window 对象 文档对象模型(Document Object Model,简称 DOM),也就是 document 对象 而 Node.js 则是包括 V8 引擎。V8 是 Chrome 浏览器中的 JavaScript 引擎,经过多年的发展和优化,性能和安全性都已经达到了相当的高度。而 Node.js 则进一步将 V8 引擎加工成可以在任何操作系统中运行 JavaScript 的平台。 预备知识 在正式开始这篇教程之前,我们希望你已经做好了以下准备: 了解 JavaScript 语言的基础知识,如果有过浏览器 JS 开发经验就更好了 已经安装了 Node.js,配置好了适合自己的编辑器或 IDE 了解相对路径和绝对路径 学习目标 这篇教程将会让你学到: 浏览器 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 的经验,那么你一定对全局对象不陌生。在浏览器中,我们有 document 和 window 等全局对象;而 Node 只包含 ECMAScript 和 V8,不包含 BOM 和 DOM,因此 Node 中不存在 document 和 window;取而代之,Node 专属的全局对象是 process。在这一节中,我们将初步探索一番 Node 全局对象。 JavaScript 全局对象的分类 在此之前,我们先看一下 JavaScript 各个运行环境的全局对象的比较,如下图所示: 可以看到 JavaScript 全局对象可以分为四类: 浏览器专属,例如 window、alert 等等; Node 专属,例如 process、Buffer、__dirname、__filename 等等; 浏览器和 Node 共有,但是实现方式不同,例如 console(第一节中已提到)、setTimeout、setInterval 等; 浏览器和 Node 共有,并且属于 ECMAScript 语言定义的一部分,例如 Date、String、Promise 等; 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 实现方式不同的共有全局对象:console 和 setTimeout 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 语言定义本身并没有模块化的机制,构建复杂应用也没有统一的接口标准。人们通常使用一系列的 这种组织 JS 代码的方式有很多问题,其中最显著的包括: 导入的多个 JS 文件直接作用于全局命名空间,很容易产生命名冲突 导入的 JS 文件之间不能相互访问,例如 fileB.js 中无法访问 fileA.js 中的内容,很不方便 导入的