1. 程式人生 > 實用技巧 >Node.js 概述

Node.js 概述

Node.js 概述

1. 簡介

Node是JavaScript語言的伺服器執行環境。

所謂“執行環境”有兩層意思:首先,JavaScript語言通過Node在伺服器執行,在這個意義上,Node有點像JavaScript虛擬機器;其次,Node提供大量工具庫,使得JavaScript語言與作業系統互動(比如讀寫檔案、新建子程序),在這個意義上,Node又是JavaScript的工具庫。

Node內部採用Google公司的V8引擎,作為JavaScript語言直譯器;通過自行開發的libuv庫,呼叫作業系統資源。

1.1 安裝與更新

訪問官方網站nodejs.org或者github.com/nodesource/distributions

,檢視Node的最新版本和安裝方法。

官方網站提供編譯好的二進位制包,可以把它們解壓到/usr/local目錄下面。

$ tar -xf node-someversion.tgz

然後,建立符號連結,把它們加到$PATH變數裡面的路徑。

$ ln -s /usr/local/node/bin/node /usr/local/bin/node
$ ln -s /usr/local/node/bin/npm /usr/local/bin/npm

下面是Ubuntu和Debian下面安裝Deb軟體包的安裝方法。

$ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
$ sudo apt-get install -y nodejs $ apt-get install nodejs

安裝完成以後,執行下面的命令,檢視是否能正常執行。

$ node --version
# 或者
$ node -v

更新node.js版本,可以通過node.js的n模組完成。

$ sudo npm install n -g
$ sudo n stable

上面程式碼通過n模組,將node.js更新為最新發布的穩定版。

n模組也可以指定安裝特定版本的node。

$ sudo n 0.10.21

1.2 版本管理工具nvm

如果想在同一臺機器,同時安裝多個版本的node.js,就需要用到版本管理工具nvm。

$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh

安裝以後,nvm的執行指令碼,每次使用前都要啟用,建議將其加入~/.bashrc檔案(假定使用Bash)。啟用後,就可以安裝指定版本的Node。

# 安裝最新版本
$ nvm install node

# 安裝指定版本
$ nvm install 0.12.1

# 使用已安裝的最新版本
$ nvm use node

# 使用指定版本的node
$ nvm use 0.12

nvm也允許進入指定版本的REPL環境。

$ nvm run 0.12

如果在專案根目錄下新建一個.nvmrc檔案,將版本號寫入其中,就只輸入nvm use命令即可,不再需要附加版本號。

下面是其他經常用到的命令。

# 檢視本地安裝的所有版本
$ nvm ls

# 檢視伺服器上所有可供安裝的版本。
$ nvm ls-remote

# 退出已經啟用的nvm,使用deactivate命令。
$ nvm deactivate

1.3 基本用法

安裝完成後,執行node.js程式,就是使用node命令讀取JavaScript指令碼。

當前目錄的demo.js指令碼檔案,可以這樣執行。

$ node demo
# 或者
$ node demo.js

使用-e引數,可以執行程式碼字串。

$ node -e 'console.log("Hello World")'
Hello World

1.4 REPL環境

在命令列鍵入node命令,後面沒有檔名,就進入一個Node.js的REPL環境(Read–eval–print loop,”讀取-求值-輸出”迴圈),可以直接執行各種JavaScript命令。

$ node
> 1+1
2
>

如果使用引數 –use_strict,則REPL將在嚴格模式下執行。

$ node --use_strict

REPL是Node.js與使用者互動的shell,各種基本的shell功能都可以在裡面使用,比如使用上下方向鍵遍歷曾經使用過的命令。

特殊變數下劃線(_)表示上一個命令的返回結果。

> 1 + 1
2
> _ + 1
3

在REPL中,如果執行一個表示式,會直接在命令列返回結果。如果執行一條語句,就不會有任何輸出,因為語句沒有返回值。

> x = 1
1
> var x = 1

上面程式碼的第二條命令,沒有顯示任何結果。因為這是一條語句,不是表示式,所以沒有返回值。

1.5 非同步操作

Node採用V8引擎處理JavaScript指令碼,最大特點就是單執行緒執行,一次只能執行一個任務。這導致Node大量採用非同步操作(asynchronous operation),即任務不是馬上執行,而是插在任務佇列的尾部,等到前面的任務執行完後再執行。

由於這種特性,某一個任務的後續操作,往往採用回撥函式(callback)的形式進行定義。

var isTrue = function(value, callback) {
  if (value === true) {
    callback(null, "Value was true.");
  }
  else {
    callback(new Error("Value is not true!"));
  }
}

上面程式碼就把進一步的處理,交給回撥函式callback。

Node約定,如果某個函式需要回調函式作為引數,則回撥函式是最後一個引數。另外,回撥函式本身的第一個引數,約定為上一步傳入的錯誤物件。

var callback = function (error, value) {
  if (error) {
    return console.log(error);
  }
  console.log(value);
}

上面程式碼中,callback的第一個引數是Error物件,第二個引數才是真正的資料引數。這是因為回撥函式主要用於非同步操作,當回撥函式執行時,前期的操作早結束了,錯誤的執行棧早就不存在了,傳統的錯誤捕捉機制try…catch對於非同步操作行不通,所以只能把錯誤交給回撥函式處理。

try {
  db.User.get(userId, function(err, user) {
    if(err) {
      throw err
    }
    // ...
  })
} catch(e) {
  console.log(‘Oh no!’);
}

上面程式碼中,db.User.get方法是一個非同步操作,等到丟擲錯誤時,可能它所在的try…catch程式碼塊早就執行結束了,這會導致錯誤無法被捕捉。所以,Node統一規定,一旦非同步操作發生錯誤,就把錯誤物件傳遞到回撥函式。

如果沒有發生錯誤,回撥函式的第一個引數就傳入null。這種寫法有一個很大的好處,就是說只要判斷回撥函式的第一個引數,就知道有沒有出錯,如果不是null,就肯定出錯了。另外,這樣還可以層層傳遞錯誤。

if(err) {
  // 除了放過No Permission錯誤意外,其他錯誤傳給下一個回撥函式
  if(!err.noPermission) {
    return next(err);
  }
}

1.6 全域性物件和全域性變數

Node提供以下幾個全域性物件,它們是所有模組都可以呼叫的。

  • global:表示Node所在的全域性環境,類似於瀏覽器的window物件。需要注意的是,如果在瀏覽器中宣告一個全域性變數,實際上是聲明瞭一個全域性物件的屬性,比如var x = 1等同於設定window.x = 1,但是Node不是這樣,至少在模組中不是這樣(REPL環境的行為與瀏覽器一致)。在模組檔案中,宣告var x = 1,該變數不是global物件的屬性,global.x等於undefined。這是因為模組的全域性變數都是該模組私有的,其他模組無法取到。

  • process:該物件表示Node所處的當前程序,允許開發者與該程序互動。

  • console:指向Node內建的console模組,提供命令列環境中的標準輸入、標準輸出功能。

Node還提供一些全域性函式。

  • setTimeout():用於在指定毫秒之後,執行回撥函式。實際的呼叫間隔,還取決於系統因素。間隔的毫秒數在1毫秒到2,147,483,647毫秒(約24.8天)之間。如果超過這個範圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。
  • clearTimeout():用於終止一個setTimeout方法新建的定時器。
  • setInterval():用於每隔一定毫秒呼叫回撥函式。由於系統因素,可能無法保證每次呼叫之間正好間隔指定的毫秒數,但只會多於這個間隔,而不會少於它。指定的毫秒數必須是1到2,147,483,647(大約24.8天)之間的整數,如果超過這個範圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。
  • clearInterval():終止一個用setInterval方法新建的定時器。
  • require():用於載入模組。
  • Buffer():用於操作二進位制資料。

Node提供兩個全域性變數,都以兩個下劃線開頭。

  • __filename:指向當前執行的指令碼檔名。
  • __dirname:指向當前執行的指令碼所在的目錄。

除此之外,還有一些物件實際上是模組內部的區域性變數,指向的物件根據模組不同而不同,但是所有模組都適用,可以看作是偽全域性變數,主要為module, module.exports, exports等。

2. 模組化結構

2.1 概述

Node.js採用模組化結構,按照CommonJS規範定義和使用模組。模組與檔案是一一對應關係,即載入一個模組,實際上就是載入對應的一個模組檔案。

require命令用於指定載入模組,載入時可以省略指令碼檔案的字尾名。

var circle = require('./circle.js');
// 或者
var circle = require('./circle');

require方法的引數是模組檔案的名字。它分成兩種情況,第一種情況是引數中含有檔案路徑(比如上例),這時路徑是相對於當前指令碼所在的目錄,第二種情況是引數中不含有檔案路徑,這時Node到模組的安裝目錄,去尋找已安裝的模組(比如下例)。

var bar = require('bar');

有時候,一個模組本身就是一個目錄,目錄中包含多個檔案。這時候,Node在package.json檔案中,尋找main屬性所指明的模組入口檔案。

{
  "name" : "bar",
  "main" : "./lib/bar.js"
}

上面程式碼中,模組的啟動檔案為lib子目錄下的bar.js。當使用require('bar')命令載入該模組時,實際上載入的是./node_modules/bar/lib/bar.js檔案。下面寫法會起到同樣效果。

var bar = require('bar/lib/bar.js')

如果模組目錄中沒有package.json檔案,node.js會嘗試在模組目錄中尋找index.js或index.node檔案進行載入。

模組一旦被載入以後,就會被系統快取。如果第二次還載入該模組,則會返回快取中的版本,這意味著模組實際上只會執行一次。如果希望模組執行多次,則可以讓模組返回一個函式,然後多次呼叫該函式。

2.2 核心模組

如果只是在伺服器執行JavaScript程式碼,用處並不大,因為伺服器指令碼語言已經有很多種了。Node.js的用處在於,它本身還提供了一系列功能模組,與作業系統互動。這些核心的功能模組,不用安裝就可以使用,下面是它們的清單。

  • http:提供HTTP伺服器功能。
  • url:解析URL。
  • fs:與檔案系統互動。
  • querystring:解析URL的查詢字串。
  • child_process:新建子程序。
  • util:提供一系列實用小工具。
  • path:處理檔案路徑。
  • crypto:提供加密和解密功能,基本上是對OpenSSL的包裝。

上面這些核心模組,原始碼都在Node的lib子目錄中。為了提高執行速度,它們安裝時都會被編譯成二進位制檔案。

核心模組總是最優先載入的。如果你自己寫了一個HTTP模組,require('http')載入的還是核心模組。

2.3 自定義模組

Node模組採用CommonJS規範。只要符合這個規範,就可以自定義模組。

下面是一個最簡單的模組,假定新建一個foo.js檔案,寫入以下內容。

// foo.js

module.exports = function(x) {
    console.log(x);
};

上面程式碼就是一個模組,它通過module.exports變數,對外輸出一個方法。

這個模組的使用方法如下。

// index.js

var m = require('./foo');

m("這是自定義模組");

上面程式碼通過require命令載入模組檔案foo.js(字尾名省略),將模組的對外介面輸出到變數m,然後呼叫m。這時,在命令列下執行index.js,螢幕上就會輸出“這是自定義模組”。

$ node index
這是自定義模組

module變數是整個模組檔案的頂層變數,它的exports屬性就是模組向外輸出的介面。如果直接輸出一個函式(就像上面的foo.js),那麼呼叫模組就是呼叫一個函式。但是,模組也可以輸出一個物件。下面對foo.js進行改寫。

// foo.js

var out = new Object();

function p(string) {
  console.log(string);
}

out.print = p;

module.exports = out;

上面的程式碼表示模組輸出out物件,該物件有一個print屬性,指向一個函式。下面是這個模組的使用方法。

// index.js

var m = require('./foo');

m.print("這是自定義模組");

上面程式碼表示,由於具體的方法定義在模組的print屬性上,所以必須顯式呼叫print屬性。

3. 異常處理

Node是單執行緒執行環境,一旦丟擲的異常沒有被捕獲,就會引起整個程序的崩潰。所以,Node的異常處理對於保證系統的穩定執行非常重要。

一般來說,Node有三種方法,傳播一個錯誤。

  • 使用throw語句丟擲一個錯誤物件,即丟擲異常。
  • 將錯誤物件傳遞給回撥函式,由回撥函式負責發出錯誤。
  • 通過EventEmitter介面,發出一個error事件。

3.1 try...catch結構

最常用的捕獲異常的方式,就是使用try…catch結構。但是,這個結構無法捕獲非同步執行的程式碼丟擲的異常。

try {
  process.nextTick(function () {
    throw new Error("error");
  });
} catch (err) {
  //can not catch it
  console.log(err);
}

try {
  setTimeout(function(){
    throw new Error("error");
  },1)
} catch (err) {
  //can not catch it
  console.log(err);
}

上面程式碼分別用process.nextTick和setTimeout方法,在下一輪事件迴圈丟擲兩個異常,代表非同步操作丟擲的錯誤。它們都無法被catch程式碼塊捕獲,因為catch程式碼塊所在的那部分已經執行結束了。

一種解決方法是將錯誤捕獲程式碼,也放到非同步執行。

function async(cb, err) {
  setTimeout(function() {
    try {
      if (true)
        throw new Error("woops!");
      else
        cb("done");
    } catch(e) {
      err(e);
    }
  }, 2000)
}

async(function(res) {
  console.log("received:", res);
}, function(err) {
  console.log("Error: async threw an exception:", err);
});
// Error: async threw an exception: Error: woops!

上面程式碼中,async函式非同步丟擲的錯誤,可以同樣部署在非同步的catch程式碼塊捕獲。

這兩種處理方法都不太理想。一般來說,Node只在很少場合才用try/catch語句,比如使用JSON.parse解析JSON文字。

3.2 回撥函式

Node採用的方法,是將錯誤物件作為第一個引數,傳入回撥函式。這樣就避免了捕獲程式碼與發生錯誤的程式碼不在同一個時間段的問題。

fs.readFile('/foo.txt', function(err, data) {
  if (err !== null) throw err;
  console.log(data);
});

上面程式碼表示,讀取檔案foo.txt是一個非同步操作,它的回撥函式有兩個引數,第一個是錯誤物件,第二個是讀取到的檔案資料。如果第一個引數不是null,就意味著發生錯誤,後面程式碼也就不再執行了。

下面是一個完整的例子。

function async2(continuation) {
  setTimeout(function() {
    try {
      var res = 42;
      if (true)
        throw new Error("woops!");
      else
        continuation(null, res); // pass 'null' for error
    } catch(e) {
      continuation(e, null);
    }
  }, 2000);
}

async2(function(err, res) {
  if (err)
    console.log("Error: (cps) failed:", err);
  else
    console.log("(cps) received:", res);
});
// Error: (cps) failed: woops!

上面程式碼中,async2函式的回撥函式的第一個引數就是一個錯誤物件,這是為了處理非同步操作丟擲的錯誤。

3.3 EventEmitter介面的error事件

發生錯誤的時候,也可以用EventEmitter介面丟擲error事件。

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.emit('error', new Error('something bad happened'));

使用上面的程式碼必須小心,因為如果沒有對error事件部署監聽函式,會導致整個應用程式崩潰。所以,一般總是必須同時部署下面的程式碼。

emitter.on('error', function(err) {
  console.error('出錯:' + err.message);
});

3.4 uncaughtException事件

當一個異常未被捕獲,就會觸發uncaughtException事件,可以對這個事件註冊回撥函式,從而捕獲異常。

var logger = require('tracer').console();
process.on('uncaughtException', function(err) {
  console.error('Error caught in uncaughtException event:', err);
});

try {
  setTimeout(function(){
    throw new Error("error");
  },1);
} catch (err) {
  //can not catch it
  console.log(err);
}

只要給uncaughtException配置了回撥,Node程序不會異常退出,但異常發生的上下文已經丟失,無法給出異常發生的詳細資訊。而且,異常可能導致Node不能正常進行記憶體回收,出現記憶體洩露。所以,當uncaughtException觸發後,最好記錄錯誤日誌,然後結束Node程序。

process.on('uncaughtException', function(err) {
  logger.log(err);
  process.exit(1);
});

3.5 unhandledRejection事件

iojs有一個unhandledRejection事件,用來監聽沒有捕獲的Promise物件的rejected狀態。

var promise = new Promise(function(resolve, reject) {
  reject(new Error("Broken."));
});

promise.then(function(result) {
  console.log(result);
})

上面程式碼中,promise的狀態變為rejected,並且丟擲一個錯誤。但是,不會有任何反應,因為沒有設定任何處理函式。

只要監聽unhandledRejection事件,就能解決這個問題。

process.on('unhandledRejection', function (err, p) {
  console.error(err.stack);
})

需要注意的是,unhandledRejection事件的監聽函式有兩個引數,第一個是錯誤物件,第二個是產生錯誤的promise物件。這可以提供很多有用的資訊。

var http = require('http');

http.createServer(function (req, res) {
  var promise = new Promise(function(resolve, reject) {
    reject(new Error("Broken."))
  })

  promise.info = {url: req.url}
}).listen(8080)

process.on('unhandledRejection', function (err, p) {
  if (p.info && p.info.url) {
    console.log('Error in URL', p.info.url)
  }
  console.error(err.stack)
})

上面程式碼會在出錯時,輸出使用者請求的網址。

Error in URL /testurl
Error: Broken.
  at /Users/mikeal/tmp/test.js:9:14
  at Server.<anonymous> (/Users/mikeal/tmp/test.js:4:17)
  at emitTwo (events.js:87:13)
  at Server.emit (events.js:169:7)
  at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:471:12)
  at HTTPParser.parserOnHeadersComplete (_http_common.js:88:23)
  at Socket.socketOnData (_http_server.js:322:22)
  at emitOne (events.js:77:13)
  at Socket.emit (events.js:166:7)
  at readableAddChunk (_stream_readable.js:145:16)

4. 命令列指令碼

node指令碼可以作為命令列指令碼使用。

$ node foo.js

上面程式碼執行了foo.js指令碼檔案。

foo.js檔案的第一行,如果加入瞭解釋器的位置,就可以將其作為命令列工具直接呼叫。

#!/usr/bin/env node

呼叫前,需更改檔案的執行許可權。

$ chmod u+x foo.js
$ ./foo.js arg1 arg2 ...

作為命令列指令碼時,console.log用於輸出內容到標準輸出,process.stdin用於讀取標準輸入,child_process.exec()用於執行一個shell命令。

5. 參考連結

轉自:(阮一峰https://javascript.ruanyifeng.com/nodejs/basic.html#toc1