一杯茶的時間,上手 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 事件已經觸發,那咱們也下次再見啦~

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

相關文章
相關標籤/搜索