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)本身。現(xiàn)在 ECMAScript 的發(fā)展速度非常驚人喜爷,幾乎能夠做到每年發(fā)展一個(gè)版本膜楷。
提示
ECMAScript 和 JavaScript 的關(guān)系是,前者是后者的規(guī)格贞奋,后者是前者的一種實(shí)現(xiàn)赌厅。在日常場(chǎng)合,這兩個(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))胚股,通惩ピ伲可以理解為交互式解釋器,你可以輸入任何表達(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ù)【湔迹可能有點(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)槠渌蛲顺觯ɡ鐠伋霎惓EЧ妫蛘哂?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ū)逛逛吧耗啦。