Node.js 太火了,火到幾乎所有前端工程師都想學(xué),幾乎所有后端工程師也想學(xué)。一說到 Node.js,我們馬上就會(huì)想到“異步”、“事件驅(qū)動(dòng)”、“非阻塞”、“性能優(yōu)良”這幾個(gè)特點(diǎn),但是你真的理解這些詞的含義嗎?這篇教程將帶你快速入門 Node.js,為后續(xù)的前端學(xué)習(xí)或是 Node.js 進(jìn)階打下堅(jiān)實(shí)的基礎(chǔ)。
此教程屬于Node.js 后端工程師學(xué)習(xí)路線的一部分,點(diǎn)擊可查看全部內(nèi)容。
起步
什么是 Node?
簡單地說,Node(或者說 Node.js,兩者是等價(jià)的)是 JavaScript 的一種運(yùn)行環(huán)境。在此之前,我們知道 JavaScript 都是在瀏覽器中執(zhí)行的,用于給網(wǎng)頁添加各種動(dòng)態(tài)效果,那么可以說瀏覽器也是 JavaScript 的運(yùn)行環(huán)境。那么這兩個(gè)運(yùn)行環(huán)境有哪些差異呢?請(qǐng)看下圖:
兩個(gè)運(yùn)行環(huán)境共同包含了 ECMAScript,也就是剝離了所有運(yùn)行環(huán)境的 JavaScript 語言標(biāo)準(zhǔn)本身?,F(xiàn)在 ECMAScript 的發(fā)展速度非常驚人,幾乎能夠做到每年發(fā)展一個(gè)版本。
提示
ECMAScript 和 JavaScript 的關(guān)系是,前者是后者的規(guī)格,后者是前者的一種實(shí)現(xiàn)。在日常場合,這兩個(gè)詞是可以互換的。更多背景知識(shí)可參考阮一峰的《JavaScript語言的歷史》。
另一方面,瀏覽器端 JavaScript 還包括了:
- 瀏覽器對(duì)象模型(Browser Object Model,簡稱 BOM),也就是
window對(duì)象 - 文檔對(duì)象模型(Document Object Model,簡稱 DOM),也就是
document對(duì)象
而 Node.js 則是包括 V8 引擎。V8 是 Chrome 瀏覽器中的 JavaScript 引擎,經(jīng)過多年的發(fā)展和優(yōu)化,性能和安全性都已經(jīng)達(dá)到了相當(dāng)?shù)母叨取6?Node.js 則進(jìn)一步將 V8 引擎加工成可以在任何操作系統(tǒng)中運(yùn)行 JavaScript 的平臺(tái)。
預(yù)備知識(shí)
在正式開始這篇教程之前,我們希望你已經(jīng)做好了以下準(zhǔn)備:
- 了解 JavaScript 語言的基礎(chǔ)知識(shí),如果有過瀏覽器 JS 開發(fā)經(jīng)驗(yàn)就更好了
- 已經(jīng)安裝了 Node.js,配置好了適合自己的編輯器或 IDE
- 了解相對(duì)路徑和絕對(duì)路徑
學(xué)習(xí)目標(biāo)
這篇教程將會(huì)讓你學(xué)到:
- 瀏覽器 JavaScript 與 Node.js 的關(guān)系與區(qū)別
- 了解 Node.js 有哪些全局對(duì)象
- 掌握 Node.js 如何導(dǎo)入和導(dǎo)出模塊,以及模塊機(jī)制的原理
- 了解如何用 Node.js 開發(fā)簡單的命令行應(yīng)用
- 學(xué)會(huì)利用 npm 社區(qū)的力量解決開發(fā)中遇到的難題,避免“重復(fù)造輪子”
- 了解 npm scripts 的基本概念和使用
- 初步了解 Node.js 的事件機(jī)制
運(yùn)行 Node 代碼
運(yùn)行 Node 代碼通常有兩種方式:1)在 REPL 中交互式輸入和運(yùn)行;2)將代碼寫入 JS 文件,并用 Node 執(zhí)行。
提示
REPL 的全稱是 Read Eval Print Loop(讀取-執(zhí)行-輸出-循環(huán)),通??梢岳斫鉃?strong>交互式解釋器,你可以輸入任何表達(dá)式或語句,然后就會(huì)立刻執(zhí)行并返回結(jié)果。如果你用過 Python 的 REPL 一定會(huì)覺得很熟悉。
使用 REPL 快速體驗(yàn)
如果你已經(jīng)安裝好了 Node,那么運(yùn)行以下命令就可以輸出 Node.js 的版本:
$ node -v
v12.10.0
然后,我們還可以進(jìn)入 Node REPL(直接輸入 node),然后輸入任何合法的 JavaScript 表達(dá)式或語句:
$ 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
有些行的開頭是 >,代表輸入提示符,因此 > 后面的都是我們要輸入的命令,其他行則是表達(dá)式的返回值或標(biāo)準(zhǔn)輸出(Standard Output,stdout)。運(yùn)行的效果如下:
編寫 Node 腳本
REPL 通常用來進(jìn)行一些代碼的試驗(yàn)。在搭建具體應(yīng)用時(shí),更多的還是創(chuàng)建 Node 文件。我們先創(chuàng)建一個(gè)最簡單的 Node.js 腳本文件,叫做 timer.js,代碼如下:
console.log('Hello World!');
然后用 Node 解釋器執(zhí)行這個(gè)文件:
$ node timer.js
Hello World!
看上去非常平淡無奇,但是這一行代碼卻凝聚了 Node.js 團(tuán)隊(duì)背后的心血。我們來對(duì)比一下,在瀏覽器和 Node 環(huán)境中執(zhí)行這行代碼有什么區(qū)別:
- 在瀏覽器運(yùn)行
console.log調(diào)用了 BOM,實(shí)際上執(zhí)行的是window.console.log('Hello World!') - Node 首先在所處的操作系統(tǒng)中創(chuàng)建一個(gè)新的進(jìn)程,然后向標(biāo)準(zhǔn)輸出打印了指定的字符串, 實(shí)際上執(zhí)行的是
process.stdout.write('Hello World!\n')
簡而言之,Node 為我們提供了一個(gè)無需依賴瀏覽器、能夠直接與操作系統(tǒng)進(jìn)行交互的 JavaScript 代碼運(yùn)行環(huán)境!
Node 全局對(duì)象初探
如果你有過編寫 JavaScript 的經(jīng)驗(yàn),那么你一定對(duì)全局對(duì)象不陌生。在瀏覽器中,我們有 document 和 window 等全局對(duì)象;而 Node 只包含 ECMAScript 和 V8,不包含 BOM 和 DOM,因此 Node 中不存在 document 和 window;取而代之,Node 專屬的全局對(duì)象是 process。在這一節(jié)中,我們將初步探索一番 Node 全局對(duì)象。
JavaScript 全局對(duì)象的分類
在此之前,我們先看一下 JavaScript 各個(gè)運(yùn)行環(huán)境的全局對(duì)象的比較,如下圖所示:
可以看到 JavaScript 全局對(duì)象可以分為四類:
- 瀏覽器專屬,例如
window、alert等等; - Node 專屬,例如
process、Buffer、__dirname、__filename等等; - 瀏覽器和 Node 共有,但是實(shí)現(xiàn)方式不同,例如
console(第一節(jié)中已提到)、setTimeout、setInterval等; - 瀏覽器和 Node 共有,并且屬于 ECMAScript 語言定義的一部分,例如
Date、String、Promise等;
Node 專屬全局對(duì)象解析
process
process 全局對(duì)象可以說是 Node.js 的靈魂,它是管理當(dāng)前 Node.js 進(jìn)程狀態(tài)的對(duì)象,提供了與操作系統(tǒng)的簡單接口。
首先我們探索一下 process 對(duì)象的重要屬性。打開 Node REPL,然后我們查看一下 process 對(duì)象的一些屬性:
-
pid:進(jìn)程編號(hào) -
env:系統(tǒng)環(huán)境變量 -
argv:命令行執(zhí)行此腳本時(shí)的輸入?yún)?shù) -
platform:當(dāng)前操作系統(tǒng)的平臺(tái)
提示
可以在 Node REPL 中嘗試一下這些對(duì)象。像上面說的那樣進(jìn)入 REPL(你的輸出很有可能跟我的不一樣):
$ node Welcome to Node.js v12.10.0. Type ".help" for more information. > process.pid 3 > process.platform 'darwin'
Buffer
Buffer 全局對(duì)象讓 JavaScript 也能夠輕松地處理二進(jìn)制數(shù)據(jù)流,結(jié)合 Node 的流接口(Stream),能夠?qū)崿F(xiàn)高效的二進(jìn)制文件處理。這篇教程不會(huì)涉及 Buffer。
__filename 和 __dirname
分別代表當(dāng)前所運(yùn)行 Node 腳本的文件路徑和所在目錄路徑。
警告
__filename和__dirname只能在 Node 腳本文件中使用,在 REPL 中是沒有定義的。
使用 Node 全局對(duì)象
接下來我們將在剛才寫的腳本文件中使用 Node 全局對(duì)象,分別涵蓋上面的三類:
- Node 專屬:
process - 實(shí)現(xiàn)方式不同的共有全局對(duì)象:
console和setTimeout - ECMAScript 語言定義的全局對(duì)象:
Date
提示
setTimeout用于在一定時(shí)間后執(zhí)行特定的邏輯,第一個(gè)參數(shù)為時(shí)間到了之后要執(zhí)行的函數(shù)(回調(diào)函數(shù)),第二個(gè)參數(shù)是等待時(shí)間。例如:setTimeout(someFunction, 1000);就會(huì)在
1000毫秒后執(zhí)行someFunction函數(shù)。
代碼如下:
setTimeout(() => {
console.log('Hello World!');
}, 3000);
console.log('當(dāng)前進(jìn)程 ID', process.pid);
console.log('當(dāng)前腳本路徑', __filename);
const time = new Date();
console.log('當(dāng)前時(shí)間', time.toLocaleString());
運(yùn)行以上腳本,在我機(jī)器上的輸出如下(Hello World! 會(huì)延遲三秒輸出):
$ node timer.js
當(dāng)前進(jìn)程 ID 7310
當(dāng)前腳本路徑 /Users/mRc/Tutorials/nodejs-quickstart/timer.js
當(dāng)前時(shí)間 12/4/2019, 9:49:28 AM
Hello World!
從上面的代碼中也可以一瞥 Node.js 異步的魅力:在 setTimeout 等待的 3 秒內(nèi),程序并沒有阻塞,而是繼續(xù)向下執(zhí)行,這就是 Node.js 的異步非阻塞!
提示
在實(shí)際的應(yīng)用環(huán)境中,往往有很多 I/O 操作(例如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫查詢等等)需要耗費(fèi)相當(dāng)多的時(shí)間,而 Node.js 能夠在等待的同時(shí)繼續(xù)處理新的請(qǐng)求,大大提高了系統(tǒng)的吞吐率。
在后續(xù)教程中,我們會(huì)出一篇深入講解 Node.js 異步編程的教程,敬請(qǐng)期待!
理解 Node 模塊機(jī)制
Node.js 相比之前的瀏覽器 JavaScript 的另一個(gè)重點(diǎn)改變就是:模塊機(jī)制的引入。這一節(jié)內(nèi)容很長,但卻是入門 Node.js 最為關(guān)鍵的一步,加油吧??!
JavaScript 的模塊化之路
Eric Raymond 在《UNIX編程藝術(shù)》中定義了模塊性(Modularity)的規(guī)則:
開發(fā)人員應(yīng)使用通過定義明確的接口連接的簡單零件來構(gòu)建程序,因此問題是局部的,可以在將來的版本中替換程序的某些部分以支持新功能。 該規(guī)則旨在節(jié)省調(diào)試復(fù)雜、冗長且不可讀的復(fù)雜代碼的時(shí)間。
“分而治之”的思想在計(jì)算機(jī)的世界非常普遍,但是在 ES2015 標(biāo)準(zhǔn)出現(xiàn)以前(不了解沒關(guān)系,后面會(huì)講到), JavaScript 語言定義本身并沒有模塊化的機(jī)制,構(gòu)建復(fù)雜應(yīng)用也沒有統(tǒng)一的接口標(biāo)準(zhǔn)。人們通常使用一系列的 <script> 標(biāo)簽來導(dǎo)入相應(yīng)的模塊(依賴):
<head>
<script src="fileA.js"></script>
<script src="fileB.js"></script>
</head>
這種組織 JS 代碼的方式有很多問題,其中最顯著的包括:
- 導(dǎo)入的多個(gè) JS 文件直接作用于全局命名空間,很容易產(chǎn)生命名沖突
- 導(dǎo)入的 JS 文件之間不能相互訪問,例如 fileB.js 中無法訪問 fileA.js 中的內(nèi)容,很不方便
- 導(dǎo)入的
<script>無法被輕易去除或修改
人們漸漸認(rèn)識(shí)到了 JavaScript 模塊化機(jī)制的缺失帶來的問題,于是兩大模塊化規(guī)范被提出:
- AMD(Asynchronous Module Definition)規(guī)范,在瀏覽器中使用較為普遍,最經(jīng)典的實(shí)現(xiàn)包括 RequireJS;
- CommonJS 規(guī)范,致力于為 JavaScript 生態(tài)圈提供統(tǒng)一的接口 API,Node.js 所實(shí)現(xiàn)的正是這一模塊標(biāo)準(zhǔn)。
提示
ECMAScript 2015(也就是大家常說的 ES6)標(biāo)準(zhǔn)為 JavaScript 語言引入了全新的模塊機(jī)制(稱為 ES 模塊,全稱 ECMAScript Modules),并提供了
import和export關(guān)鍵詞,如果感興趣可參考這篇文章。但是截止目前,Node.js 對(duì) ES 模塊的支持還處于試驗(yàn)階段,因此這篇文章不會(huì)講解、也不提倡使用。
什么是 Node 模塊
在正式分析 Node 模塊機(jī)制之前,我們需要明確定義什么是 Node 模塊。通常來說,Node 模塊可分為兩大類:
- 核心模塊:Node 提供的內(nèi)置模塊,在安裝 Node 時(shí)已經(jīng)被編譯成二進(jìn)制可執(zhí)行文件
- 文件模塊:用戶編寫的模塊,可以是自己寫的,也可以是通過 npm 安裝的(后面會(huì)講到)。
其中,文件模塊可以是一個(gè)單獨(dú)的文件(以 .js、.node 或 .json 結(jié)尾),或者是一個(gè)目錄。當(dāng)這個(gè)模塊是一個(gè)目錄時(shí),模塊名就是目錄名,有兩種情況:
- 目錄中有一個(gè) package.json 文件,則這個(gè) Node 模塊的入口就是其中
main字段指向的文件; - 目錄中有一個(gè)名為 index 的文件,擴(kuò)展名為
.js、.node或.json,此文件則為模塊入口文件。
一下子消化不了沒關(guān)系,可以先閱讀后面的內(nèi)容,忘記了模塊的定義可以再回過來看看哦。
Node 模塊機(jī)制淺析
知道了 Node 模塊的具體定義后,我們來了解一下 Node 具體是怎樣實(shí)現(xiàn)模塊機(jī)制的。具體而言,Node 引入了三個(gè)新的全局對(duì)象(還是 Node 專屬哦):1)require;2) exports 和 3)module。下面我們逐一講解。
require
require 用于導(dǎo)入其他 Node 模塊,其參數(shù)接受一個(gè)字符串代表模塊的名稱或路徑,通常被稱為模塊標(biāo)識(shí)符。具體有以下三種形式:
- 直接寫模塊名稱,通常是核心模塊或第三方文件模塊,例如
os、express等 - 模塊的相對(duì)路徑,指向項(xiàng)目中其他 Node 模塊,例如
./utils - 模塊的絕對(duì)路徑(不推薦!),例如
/home/xxx/MyProject/utils
提示
在通過路徑導(dǎo)入模塊時(shí),通常省略文件名中的
.js后綴。
代碼示例如下:
// 導(dǎo)入內(nèi)置庫或第三方模塊
const os = require('os');
const express = require('express');
// 通過相對(duì)路徑導(dǎo)入其他模塊
const utils = require('./utils');
// 通過絕對(duì)路徑導(dǎo)入其他模塊
const utils = require('/home/xxx/MyProject/utils');
你也許會(huì)好奇,通過名稱導(dǎo)入 Node 模塊的時(shí)候(例如 express),是從哪里找到這個(gè)模塊的?實(shí)際上每個(gè)模塊都有個(gè)路徑搜索列表 module.paths,在后面講解 module 對(duì)象的時(shí)候就會(huì)一清二楚了。
exports
我們已經(jīng)學(xué)會(huì)了用 require 導(dǎo)入其他模塊中的內(nèi)容,那么怎么寫一個(gè) Node 模塊,并導(dǎo)出其中內(nèi)容呢?答案就是用 exports 對(duì)象。
例如我們寫一個(gè) Node 模塊 myModule.js:
// myModule.js
function add(a, b) {
return a + b;
}
// 導(dǎo)出函數(shù) add
exports.add = add;
通過將 add 函數(shù)添加到 exports 對(duì)象中,外面的模塊就可以通過以下代碼使用這個(gè)函數(shù)。在 myModule.js 旁邊創(chuàng)建一個(gè) main.js,代碼如下:
// main.js
const myModule = require('./myModule');
// 調(diào)用 myModule.js 中的 add 函數(shù)
myModule.add(1, 2);
提示
如果你熟悉 ECMAScript 6 中的解構(gòu)賦值,那么可以用更優(yōu)雅的方式獲取
add函數(shù):const { add } = require('./myModule');
module
通過 require 和 exports,我們已經(jīng)知道了如何導(dǎo)入、導(dǎo)出 Node 模塊中的內(nèi)容,但是你可能還是覺得 Node 模塊機(jī)制有一絲絲神秘的感覺。接下來,我們將掀開這神秘的面紗,了解一下背后的主角——module 模塊對(duì)象。
我們可以在剛才的 myModule.js 文件的最后加上這一行代碼:
console.log('module myModule:', module);
在 main.js 最后加上:
console.log('module main:', module);
運(yùn)行后會(huì)打印出來這樣的內(nèi)容(左邊是 myModule,右邊是 module):
可以看到 module 對(duì)象有以下字段:
-
id:模塊的唯一標(biāo)識(shí)符,如果是被運(yùn)行的主程序(例如 main.js)則為.,如果是被導(dǎo)入的模塊(例如 myModule.js)則等同于此文件名(即下面的filename字段) -
path和filename:模塊所在路徑和文件名,沒啥好說的 -
exports:模塊所導(dǎo)出的內(nèi)容,實(shí)際上之前的exports對(duì)象是指向module.exports的引用。例如對(duì)于 myModule.js,剛才我們導(dǎo)出了add函數(shù),因此出現(xiàn)在了這個(gè)exports字段里面;而 main.js 沒有導(dǎo)出任何內(nèi)容,因此exports字段為空 -
parent和children:用于記錄模塊之間的導(dǎo)入關(guān)系,例如 main.js 中require了 myModule.js,那么 main 就是 myModule 的parent,myModule 就是 main 的children -
loaded:模塊是否被加載,從上圖中可以看出只有children中列出的模塊才會(huì)被加載 -
paths:這個(gè)就是 Node 搜索文件模塊的路徑列表,Node 會(huì)從第一個(gè)路徑到最后一個(gè)路徑依次搜索指定的 Node 模塊,找到了則導(dǎo)入,找不到就會(huì)報(bào)錯(cuò)
提示
如果你仔細(xì)觀察,會(huì)發(fā)現(xiàn) Node 文件模塊查找路徑(
module.paths)的方式其實(shí)是這樣的:先找當(dāng)前目錄下的 node_modules,沒有的話再找上一級(jí)目錄的 node_modules,還沒找到的話就一直向上找,直到根目錄下的 node_modules。
深入理解 module.exports
之前我們提到,exports 對(duì)象本質(zhì)上是 module.exports 的引用。也就是說,下面兩行代碼是等價(jià)的:
// 導(dǎo)出 add 函數(shù)
exports.add = add;
// 和上面一行代碼是一樣的
module.exports.add = add;
實(shí)際上還有第二種導(dǎo)出方式,直接把 add 函數(shù)賦給 module.exports 對(duì)象:
module.exports = add;
這樣寫和第一種導(dǎo)出方式有什么區(qū)別呢?第一種方式,在 exports 對(duì)象上添加一個(gè)屬性名為 add,該屬性的值為 add 函數(shù);第二種方式,直接令 exports 對(duì)象為 add 函數(shù)??赡苡悬c(diǎn)繞,但是請(qǐng)一定要理解這兩者的重大區(qū)別!
在 require 時(shí),兩者的區(qū)別就很明顯了:
// 第一種導(dǎo)出方式,需要訪問 add 屬性獲取到 add 函數(shù)
const myModule = require('myModule');
myModule.add(1, 2);
// 第二種導(dǎo)出方式,可以直接使用 add 函數(shù)
const add = require('myModule');
add(1, 2);
警告
直接寫
exports = add;無法導(dǎo)出add函數(shù),因?yàn)?exports本質(zhì)上是指向module的exports屬性的引用,直接對(duì)exports賦值只會(huì)改變exports,對(duì)module.exports沒有影響。如果你覺得難以理解,那我們用apple和price類比module和exports:apple = { price: 1 }; // 想象 apple 就是 module price = apple.price; // 想象 price 就是 exports apple.price = 3; // 改變了 apple.price price = 3; // 只改變了 price,沒有改變 apple.price我們只能通過
apple.price = 1設(shè)置price屬性,而直接對(duì)price賦值并不能修改apple.price。
重構(gòu) timer 腳本
在聊了這么多關(guān)于 Node 模塊機(jī)制的內(nèi)容后,是時(shí)候回到我們之前的定時(shí)器腳本 timer.js 了。我們首先創(chuàng)建一個(gè)新的 Node 模塊 info.js,用于打印系統(tǒng)信息,代碼如下:
const os = require('os');
function printProgramInfo() {
console.log('當(dāng)前用戶', os.userInfo().username);
console.log('當(dāng)前進(jìn)程 ID', process.pid);
console.log('當(dāng)前腳本路徑', __filename);
}
module.exports = printProgramInfo;
這里我們導(dǎo)入了 Node 內(nèi)置模塊 os,并通過 os.userInfo() 查詢到了系統(tǒng)用戶名,接著通過 module.exports 導(dǎo)出了 printProgramInfo 函數(shù)。
然后創(chuàng)建第二個(gè) Node 模塊 datetime.js,用于返回當(dāng)前的時(shí)間,代碼如下:
function getCurrentTime() {
const time = new Date();
return time.toLocaleString();
}
exports.getCurrentTime = getCurrentTime;
上面的模塊中,我們選擇了通過 exports 導(dǎo)出 getCurrentTime 函數(shù)。
最后,我們?cè)?timer.js 中通過 require 導(dǎo)入剛才兩個(gè)模塊,并分別調(diào)用模塊中的函數(shù) printProgramInfo 和 getCurrentTime,代碼如下:
const printProgramInfo = require('./info');
const datetime = require('./datetime');
setTimeout(() => {
console.log('Hello World!');
}, 3000);
printProgramInfo();
console.log('當(dāng)前時(shí)間', datetime.getCurrentTime());
再運(yùn)行一下 timer.js,輸出內(nèi)容應(yīng)該與之前完全一致。
讀到這里,我想先恭喜你渡過了 Node.js 入門最難的一關(guān)!如果你已經(jīng)真正地理解了 Node 模塊機(jī)制,那么我相信接下來的學(xué)習(xí)會(huì)無比輕松哦。
命令行開發(fā):接受輸入?yún)?shù)
Node.js 作為可以在操作系統(tǒng)中直接運(yùn)行 JavaScript 代碼的平臺(tái),為前端開發(fā)者開啟了無限可能,其中就包括一系列用于實(shí)現(xiàn)前端自動(dòng)化工作流的命令行工具,例如 Grunt、Gulp 還有大名鼎鼎的 Webpack。
從這一步開始,我們將把 timer.js 改造成一個(gè)命令行應(yīng)用。具體地,我們希望 timer.js 可以通過命令行參數(shù)指定等待的時(shí)間(time 選項(xiàng))和最終輸出的信息(message 選項(xiàng)):
$ node timer.js --time 5 --message "Hello Tuture"
通過 process.argv 讀取命令行參數(shù)
之前在講全局對(duì)象 process 時(shí)提到一個(gè) argv 屬性,能夠獲取命令行參數(shù)的數(shù)組。創(chuàng)建一個(gè) args.js 文件,代碼如下:
console.log(process.argv);
然后運(yùn)行以下命令:
$ node args.js --time 5 --message "Hello Tuture"
輸出一個(gè)數(shù)組:
[
'/Users/mRc/.nvm/versions/node/v12.10.0/bin/node',
'/Users/mRc/Tutorials/nodejs-quickstart/args.js',
'--time',
'5',
'--message',
'Hello Tuture'
]
可以看到,process.argv 數(shù)組的第 0 個(gè)元素是 node 的實(shí)際路徑,第 1 個(gè)元素是 args.js 的路徑,后面則是輸入的所有參數(shù)。
實(shí)現(xiàn)命令行應(yīng)用
根據(jù)剛才的分析,我們可以非常簡單粗暴地獲取 process.argv 的第 3 個(gè)和第 5 個(gè)元素,分別可以得到 time 和 message 參數(shù)。于是修改 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('當(dāng)前時(shí)間', datetime.getCurrentTime());
提醒一下,setTimeout 中時(shí)間的單位是毫秒,而我們指定的時(shí)間參數(shù)單位是秒,因此要乘 1000。
運(yùn)行 timer.js,加上剛才說的所有參數(shù):
$ node timer.js --time 5 --message "Hello Tuture"
等待 5 秒鐘后,你就看到了 Hello Tuture 的提示文本!
不過很顯然,目前這個(gè)版本有很大的問題:輸入?yún)?shù)的格式是固定的,很不靈活,比如說調(diào)換 time 和 message 的輸入順序就會(huì)出錯(cuò),也不能檢查用戶是否輸入了指定的參數(shù),格式是否正確等等。如果要親自實(shí)現(xiàn)上面所說的功能,那可得花很大的力氣,說不定還會(huì)有不少 Bug。有沒有更好的方案呢?
npm:洪荒之力,都賜予你
從這一節(jié)開始,你將不再是一個(gè)人寫代碼。你的背后將擁有百萬名 JavaScript 開發(fā)者的支持,而這一切僅需要 npm 就可以實(shí)現(xiàn)。npm 包括:
- npm 命令行工具(安裝 node 時(shí)也會(huì)附帶安裝)
- npm 集中式依賴倉庫(registry),存放了其他 JavaScript 開發(fā)者分享的 npm 包
- npm 網(wǎng)站,可以搜索需要的 npm 包、管理 npm 帳戶等
npm 初探
我們首先打開終端(命令行),檢查一下 npm 命令是否可用:
$ npm -v
6.10.3
然后在當(dāng)前目錄(也就是剛才編輯的 timer.js 所在的文件夾)運(yùn)行以下命令,把當(dāng)前項(xiàng)目初始化為 npm 項(xiàng)目:
$ npm init
這時(shí)候 npm 會(huì)提一系列問題,你可以一路回車下去,也可以仔細(xì)回答,最終會(huì)創(chuàng)建一個(gè) package.json 文件。package.json 文件是一個(gè) npm 項(xiàng)目的核心,記錄了這個(gè)項(xiàng)目所有的關(guān)鍵信息,內(nèi)容如下:
{
"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 項(xiàng)目名稱、 version 版本號(hào)、description 描述、author 作者等等。不過這個(gè) scripts 字段你可能會(huì)比較困惑,我們會(huì)在下一節(jié)中詳細(xì)介紹。
安裝 npm 包
接下來我們將講解 npm 最最最常用的命令—— install。沒錯(cuò),毫不夸張地說,一個(gè) JavaScript 程序員用的最多的 npm 命令就是 npm install。
在安裝我們需要的 npm 包之前,我們需要去探索一下有哪些包可以為我們所用。通常,我們可以在 npm 官方網(wǎng)站 上進(jìn)行關(guān)鍵詞搜索(記得用英文哦),比如說我們搜 command line:
出來的第一個(gè)結(jié)果 commander 就很符合我們的需要,點(diǎn)進(jìn)去就是安裝的說明和使用文檔。我們還想要一個(gè)“加載中”的動(dòng)畫效果,提高用戶的使用體驗(yàn),試著搜一下 loading 關(guān)鍵詞:
第二個(gè)結(jié)果 ora 也符合我們的需要。那我們現(xiàn)在就安裝這兩個(gè) npm 包:
$ npm install commander ora
少許等待后,可以看到 package.json 多了一個(gè)非常重要的 dependencies 字段:
"dependencies": {
"commander": "^4.0.1",
"ora": "^4.0.3"
}
這個(gè)字段中就記錄了我們這個(gè)項(xiàng)目的直接依賴。與直接依賴相對(duì)的就是間接依賴,例如 commander 和 ora 的依賴,我們通常不用關(guān)心。所有的 npm 包(直接依賴和間接依賴)全部都存放在項(xiàng)目的 node_modules 目錄中。
提示
node_modules 通常有很多的文件,因此不會(huì)加入到 Git 版本控制系統(tǒng)中,你從網(wǎng)上下載的 npm 項(xiàng)目一般也只會(huì)有 package.json,這時(shí)候只需運(yùn)行
npm install(后面不跟任何內(nèi)容),就可以下載并安裝所有依賴了。
整個(gè) 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"
}
}
關(guān)于版本號(hào)
在軟件開發(fā)中,版本號(hào)是一個(gè)非常重要的概念,不同版本的軟件存在或大或小的差異。npm 采用了語義版本號(hào)(Semantic Versioning,簡稱 semver),具體規(guī)定如下:
- 版本格式為:主版本號(hào).次版本號(hào).修訂號(hào)
- 主版本號(hào)的改變意味著不兼容的 API 修改
- 次版本號(hào)的改變意味著做了向下兼容的功能性新增
- 修訂號(hào)的改變意味著做了向下兼容的問題修正
提示
向下兼容的簡單理解就是功能只增不減。
因此在 package.json 的 dependencies 字段中,可以通過以下方式指定版本:
-
精確版本:例如
1.0.0,一定只會(huì)安裝版本為1.0.0的依賴 -
鎖定主版本和次版本:可以寫成
1.0、1.0.x或~1.0.0,那么可能會(huì)安裝例如1.0.8的依賴 -
僅鎖定主版本:可以寫成
1、1.x或^1.0.0(npm install默認(rèn)采用的形式),那么可能會(huì)安裝例如1.1.0的依賴 -
最新版本:可以寫成
*或x,那么直接安裝最新版本(不推薦)
你也許注意到了 npm 還創(chuàng)建了一個(gè) package-lock.json,這個(gè)文件就是用來鎖定全部直接依賴和間接依賴的精確版本號(hào),或者說提供了關(guān)于 node_modules 目錄的精確描述,從而確保在這個(gè)項(xiàng)目中開發(fā)的所有人都能有完全一致的 npm 依賴。
站在巨人的肩膀上
我們?cè)诖笾伦x了一下 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>', '等待時(shí)間 (秒)', 3)
.option('-m, --message <string>', '要輸出的信息', 'Hello World')
.parse(process.argv);
setTimeout(() => {
spinner.stop();
console.log(program.message);
}, program.time * 1000);
printProgramInfo();
console.log('當(dāng)前時(shí)間', datetime.getCurrentTime());
const spinner = ora('正在加載中,請(qǐng)稍后 ...').start();
這次,我們?cè)俅芜\(yùn)行 timer.js:
$ node timer.js --message "洪荒之力!" --time 5
轉(zhuǎn)起來了!
嘗鮮 npm scripts
在本教程的最后一節(jié)中,我們將簡單地介紹一下 npm scripts,也就是 npm 腳本。之前在 package.json 中提到,有個(gè)字段叫 scripts,這個(gè)字段就定義了全部的 npm scripts。我們發(fā)現(xiàn)在用 npm init 時(shí)創(chuàng)建的 package.json 文件默認(rèn)就添加了一個(gè) test 腳本:
"test": "echo \"Error: no test specified\" && exit 1"
那一串命令就是 test 腳本將要執(zhí)行的內(nèi)容,我們可以通過 npm test 命令執(zhí)行該腳本:
$ 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.
在初步體驗(yàn)了 npm scripts 之后,我們有必要了解一下 npm scripts 分為兩大類:
-
預(yù)定義腳本:例如
test、start、install、publish等等,直接通過npm <scriptName>運(yùn)行,例如npm test,所有預(yù)定義的腳本可查看文檔 -
自定義腳本:除了以上自帶腳本的其他腳本,需要通過
npm run <scriptName>運(yùn)行,例如npm run custom
現(xiàn)在就讓我們開始為 timer 項(xiàng)目添加兩個(gè) npm scripts,分別是 start 和 lint。第一個(gè)是預(yù)定義的,用于啟動(dòng)我們的 timer.js;第二個(gè)是靜態(tài)代碼檢查,用于在開發(fā)時(shí)檢查我們的代碼。首先安裝 ESLint npm 包:
$ npm install eslint --save-dev
$ # 或者
$ npm install eslint -D
注意到我們加了一個(gè) -D 或 --save-dev 選項(xiàng),代表 eslint 是一個(gè)開發(fā)依賴,在實(shí)際項(xiàng)目發(fā)布或部署時(shí)不需要用到。npm 會(huì)把所有開發(fā)依賴添加到 devDependencies 字段中。然后分別添加 start 和 lint 腳本,代碼如下:
{
"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 的使用需要一個(gè)配置文件,創(chuàng)建 .eslintrc.js 文件(注意最前面有一個(gè)點(diǎn)),代碼如下:
module.exports = {
"env": {
"es6": true,
"node": true,
},
"extends": "eslint:recommended",
};
運(yùn)行 npm start,可以看到成功地運(yùn)行了我們的 timer.js 腳本;而運(yùn)行 npm run lint,沒有輸出任何結(jié)果(代表靜態(tài)檢查通過)。
npm scripts 看上去平淡無奇,但是卻能為項(xiàng)目開發(fā)提供非常便利的工作流。例如,之前構(gòu)建一個(gè)項(xiàng)目需要非常復(fù)雜的命令,但是如果你實(shí)現(xiàn)了一個(gè) build npm 腳本,那么當(dāng)你的同事拿到這份代碼時(shí),只需簡單地執(zhí)行 npm run build 就可以開始構(gòu)建,而無需關(guān)心背后的技術(shù)細(xì)節(jié)。在后續(xù)的 Node.js 或是前端學(xué)習(xí)中,我們會(huì)在實(shí)際項(xiàng)目中使用各種 npm scripts 來定義我們的工作流,大家慢慢就會(huì)領(lǐng)會(huì)到它的強(qiáng)大了。
下次再見:監(jiān)聽 exit 事件
在這篇教程的最后一節(jié)中,我們將讓你簡單地感受 Node 的事件機(jī)制。Node 的事件機(jī)制是比較復(fù)雜的,足夠講半本書,但這篇教程希望能通過一個(gè)非常簡單的實(shí)例,讓你對(duì) Node 事件有個(gè)初步的了解。
提示
如果你有過在網(wǎng)頁(或其他用戶界面)開發(fā)中編寫事件處理(例如鼠標(biāo)點(diǎn)擊)的經(jīng)驗(yàn),那么你一定會(huì)覺得 Node 中處理事件的方式似曾相識(shí)而又符合直覺。
我們?cè)谇懊婧唵蔚靥崃艘幌禄卣{(diào)函數(shù)。實(shí)際上,回調(diào)函數(shù)和事件機(jī)制共同組成了 Node 的異步世界。具體而言,Node 中的事件都是通過 events 核心模塊中的 EventEmitter 這個(gè)類實(shí)現(xiàn)的。EventEmitter 包括兩個(gè)最關(guān)鍵的方法:
-
on:用來監(jiān)聽事件的發(fā)生 -
emit:用來觸發(fā)新的事件
請(qǐng)看下面這個(gè)代碼片段:
const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();
// 監(jiān)聽 connect 事件,注冊(cè)回調(diào)函數(shù)
emitter.on('connect', function (username) {
console.log(username + '已連接');
});
// 觸發(fā) connect 事件,并且加上一個(gè)參數(shù)(即上面的 username)
emitter.emit('connect', '一只圖雀');
運(yùn)行上面的代碼,就會(huì)輸出以下內(nèi)容:
一只圖雀已連接
可以說,Node 中很多對(duì)象都繼承自 EventEmitter,包括我們熟悉的 process 全局對(duì)象。在之前的 timer.js 腳本中,我們監(jiān)聽 exit 事件(即 Node 進(jìn)程結(jié)束),并添加一個(gè)自定義的回調(diào)函數(shù)打印“下次再見”的信息:
const program = require('commander');
const ora = require('ora');
const printProgramInfo = require('./info');
const datetime = require('./datetime');
program
.option('-t, --time <number>', '等待時(shí)間 (秒)', 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('當(dāng)前時(shí)間', datetime.getCurrentTime());
const spinner = ora('正在加載中,請(qǐng)稍后 ...').start();
運(yùn)行后,會(huì)在程序退出后打印“下次再見~”的字符串。你可能會(huì)問,為啥不能在 setTimeout 的回調(diào)函數(shù)中添加程序退出的邏輯呢?因?yàn)槌苏_\(yùn)行結(jié)束(也就是等待了指定的時(shí)間),我們的程序很有可能會(huì)因?yàn)槠渌蛲顺觯ɡ鐠伋霎惓#蛘哂?process.exit 強(qiáng)制退出),這時(shí)候通過監(jiān)聽 exit 事件,就可以在確保所有情況下都能執(zhí)行 exit 事件的回調(diào)函數(shù)。如果你覺得還是不能理解的話,可以看下面這張示意圖:
提示
process對(duì)象還支持其他常用的事件,例如SIGINT(用戶按 Ctrl+C 時(shí)觸發(fā))等等,可參考這篇文檔。
這篇 Node.js 快速入門教程到這里就結(jié)束了,希望能夠成為你進(jìn)一步探索 Node.js 或是前端開發(fā)的基石。exit 事件已經(jīng)觸發(fā),那我們也下次再見啦~
想要學(xué)習(xí)更多精彩的實(shí)戰(zhàn)技術(shù)教程?來圖雀社區(qū)逛逛吧。
