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 =https://www.cnblogs.com/luoluowanfeng/p/== 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. 參考鏈接
- Cody Lindley, Package Managers: An Introductory Guide For The Uninitiated Front-End Developer
- Stack Overflow, What is Node.js?
- Andrew Burgess, Using Node’s Event Module
- James Halliday, task automation with npm run- Romain Prieto, Working on related Node.js modules locally
- Alon Salant, Export This: Interface Design Patterns for Node.js Modules
- Node.js Manual & Documentation, Modules
- Brent Ertz, Creating and publishing a node.js module
- Fred K Schott, “npm install –save” No Longer Using Tildes
- Satans17, Node穩定性的研究心得
- Axel Rauschmayer, Write your shell scripts in JavaScript, via Node.js
轉自:(阮一峰 https://javascript.ruanyifeng.com/nodejs/basic.html#toc1)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/121188.html
標籤:JavaScript
