一杯茶的時間,上手 Node.js
- 2020 年 4 月 7 日
- 筆記

Node.js 太火了,火到幾乎所有前端工程師都想學,幾乎所有後端工程師也想學。一說到 Node.js,我們馬上就會想到「異步」、「事件驅動」、「非阻塞」、「性能優良」這幾個特點,但是你真的理解這些詞的含義嗎?這篇教程將帶你快速入門 Node.js,為後續的前端學習或是 Node.js 進階打下堅實的基礎。
此教程屬於Node.js 後端工程師學習路線[1]的一部分,點擊可查看全部內容。
起步
什麼是 Node?
簡單地說,Node(或者說 Node.js,兩者是等價的)是 JavaScript 的一種運行環境。在此之前,我們知道 JavaScript 都是在瀏覽器中執行的,用於給網頁添加各種動態效果,那麼可以說瀏覽器也是 JavaScript 的運行環境。那麼這兩個運行環境有哪些差異呢?請看下圖:

兩個運行環境共同包含了 ECMAScript,也就是剝離了所有運行環境的 JavaScript 語言標準本身。現在 ECMAScript 的發展速度非常驚人,幾乎能夠做到每年發展一個版本。
提示 ECMAScript 和 JavaScript 的關係是,前者是後者的規格,後者是前者的一種實現。在日常場合,這兩個詞是可以互換的。更多背景知識可參考阮一峰的《JavaScript語言的歷史》[2]。
另一方面,瀏覽器端 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 全局對象可以分為四類:
1.瀏覽器專屬,例如 window
、alert
等等;2.Node 專屬,例如 process
、Buffer
、__dirname
、__filename
等等;3.瀏覽器和 Node 共有,但是實現方式不同,例如 console
(第一節中已提到)、setTimeout
、setInterval
等;4.瀏覽器和 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 語言定義本身並沒有模塊化的機制,構建複雜應用也沒有統一的接口標準。人們通常使用一系列的 <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)規範[3],在瀏覽器中使用較為普遍,最經典的實現包括 RequireJS[4];2.CommonJS 規範[5],致力於為 JavaScript 生態圈提供統一的接口 API,Node.js 所實現的正是這一模塊標準。
提示 ECMAScript 2015(也就是大家常說的 ES6)標準為 JavaScript 語言引入了全新的模塊機制(稱為 ES 模塊,全稱 ECMAScript Modules),並提供了
import
和export
關鍵詞,如果感興趣可參考這篇文章[6]。但是截止目前,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 模塊,其參數接受一個字符串代表模塊的名稱或路徑,通常被稱為模塊標識符。具體有以下三種形式:
•直接寫模塊名稱,通常是核心模塊或第三方文件模塊,例如 os
、express
等•模塊的相對路徑,指向項目中其他 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 中的解構賦值[7],那麼可以用更優雅的方式獲取 add
函數:
const { add } = require('./myModule');
module
通過 require
和 exports
,我們已經知道了如何導入、導出 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
字段)•path
和 filename
:模塊所在路徑和文件名,沒啥好說的•exports
:模塊所導出的內容,實際上之前的 exports
對象是指向 module.exports
的引用。例如對於 myModule.js,剛才我們導出了 add
函數,因此出現在了這個 exports
字段裏面;而 main.js 沒有導出任何內容,因此 exports
字段為空•parent
和 children
:用於記錄模塊之間的導入關係,例如 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
本質上是指向 module
的 exports
屬性的引用,直接對 exports
賦值只會改變 exports
,對 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
設置 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
導入剛才兩個模塊,並分別調用模塊中的函數 printProgramInfo
和 getCurrentTime
,代碼如下:
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 代碼的平台,為前端開發者開啟了無限可能,其中就包括一系列用於實現前端自動化工作流的命令行工具,例如 Grunt[8]、Gulp[9] 還有大名鼎鼎的 Webpack[10]。
從這一步開始,我們將把 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 個元素,分別可以得到 time
和 message
參數。於是修改 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 的提示文本!
不過很顯然,目前這個版本有很大的問題:輸入參數的格式是固定的,很不靈活,比如說調換 time
和 message
的輸入順序就會出錯,也不能檢查用戶是否輸入了指定的參數,格式是否正確等等。如果要親自實現上面所說的功能,那可得花很大的力氣,說不定還會有不少 Bug。有沒有更好的方案呢?
npm:洪荒之力,都賜予你
從這一節開始,你將不再是一個人寫代碼。你的背後將擁有百萬名 JavaScript 開發者的支持,而這一切僅需要 npm 就可以實現。npm 包括:
•npm 命令行工具(安裝 node 時也會附帶安裝)•npm 集中式依賴倉庫(registry),存放了其他 JavaScript 開發者分享的 npm 包•npm 網站[11],可以搜索需要的 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 官方網站[12] 上進行關鍵詞搜索(記得用英文哦),比如說我們搜 command line:

出來的第一個結果 commander 就很符合我們的需要,點進去就是安裝的說明和使用文檔[13]。我們還想要一個「加載中」的動畫效果,提高用戶的使用體驗,試着搜一下 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[14]),具體規定如下:
•版本格式為:主版本號.次版本號.修訂號•主版本號的改變意味着不兼容的 API 修改•次版本號的改變意味着做了向下兼容的功能性新增•修訂號的改變意味着做了向下兼容的問題修正
提示 向下兼容的簡單理解就是功能只增不減。
因此在 package.json 的 dependencies
字段中,可以通過以下方式指定版本:
•精確版本:例如 1.0.0
,一定只會安裝版本為 1.0.0
的依賴•鎖定主版本和次版本:可以寫成 1.0
、1.0.x
或 ~1.0.0
( npm install
默認採用的形式),那麼可能會安裝例如 1.0.8
的依賴•僅鎖定主版本:可以寫成 1
、1.x
或 1^1.0.0
,那麼可能會安裝例如 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 > [email protected] 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 分為兩大類:
•預定義腳本:例如 test
、start
、install
、publish
等等,直接通過 npm <scriptName>
運行,例如 npm test
,所有預定義的腳本可查看文檔[15]•自定義腳本:除了以上自帶腳本的其他腳本,需要通過 npm run <scriptName>
運行,例如 npm run custom
現在就讓我們開始為 timer 項目添加兩個 npm scripts,分別是 start
和 lint
。第一個是預定義的,用於啟動我們的 timer.js;第二個是靜態代碼檢查,用於在開發時檢查我們的代碼。首先安裝 ESLint[16] npm 包:
$ npm install eslint --save-dev $ # 或者 $ npm install eslint -D
注意到我們加了一個 -D
或 --save-dev
選項,代表 eslint
是一個開發依賴,在實際項目發佈或部署時不需要用到。npm 會把所有開發依賴添加到 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 的使用需要一個配置文件,創建 .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 時觸發)等等,可參考這篇文檔[17]。
這篇 Node.js 快速入門教程到這裡就結束了,希望能夠成為你進一步探索 Node.js 或是前端開發的基石。exit 事件已經觸發,那我們也下次再見啦~
想要學習更多精彩的實戰技術教程?來圖雀社區[18]逛逛吧。
References
[1]
Node.js 後端工程師學習路線: https://github.com/tuture-dev/nodejs-roadmap [2]
《JavaScript語言的歷史》: http://javascript.ruanyifeng.com/introduction/history.html [3]
AMD(Asynchronous Module Definition)規範: https://github.com/amdjs/amdjs-api/blob/master/AMD.md [4]
RequireJS: https://requirejs.org/ [5]
CommonJS 規範: http://wiki.commonjs.org/wiki/CommonJS [6]
這篇文章: https://zhuanlan.zhihu.com/p/36358695 [7]
解構賦值: http://es6.ruanyifeng.com/#docs/destructuring [8]
Grunt: http://gruntjs.com/ [9]
Gulp: https://gulpjs.com/ [10]
Webpack: https://webpack.js.org/ [11]
網站: https://npmjs.com [12]
npm 官方網站: https://npmjs.com [13]
使用文檔: https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md [14]
semver: https://semver.org/lang/zh-CN/ [15]
文檔: https://docs.npmjs.com/misc/scripts#description [16]
ESLint: http://eslint.cn/ [17]
文檔: https://javascript.ruanyifeng.com/nodejs/process.html#toc10 [18]
圖雀社區: https://tuture.co/?utm_source=juejin_zhuanlan