Node.js是目前很是火熱的技術,可是它的誕生經歷卻很奇特。javascript
衆所周知,在Netscape設計出JavaScript後的短短几個月,JavaScript事實上已是前端開發的惟一標準。css
後來,微軟經過IE擊敗了Netscape後一統桌面,結果幾年時間,瀏覽器毫無進步。(2001年推出的古老的IE 6到今天仍然有人在使用!)html
沒有競爭就沒有發展。微軟認爲IE6瀏覽器已經很是完善,幾乎沒有可改進之處,而後解散了IE6開發團隊!而Google卻認爲支持現代Web應用的新一代瀏覽器纔剛剛起步,尤爲是瀏覽器負責運行JavaScript的引擎性能還可提高10倍。前端
先是Mozilla藉助已壯烈犧牲的Netscape遺產在2002年推出了Firefox瀏覽器,緊接着Apple於2003年在開源的KHTML瀏覽器的基礎上推出了WebKit內核的Safari瀏覽器,不過僅限於Mac平臺。java
隨後,Google也開始建立自家的瀏覽器。他們也看中了WebKit內核,因而基於WebKit內核推出了Chrome瀏覽器。node
Chrome瀏覽器是跨Windows和Mac平臺的,而且,Google認爲要運行現代Web應用,瀏覽器必須有一個性能很是強勁的JavaScript引擎,因而Google本身開發了一個高性能JavaScript引擎,名字叫V8,以BSD許可證開源。jquery
現代瀏覽器大戰讓微軟的IE瀏覽器遠遠地落後了,由於他們解散了最有經驗、戰鬥力最強的瀏覽器團隊!回過頭再追趕卻發現,支持HTML5的WebKit已經成爲手機端的標準了,IE瀏覽器今後與主流移動端設備絕緣。web
瀏覽器大戰和Node有何關係?ajax
話說有個叫Ryan Dahl的歪果仁,他的工做是用C/C++寫高性能Web服務。對於高性能,異步IO、事件驅動是基本原則,可是用C/C++寫就太痛苦了。因而這位仁兄開始設想用高級語言開發Web服務。他評估了不少種高級語言,發現不少語言雖然同時提供了同步IO和異步IO,可是開發人員一旦用了同步IO,他們就再也懶得寫異步IO了,因此,最終,Ryan瞄向了JavaScript。算法
由於JavaScript是單線程執行,根本不能進行同步IO操做,因此,JavaScript的這一「缺陷」致使了它只能使用異步IO。
選定了開發語言,還要有運行時引擎。這位仁兄曾考慮過本身寫一個,不過明智地放棄了,由於V8就是開源的JavaScript引擎。讓Google投資去優化V8,咱只負責改造一下拿來用,還不用付錢,這個買賣很划算。
因而在2009年,Ryan正式推出了基於JavaScript語言和V8引擎的開源Web服務器項目,命名爲Node.js。雖然名字很土,可是,Node第一次把JavaScript帶入到後端服務器開發,加上世界上已經有無數的JavaScript開發人員,因此Node一會兒就火了起來。
在Node上運行的JavaScript相比其餘後端開發語言有何優點?
最大的優點是藉助JavaScript天生的事件驅動機制加V8高性能引擎,使編寫高性能Web服務垂手可得。
其次,JavaScript語言自己是完善的函數式語言,在前端開發時,開發人員每每寫得比較隨意,讓人感受JavaScript就是個「玩具語言」。可是,在Node環境下,經過模塊化的JavaScript代碼,加上函數式編程,而且無需考慮瀏覽器兼容性問題,直接使用最新的ECMAScript 6標準,能夠徹底知足工程上的需求。
我還據說過io.js,這又是什麼鬼?
由於Node.js是開源項目,雖然由社區推進,但幕後一直由Joyent公司資助。因爲一羣開發者對Joyent公司的策略不滿,於2014年從Node.js項目fork出了io.js項目,決定單獨發展,但二者其實是兼容的。
然而中國有句古話,叫作「分久必合,合久必分」。分家後沒多久,Joyent公司表示要和解,因而,io.js項目又決定迴歸Node.js。
具體作法是未來io.js將首先添加新的特性,若是你們測試用得爽,就把新特性加入Node.js。io.js是「嚐鮮版」,而Node.js是線上穩定版,至關於Fedora Linux和RHEL的關係。
因爲Node.js平臺是在後端運行JavaScript代碼,因此,必須首先在本機安裝Node環境。
目前Node.js的最新版本是6.2.x。首先,從Node.js官網下載對應平臺的安裝程序,網速慢的童鞋請移步國內鏡像。
在Windows上安裝時務必選擇所有組件,包括勾選Add to Path
。
安裝完成後,在Windows環境下,請打開命令提示符,而後輸入node -v
,若是安裝正常,你應該看到v6.2.0這樣的輸出:
C:\Users\IEUser>node -vv6.2.0
繼續在命令提示符輸入node
,此刻你將進入Node.js的交互環境。在交互環境下,你能夠輸入任意JavaScript語句,例如100+200
,回車後將獲得輸出結果。
要退出Node.js環境,連按兩次Ctrl+C
。
在Mac或Linux環境下,請打開終端,而後輸入node -v
,你應該看到以下輸出:
$ node -vv6.2.0
若是版本號不是v6.2.x
,說明Node.js版本不對,後面章節的代碼不保證能正常運行,請從新安裝最新版本。
在正式開始Node.js學習以前,咱們先認識一下npm
。
npm
是什麼東東?npm
實際上是Node.js的包管理工具(package manager)。
爲啥咱們須要一個包管理工具呢?由於咱們在Node.js上開發時,會用到不少別人寫的JavaScript代碼。若是咱們要使用別人寫的某個包,每次都根據名稱搜索一下官方網站,下載代碼,解壓,再使用,很是繁瑣。因而一個集中管理的工具應運而生:你們都把本身開發的模塊打包後放到npm官網上,若是要使用,直接經過npm安裝就能夠直接用,不用管代碼存在哪,應該從哪下載。
更重要的是,若是咱們要使用模塊A,而模塊A又依賴於模塊B,模塊B又依賴於模塊X和模塊Y,npm能夠根據依賴關係,把全部依賴的包都下載下來並管理起來。不然,靠咱們本身手動管理,確定既麻煩又容易出錯。
講了這麼多,npm
究竟在哪?
其實npm
已經在Node.js安裝的時候順帶裝好了。咱們在命令提示符或者終端輸入npm -v
,應該看到相似的輸出:
C:\>npm -v3.8.9
若是直接輸入npm
,你會看到相似下面的輸出:
C:\> npmUsage: npm <command>where <command> is one of: ...
上面的一大堆文字告訴你,npm
須要跟上命令。如今咱們不用關心這些命令,後面會一一講到。目前,你只須要確保npm
正確安裝了,能運行就行。
在前面的全部章節中,咱們編寫的JavaScript代碼都是在瀏覽器中運行的,所以,咱們能夠直接在瀏覽器中敲代碼,而後直接運行。
從本章開始,咱們編寫的JavaScript代碼將不能在瀏覽器環境中執行了,而是在Node環境中執行,所以,JavaScript代碼將直接在你的計算機上以命令行的方式運行,因此,咱們要先選擇一個文本編輯器來編寫JavaScript代碼,而且把它保存到本地硬盤的某個目錄,纔可以執行。
那麼問題來了:文本編輯器到底哪家強?
推薦兩款文本編輯器:
一個是Sublime Text,無償使用,可是不付費會彈出提示框:
一個是Notepad++,無償使用,有中文界面:
請注意,用哪一個都行,可是絕對不能用Word和寫字板,Windows自帶的記事本也強烈不推薦使用。Word和寫字板保存的不是純文本文件,而記事本會自做聰明地在文件開始的地方加上幾個特殊字符(UTF-8 BOM),結果常常會致使程序運行出現莫名其妙的錯誤。
安裝好文本編輯器後,輸入如下代碼:
'use strict'; console.log('Hello, world.');
第一行老是寫上'use strict';
是由於咱們老是以嚴格模式運行JavaScript代碼,避免各類潛在陷阱。而後,選擇一個目錄,例如C:\Workspace
,把文件保存爲hello.js
,就能夠打開命令行窗口,把當前目錄切換到hello.js
所在目錄,而後輸入如下命令運行這個程序了:
C:\Workspace>node hello.js Hello, world.
也能夠保存爲別的名字,好比first.js
,可是必需要以.js
結尾。此外,文件名只能是英文字母、數字和下劃線的組合。
若是當前目錄下沒有hello.js
這個文件,運行node hello.js
就會報錯:
C:\Workspace>node hello.jsmodule.js:338 throw err; ^Error: Cannot find module 'C:\Workspace\hello.js' at Function.Module._resolveFilename at Function.Module._load at Function.Module.runMain at startup at node.js
報錯的意思就是,沒有找到hello.js
這個文件,由於文件不存在。這個時候,就要檢查一下當前目錄下是否有這個文件了。
請注意區分命令行模式和Node交互模式。看到相似C:\>
是在Windows提供的命令行模式:
在命令行模式下,能夠執行node進入Node交互式環境,也能夠執行node hello.js
運行一個.js
文件。看到>
是在Node交互式環境下:
在Node交互式環境下,咱們能夠輸入JavaScript代碼並馬上執行。此外,在命令行模式運行.js
文件和在Node交互式環境下直接運行JavaScript代碼有所不一樣。Node交互式環境會把每一行JavaScript代碼的結果自動打印出來,可是,直接運行JavaScript文件卻不會。
例如,在Node交互式環境下,輸入:
> 100 + 200 + 300; 600
直接能夠看到結果600。
可是,寫一個calc.js
的文件,內容以下:
100 + 200 + 300;
而後在命令行模式下執行:
C:\Workspace>node calc.js
發現什麼輸出都沒有。這是正常的。想要輸出結果,必須本身用console.log()
打印出來。把calc.js
改造一下:
console.log(100 + 200 + 300);
再執行,就能夠看到結果:
C:\Workspace>node calc.js 600
用文本編輯器寫JavaScript程序,而後保存爲後綴爲.js
的文件,就能夠用node直接運行這個程序了。
Node的交互模式和直接運行.js
文件有什麼區別呢?
直接輸入node
進入交互模式,至關於啓動了Node解釋器,可是等待你一行一行地輸入源代碼,每輸入一行就執行一行。
直接運行node hello.js
文件至關於啓動了Node解釋器,而後一次性把hello.js
文件的源代碼給執行了,你是沒有機會以交互的方式輸入源代碼的。
在編寫JavaScript代碼的時候,徹底能夠一邊在文本編輯器裏寫代碼,一邊開一個Node交互式命令窗口,在寫代碼的過程當中,把部分代碼粘到命令行去驗證,事半功倍!前提是得有個27'的超大顯示器!
在計算機程序的開發過程當中,隨着程序代碼越寫越多,在一個文件裏代碼就會愈來愈長,愈來愈不容易維護。
爲了編寫可維護的代碼,咱們把不少函數分組,分別放到不一樣的文件裏,這樣,每一個文件包含的代碼就相對較少,不少編程語言都採用這種組織代碼的方式。在Node環境中,一個.js
文件就稱之爲一個模塊(module)。
使用模塊有什麼好處?
最大的好處是大大提升了代碼的可維護性。其次,編寫代碼沒必要從零開始。當一個模塊編寫完畢,就能夠被其餘地方引用。咱們在編寫程序的時候,也常常引用其餘模塊,包括Node內置的模塊和來自第三方的模塊。
使用模塊還能夠避免函數名和變量名衝突。相同名字的函數和變量徹底能夠分別存在不一樣的模塊中,所以,咱們本身在編寫模塊時,沒必要考慮名字會與其餘模塊衝突。
在上一節,咱們編寫了一個hello.js
文件,這個hello.js
文件就是一個模塊,模塊的名字就是文件名(去掉.js
後綴),因此hello.js
文件就是名爲hello
的模塊。
咱們把hello.js
改造一下,建立一個函數,這樣咱們就能夠在其餘地方調用這個函數:
'use strict'; var s = 'Hello'; function greet(name) { console.log(s + ', ' + name + '!'); } module.exports = greet;
函數greet()
是咱們在hello
模塊中定義的,你可能注意到最後一行是一個奇怪的賦值語句,它的意思是,把函數greet
做爲模塊的輸出暴露出去,這樣其餘模塊就可使用greet
函數了。
問題是其餘模塊怎麼使用hello
模塊的這個greet
函數呢?咱們再編寫一個main.js
文件,調用hello
模塊的greet
函數:
'use strict'; // 引入hello模塊: var greet = require('./hello'); var s = 'Michael'; greet(s); // Hello, Michael!
注意到引入hello
模塊用Node提供的require
函數:
var greet = require('./hello');
引入的模塊做爲變量保存在greet
變量中,那greet
變量究竟是什麼東西?其實變量greet
就是在hello.js
中咱們用module.exports = greet;
輸出的greet
函數。因此,main.js
就成功地引用了hello.js
模塊中定義的greet()
函數,接下來就能夠直接使用它了。
在使用require()
引入模塊的時候,請注意模塊的相對路徑。由於main.js
和hello.js
位於同一個目錄,因此咱們用了當前目錄.:
var greet = require('./hello'); // 不要忘了寫相對目錄!
若是隻寫模塊名:
var greet = require('hello');
則Node會依次在內置模塊、全局模塊和當前模塊下查找hello.js
,你極可能會獲得一個錯誤:
module.js throw err; ^Error: Cannot find module 'hello' at Function.Module._resolveFilename at Function.Module._load ... at Function.Module._load at Function.Module.runMain
遇到這個錯誤,你要檢查:
這種模塊加載機制被稱爲CommonJS規範。在這個規範下,每一個.js
文件都是一個模塊,它們內部各自使用的變量名和函數名都互不衝突,例如,hello.js
和main.js
都申明瞭全局變量var s = 'xxx'
,但互不影響。
一個模塊想要對外暴露變量(函數也是變量),能夠用module.exports = variable;
,一個模塊要引用其餘模塊暴露的變量,用
var ref = require('module_name');
就拿到了引用模塊的變量。
要在模塊中對外輸出變量,用:module.exports = variable;
輸出的變量能夠是任意對象、函數、數組等等。要引入其餘模塊輸出的對象,用:var foo = require('other_module');
引入的對象具體是什麼,取決於引入模塊輸出的對象。
若是你想詳細地瞭解CommonJS的模塊實現原理,請繼續往下閱讀
當咱們編寫JavaScript代碼時,咱們能夠申明全局變量:
var s = 'global';
在瀏覽器中,大量使用全局變量可很差。若是你在a.js
中使用了全局變量s
,那麼,在b.js
中也使用全局變量s
,將形成衝突,b.js
中對s
賦值會改變a.js
的運行邏輯。
也就是說,JavaScript語言自己並無一種模塊機制來保證不一樣模塊可使用相同的變量名。
那Node.js是如何實現這一點的?
其實要實現「模塊」這個功能,並不須要語法層面的支持。Node.js也並不會增長任何JavaScript語法。實現「模塊」功能的奧妙就在於JavaScript是一種函數式編程語言,它支持閉包。若是咱們把一段JavaScript代碼用一個函數包裝起來,這段代碼的全部「全局」變量就變成了函數內部的局部變量。
請注意咱們編寫的hello.js
代碼是這樣的:
var s = 'Hello'; var name = 'world'; console.log(s + ' ' + name + '!');
Node.js加載了hello.js
後,它能夠把代碼包裝一下,變成這樣執行:
(function () { // 讀取的hello.js代碼: var s = 'Hello'; var name = 'world'; console.log(s + ' ' + name + '!'); // hello.js代碼結束 })();
這樣一來,原來的全局變量s
如今變成了匿名函數內部的局部變量。若是Node.js繼續加載其餘模塊,這些模塊中定義的「全局」變量s
也互不干擾。
因此,Node利用JavaScript的函數式編程的特性,垂手可得地實現了模塊的隔離。
可是,模塊的輸出module.exports
怎麼實現?
這個也很容易實現,Node能夠先準備一個對象module:
// 準備module對象: var module = { id: 'hello', exports: {} }; var load = function (module) { // 讀取的hello.js代碼: function greet(name) { console.log('Hello, ' + name + '!'); } module.exports = greet; // hello.js代碼結束 return module.exports; }; var exported = load(module); // 保存module: save(module, exported);
可見,變量module
是Node在加載js文件前準備的一個變量,並將其傳入加載函數,咱們在hello.js
中能夠直接使用變量module
緣由就在於它其實是函數的一個參數:module.exports = greet;
經過把參數module
傳遞給load()
函數,hello.js
就順利地把一個變量傳遞給了Node執行環境,Node會把module
變量保存到某個地方。
因爲Node保存了全部導入的module
,當咱們用require()
獲取module時,Node找到對應的module
,把這個module
的exports
變量返回,這樣,另外一個模塊就順利拿到了模塊的輸出:
var greet = require('./hello');
以上是Node實現JavaScript模塊的一個簡單的原理介紹。
不少時候,你會看到,在Node環境中,有兩種方法能夠在一個模塊中輸出變量:
方法一:對module.exports
賦值:
// hello.js function hello() { console.log('Hello, world!'); } function greet(name) { console.log('Hello, ' + name + '!'); } function hello() { console.log('Hello, world!'); } module.exports = { hello: hello, greet: greet};
方法二:直接使用exports:
// hello.js function hello() { console.log('Hello, world!'); } function greet(name) { console.log('Hello, ' + name + '!'); } function hello() { console.log('Hello, world!'); } exports.hello = hello; exports.greet = greet;
可是你不能夠直接對exports
賦值:
// 代碼能夠執行,可是模塊並無輸出任何變量: exports = { hello: hello, greet: greet };
若是你對上面的寫法感到十分困惑,不要着急,咱們來分析Node的加載機制:
首先,Node會把整個待加載的hello.js
文件放入一個包裝函數load
中執行。在執行這個load()
函數前,Node準備好了module
變量:
var module = { id: 'hello', exports: {} };
load()
函數最終返回module.exports
:
var load = function (exports, module) { // hello.js的文件內容 ... // load函數返回: return module.exports; }; var exported = load(module.exports, module);
也就是說,默認狀況下,Node準備的exports
變量和module.exports
變量其實是同一個變量,而且初始化爲空對象{}
,因而,咱們能夠寫:
exports.foo = function () { return 'foo'; }; exports.bar = function () { return 'bar'; };
也能夠寫:
module.exports.foo = function () { return 'foo'; }; module.exports.bar = function () { return 'bar'; };
換句話說,Node默認給你準備了一個空對象{}
,這樣你能夠直接往裏面加東西。
可是,若是咱們要輸出的是一個函數或數組,那麼,只能給module.exports
賦值:
module.exports = function () { return 'foo'; };
給exports
賦值是無效的,由於賦值後,module.exports
仍然是空對象{}
。
結論:
若是要輸出一個鍵值對象{}
,能夠利用exports
這個已存在的空對象{}
,並繼續在上面添加新的鍵值;
若是要輸出一個函數或數組,必須直接對module.exports
對象賦值。
因此咱們能夠得出結論:直接對module.exports
賦值,能夠應對任何狀況:
module.exports = { foo: function () { return 'foo'; } };
或者:
module.exports = function () { return 'foo'; };
最終,咱們強烈建議使用module.exports = xxx
的方式來輸出模塊變量,這樣,你只須要記憶一種方法。
由於Node.js是運行在服務區端的JavaScript環境,服務器程序和瀏覽器程序相比,最大的特色是沒有瀏覽器的安全限制了,並且,服務器程序必須能接收網絡請求,讀寫文件,處理二進制內容,因此,Node.js內置的經常使用模塊就是爲了實現基本的服務器功能。這些模塊在瀏覽器環境中是沒法被執行的,由於它們的底層代碼是用C/C++在Node.js運行環境中實現的。
在前面的JavaScript課程中,咱們已經知道,JavaScript有且僅有一個全局對象,在瀏覽器中,叫window
對象。而在Node.js環境中,也有惟一的全局對象,但不叫window
,而叫global
,這個對象的屬性和方法也和瀏覽器環境的window
不一樣。進入Node.js交互環境,能夠直接輸入:
> global.console Console { log: [Function: bound ], info: [Function: bound ], warn: [Function: bound ], error: [Function: bound ], dir: [Function: bound ], time: [Function: bound ], timeEnd: [Function: bound ], trace: [Function: bound trace], assert: [Function: bound ], Console: [Function: Console] }
process
也是Node.js提供的一個對象,它表明當前Node.js進程。經過process
對象能夠拿到許多有用信息:
> process === global.process; true > process.version; 'v5.2.0' > process.platform; 'darwin' > process.arch; 'x64' > process.cwd(); //返回當前工做目錄 '/Users/michael' > process.chdir('/private/tmp'); // 切換當前工做目錄 undefined > process.cwd(); '/private/tmp'
JavaScript程序是由事件驅動執行的單線程模型,Node.js也不例外。Node.js不斷執行響應事件的JavaScript函數,直到沒有任何響應事件的函數能夠執行時,Node.js就退出了。
若是咱們想要在下一次事件響應中執行代碼,能夠調用process.nextTick()
:
// test.js // process.nextTick()將在下一輪事件循環中調用: process.nextTick(function () { console.log('nextTick callback!'); }); console.log('nextTick was set!');
用Node執行上面的代碼node test.js
,你會看到,打印輸出是:
nextTick was set! nextTick callback!
這說明傳入process.nextTick()
的函數不是馬上執行,而是要等到下一次事件循環。
Node.js進程自己的事件就由process
對象來處理。若是咱們響應exit
事件,就能夠在程序即將退出時執行某個回調函數:
// 程序即將退出時的回調函數: process.on('exit', function (code) { console.log('about to exit with code: ' + code); });
有不少JavaScript代碼既能在瀏覽器中執行,也能在Node環境執行,但有些時候,程序自己須要判斷本身究竟是在什麼環境下執行的,經常使用的方式就是根據瀏覽器和Node環境提供的全局變量名稱來判斷:
if (typeof(window) === 'undefined') { console.log('node.js'); } else { console.log('browser'); }
後面,咱們將介紹Node.js的經常使用內置模塊。
Node.js內置的fs
模塊就是文件系統模塊,負責讀寫文件。
和全部其它JavaScript模塊不一樣的是,fs
模塊同時提供了異步和同步的方法。
回顧一下什麼是異步方法。由於JavaScript的單線程模型,執行IO操做時,JavaScript代碼無需等待,而是傳入回調函數後,繼續執行後續JavaScript代碼。好比jQuery提供的getJSON()
操做:
$.getJSON('http://example.com/ajax', function (data) { console.log('IO結果返回後執行...'); }); console.log('不等待IO結果直接執行後續代碼...');
而同步的IO操做則須要等待函數返回:
// 根據網絡耗時,函數將執行幾十毫秒~幾秒不等: var data = getJSONSync('http://example.com/ajax');
同步操做的好處是代碼簡單,缺點是程序將等待IO操做,在等待時間內,沒法響應其它任何事件。而異步讀取不用等待IO操做,但代碼較麻煩。
按照JavaScript的標準,異步讀取一個文本文件的代碼以下:
'use strict'; var fs = require('fs'); fs.readFile('sample.txt', 'utf-8', function (err, data) { if (err) { console.log(err); } else { console.log(data); } });
請注意,sample.txt
文件必須在當前目錄下,且文件編碼爲utf-8。
異步讀取時,傳入的回調函數接收兩個參數,當正常讀取時,err
參數爲null
,data
參數爲讀取到的String
。當讀取發生錯誤時,err
參數表明一個錯誤對象,data
爲undefined
。
這也是Node.js標準的回調函數:第一個參數表明錯誤信息,第二個參數表明結果。後面咱們還會常常編寫這種回調函數。
因爲err
是否爲null
就是判斷是否出錯的標誌,因此一般的判斷邏輯老是:
if (err) { // 出錯了 } else { // 正常 }
若是咱們要讀取的文件不是文本文件,而是二進制文件,怎麼辦?
下面的例子演示瞭如何讀取一個圖片文件:
'use strict'; var fs = require('fs'); fs.readFile('sample.png', function (err, data) { if (err) { console.log(err); } else { console.log(data); console.log(data.length + ' bytes'); } });
當讀取二進制文件時,不傳入文件編碼時,回調函數的data
參數將返回一個Buffer
對象。在Node.js中,Buffer
對象就是一個包含零個或任意個字節的數組(注意和Array不一樣)。
Buffer
對象能夠和String做轉換,例如,把一個Buffer
對象轉換成String:
// Buffer -> String var text = data.toString('utf-8'); console.log(text);
或者把一個String
轉換成Buffer
:
// String -> Buffer var buf = new Buffer(text, 'utf-8'); console.log(buf);
除了標準的異步讀取模式外,fs
也提供相應的同步讀取函數。同步讀取的函數和異步函數相比,多了一個Sync
後綴,而且不接收回調函數,函數直接返回結果。
用fs
模塊同步讀取一個文本文件的代碼以下:
'use strict'; var fs = require('fs'); var data = fs.readFileSync('sample.txt', 'utf-8'); console.log(data);
可見,原異步調用的回調函數的data
被函數直接返回,函數名須要改成readFileSync
,其它參數不變。
若是同步讀取文件發生錯誤,則須要用try...catch
捕獲該錯誤:
try { var data = fs.readFileSync('sample.txt', 'utf-8'); console.log(data); } catch (err) { // 出錯了 }
將數據寫入文件是經過fs.writeFile()
實現的:
'use strict'; var fs = require('fs'); var data = 'Hello, Node.js'; fs.writeFile('output.txt', data, function (err) { if (err) { console.log(err); } else { console.log('ok.'); } });
writeFile()
的參數依次爲文件名、數據和回調函數。若是傳入的數據是String
,默認按UTF-8編碼寫入文本文件,若是傳入的參數是Buffer
,則寫入的是二進制文件。回調函數因爲只關心成功與否,所以只須要一個err
參數。
和readFile()
相似,writeFile()
也有一個同步方法,叫writeFileSync()
:
'use strict'; var fs = require('fs'); var data = 'Hello, Node.js'; fs.writeFileSync('output.txt', data);
若是咱們要獲取文件大小,建立時間等信息,可使用fs.stat()
,它返回一個Stat
對象,能告訴咱們文件或目錄的詳細信息:
'use strict'; var fs = require('fs'); fs.stat('sample.txt', function (err, stat) { if (err) { console.log(err); } else { // 是不是文件: console.log('isFile: ' + stat.isFile()); // 是不是目錄: console.log('isDirectory: ' + stat.isDirectory()); if (stat.isFile()) { // 文件大小: console.log('size: ' + stat.size); // 建立時間, Date對象: console.log('birth time: ' + stat.birthtime); // 修改時間, Date對象: console.log('modified time: ' + stat.mtime); } } });
運行結果以下:
isFile: true isDirectory: false size: 181 birth time: Fri Dec 11 2015 09:43:41 GMT+0800 (CST) modified time: Fri Dec 11 2015 12:09:00 GMT+0800 (CST)
stat()
也有一個對應的同步函數statSync()
,請試着改寫上述異步代碼爲同步代碼。
在fs
模塊中,提供同步方法是爲了方便使用。那咱們究竟是應該用異步方法仍是同步方法呢?
因爲Node環境執行的JavaScript代碼是服務器端代碼,因此,絕大部分須要在服務器運行期反覆執行業務邏輯的代碼,必須使用異步代碼,不然,同步代碼在執行時期,服務器將中止響應,由於JavaScript只有一個執行線程。
服務器啓動時若是須要讀取配置文件,或者結束時須要寫入到狀態文件時,可使用同步代碼,由於這些代碼只在啓動和結束時執行一次,不影響服務器正常運行時的異步執行。
stream
是Node.js提供的又一個僅在服務區端可用的模塊,目的是支持「流」這種數據結構。
什麼是流?流是一種抽象的數據結構。想象水流,當在水管中流動時,就能夠從某個地方(例如自來水廠)源源不斷地到達另外一個地方(好比你家的洗手池)。
咱們也能夠把數據當作是數據流,好比你敲鍵盤的時候,就能夠把每一個字符依次連起來,當作字符流。這個流是從鍵盤輸入到應用程序,實際上它還對應着一個名字:標準輸入流(stdin)。
若是應用程序把字符一個一個輸出到顯示器上,這也能夠當作是一個流,這個流也有名字:標準輸出流(stdout)。流的特色是數據是有序的,並且必須依次讀取,或者依次寫入,不能像Array那樣隨機定位。
有些流用來讀取數據,好比從文件讀取數據時,能夠打開一個文件流,而後從文件流中不斷地讀取數據。有些流用來寫入數據,好比向文件寫入數據時,只須要把數據不斷地往文件流中寫進去就能夠了。
在Node.js中,流也是一個對象,咱們只須要響應流的事件就能夠了:data
事件表示流的數據已經能夠讀取了,end
事件表示這個流已經到末尾了,沒有數據能夠讀取了,error
事件表示出錯了。
下面是一個從文件流讀取文本內容的示例:
'use strict'; var fs = require('fs'); // 打開一個流: var rs = fs.createReadStream('sample.txt', 'utf-8'); rs.on('data', function (chunk) { console.log('DATA:') console.log(chunk); }); rs.on('end', function () { console.log('END'); }); rs.on('error', function (err) { console.log('ERROR: ' + err); });
要注意,data
事件可能會有屢次,每次傳遞的chunk
是流的一部分數據。
要以流的形式寫入文件,只須要不斷調用write()
方法,最後以end()
結束:
'use strict'; var fs = require('fs'); var ws1 = fs.createWriteStream('output1.txt', 'utf-8'); ws1.write('使用Stream寫入文本數據...\n'); ws1.write('END.'); ws1.end(); var ws2 = fs.createWriteStream('output2.txt'); ws2.write(new Buffer('使用Stream寫入二進制數據...\n', 'utf-8')); ws2.write(new Buffer('END.', 'utf-8')); ws2.end();
全部能夠讀取數據的流都繼承自stream.Readable
,全部能夠寫入的流都繼承自stream.Writable
。
就像能夠把兩個水管串成一個更長的水管同樣,兩個流也能夠串起來。一個Readable
流和一個Writable
流串起來後,全部的數據自動從Readable
流進入Writable
流,這種操做叫pipe
。
在Node.js中,Readable
流有一個pipe()
方法,就是用來幹這件事的。
讓咱們用pipe()
把一個文件流和另外一個文件流串起來,這樣源文件的全部數據就自動寫入到目標文件裏了,因此,這其實是一個複製文件的程序:
'use strict'; var fs = require('fs'); var rs = fs.createReadStream('sample.txt'); var ws = fs.createWriteStream('copied.txt'); rs.pipe(ws);
默認狀況下,當Readable
流的數據讀取完畢,end
事件觸發後,將自動關閉Writable
流。若是咱們不但願自動關閉Writable
流,須要傳入參數:
readable.pipe(writable, { end: false });
Node.js開發的目的就是爲了用JavaScript編寫Web服務器程序。由於JavaScript實際上已經統治了瀏覽器端的腳本,其優點就是有世界上數量最多的前端開發人員。若是已經掌握了JavaScript前端開發,再學習一下如何將JavaScript應用在後端開發,就是名副其實的全棧了。
要理解Web服務器程序的工做原理,首先,咱們要對HTTP協議有基本的瞭解。若是你對HTTP協議不太熟悉,先看一看HTTP協議簡介。
要開發HTTP服務器程序,從頭處理TCP
鏈接,解析HTTP是不現實的。這些工做實際上已經由Node.js自帶的http
模塊完成了。應用程序並不直接和HTTP協議打交道,而是操做http
模塊提供的request
和response
對象。
request
對象封裝了HTTP請求,咱們調用request
對象的屬性和方法就能夠拿到全部HTTP請求的信息;
response
對象封裝了HTTP響應,咱們操做response
對象的方法,就能夠把HTTP響應返回給瀏覽器。
用Node.js實現一個HTTP服務器程序很是簡單。咱們來實現一個最簡單的Web程序hello.js
,它對於全部請求,都返回Hello world!
:
'use strict'; // 導入http模塊: var http = require('http'); // 建立http server,並傳入回調函數: var server = http.createServer(function (request, response) { // 回調函數接收request和response對象, // 得到HTTP請求的method和url: console.log(request.method + ': ' + request.url); // 將HTTP響應200寫入response, 同時設置Content-Type: text/html: response.writeHead(200, {'Content-Type': 'text/html'}); // 將HTTP響應的HTML內容寫入response: response.end('<h1>Hello world!</h1>'); }); // 讓服務器監聽8080端口: server.listen(8080); console.log('Server is running at http://127.0.0.1:8080/');
在命令提示符下運行該程序,能夠看到如下輸出:
$ node hello.js Server is running at http://127.0.0.1:8080/
不要關閉命令提示符,直接打開瀏覽器輸入http://localhost:8080
,便可看到服務器響應的內容:
同時,在命令提示符窗口,能夠看到程序打印的請求信息:
GET: /GET: /favicon.ico
這就是咱們編寫的第一個HTTP服務器程序!
讓咱們繼續擴展一下上面的Web程序。咱們能夠設定一個目錄,而後讓Web程序變成一個文件服務器。要實現這一點,咱們只須要解析request.url
中的路徑,而後在本地找到對應的文件,把文件內容發送出去就能夠了。
解析URL須要用到Node.js提供的url
模塊,它使用起來很是簡單,經過parse()
將一個字符串解析爲一個Url
對象:
'use strict'; var url =require('url'); console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));
結果以下:
Url { protocol: 'http:', slashes: true, auth: 'user:pass', host: 'host.com:8080', port: '8080', hostname: 'host.com', hash: '#hash', search: '?query=string', query: 'query=string', pathname: '/path/to/file', path: '/path/to/file?query=string', href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' }
處理本地文件目錄須要使用Node.js提供的path
模塊,它能夠方便地構造目錄:
'use strict'; var path = require('path'); // 解析當前目錄: var workDir = path.resolve('.'); // '/Users/michael' // 組合完整的文件路徑:當前目錄+'pub'+'index.html': var filePath = path.join(workDir, 'pub', 'index.html'); // '/Users/michael/pub/index.html'
使用path
模塊能夠正確處理操做系統相關的文件路徑。在Windows系統下,返回的路徑相似於C:\Users\michael\static\index.html
,這樣,咱們就不關心怎麼拼接路徑了。
最後,咱們實現一個文件服務器file_server.js
:
'use strict'; var fs = require('fs'), url = require('url'), path = require('path'), http = require('http'); // 從命令行參數獲取root目錄,默認是當前目錄: var root = path.resolve(process.argv[2] || '.'); console.log('Static root dir: ' + root); // 建立服務器: var server = http.createServer(function (request, response) { // 得到URL的path,相似 '/css/bootstrap.css': var pathname = url.parse(request.url).pathname; // 得到對應的本地文件路徑,相似 '/srv/www/css/bootstrap.css': var filepath = path.join(root, pathname); // 獲取文件狀態: fs.stat(filepath, function (err, stats) { if (!err && stats.isFile()) { // 沒有出錯而且文件存在: console.log('200 ' + request.url); // 發送200響應: response.writeHead(200); // 將文件流導向response: fs.createReadStream(filepath).pipe(response); } else { // 出錯了或者文件不存在: console.log('404 ' + request.url); // 發送404響應: response.writeHead(404); response.end('404 Not Found'); } }); }); server.listen(8080); console.log('Server is running at http://127.0.0.1:8080/');
沒有必要手動讀取文件內容。因爲response
對象自己是一個Writable Stream
,直接用pipe()
方法就實現了自動讀取文件內容並輸出到HTTP響應。
在命令行運行node file_server.js /path/to/dir
,把/path/to/dir
改爲你本地的一個有效的目錄,而後在瀏覽器中輸入
http://localhost:8080/index.html
只要當前目錄下存在文件index.html,服務器就能夠把文件內容發送給瀏覽器。觀察控制檯輸出:
200 /index.html 200 /css/uikit.min.css 200 /js/jquery.min.js 200 /fonts/fontawesome-webfont.woff2
第一個請求是瀏覽器請求index.html
頁面,後續請求是瀏覽器解析HTML後發送的其它資源請求。
crypto
模塊的目的是爲了提供通用的加密和哈希算法。用純JavaScript代碼實現這些功能不是不可能,但速度會很是慢。Nodejs用C/C++實現這些算法後,經過cypto
這個模塊暴露爲JavaScript接口,這樣用起來方便,運行速度也快。
MD5是一種經常使用的哈希算法,用於給任意數據一個「簽名」。這個簽名一般用一個十六進制的字符串表示:
const crypto = require('crypto'); const hash = crypto.createHash('md5'); // 可任意屢次調用update(): hash.update('Hello, world!'); hash.update('Hello, nodejs!'); console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
update()
方法默認字符串編碼爲UTF-8
,也能夠傳入Buffer
。
若是要計算SHA1
,只須要把'md5'
改爲'sha1'
,就能夠獲得SHA1的結果
1f32b9c9932c02227819a4151feed43e131aca40
還可使用更安全的sha256
和sha512
。
Hmac算法也是一種哈希算法,它能夠利用MD5或SHA1等哈希算法。不一樣的是,Hmac
還須要一個密鑰:
const crypto = require('crypto'); const hmac = crypto.createHmac('sha256', 'secret-key'); hmac.update('Hello, world!'); hmac.update('Hello, nodejs!'); console.log(hmac.digest('hex')); // 80f7e22570...
只要密鑰發生了變化,那麼一樣的輸入數據也會獲得不一樣的簽名,所以,能夠把Hmac理解爲用隨機數「加強」的哈希算法。
AES是一種經常使用的對稱加密算法,加解密都用同一個密鑰。crypto
模塊提供了AES支持,可是須要本身封裝好函數,便於使用:
const crypto = require('crypto'); function aesEncrypt(data, key) { const cipher = crypto.createCipher('aes192', key); var crypted = cipher.update(data, 'utf8', 'hex'); crypted += cipher.final('hex'); return crypted; } function aesDecrypt(data, key) { const decipher = crypto.createDecipher('aes192', key); var decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } var data = 'Hello, this is a secret message!'; var key = 'Password!'; var encrypted = aesEncrypt(data, key); var decrypted = aesDecrypt(encrypted, key); console.log('Plain text: ' + data); console.log('Encrypted text: ' + encrypted); console.log('Decrypted text: ' + decrypted);
運行結果以下:
Plain text: Hello, this is a secret message! Encrypted text: 8a944d97bdabc157a5b7a40cb180e7... Decrypted text: Hello, this is a secret message!
能夠看出,加密後的字符串經過解密又獲得了原始內容。
注意到AES有不少不一樣的算法,如aes192
,aes-128-ecb
,aes-256-cbc
等,AES除了密鑰外還能夠指定IV
(Initial Vector),不一樣的系統只要IV不一樣,用相同的密鑰加密相同的數據獲得的加密結果也是不一樣的。
加密結果一般有兩種表示方法:hex
和base64
,這些功能Nodejs所有都支持,可是在應用中要注意,若是加解密雙方一方用Nodejs,另外一方用Java、PHP等其它語言,須要仔細測試。若是沒法正確解密,要確認雙方是否遵循一樣的AES算法,字符串密鑰和IV是否相同,加密後的數據是否統一爲hex或base64格式。
DH算法是一種密鑰交換協議,它可讓雙方在不泄漏密鑰的狀況下協商出一個密鑰來。DH算法基於數學原理,好比小明和小紅想要協商一個密鑰,能夠這麼作:
小明先選一個素數和一個底數,例如,素數p=23
,底數g=5
(底數能夠任選),再選擇一個祕密整數a=6
,計算A=g^a mod p=8
,而後大聲告訴小紅:p=23,g=5,A=8
;
小紅收到小明發來的p,g,A後,也選一個祕密整數b=15
,而後計算B=g^b mod p=19
,並大聲告訴小明:B=19
;
小明本身計算出s=B^a mod p=2
,小紅也本身計算出s=A^b mod p=2
,所以,最終協商的密鑰s爲2。
在這個過程當中,密鑰2並非小明告訴小紅的,也不是小紅告訴小明的,而是雙方協商計算出來的。第三方只能知道p=23
,g=5
,A=8
,B=19
,因爲不知道雙方選的祕密整數a=6
和b=15
,所以沒法計算出密鑰2。
用crypto模塊實現DH算法以下:
const crypto = require('crypto'); // xiaoming's keys: var ming = crypto.createDiffieHellman(512); var ming_keys = ming.generateKeys(); var prime = ming.getPrime(); var generator = ming.getGenerator(); console.log('Prime: ' + prime.toString('hex')); console.log('Generator: ' + generator.toString('hex')); // xiaohong's keys: var hong = crypto.createDiffieHellman(prime, generator); var hong_keys = hong.generateKeys(); // exchange and generate secret: var ming_secret = ming.computeSecret(hong_keys); var hong_secret = hong.computeSecret(ming_keys); // print secret: console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex')); console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));
運行後,能夠獲得以下輸出:
$ node dh.js Prime: a8224c...deead3 Generator: 02 Secret of Xiao Ming: 695308...d519be Secret of Xiao Hong: 695308...d519be
注意每次輸出都不同,由於素數的選擇是隨機的。
crypto模塊也能夠處理數字證書。數字證書一般用在SSL鏈接,也就是Web的https鏈接。通常狀況下,https鏈接只須要處理服務器端的單向認證,如無特殊需求(例如本身做爲Root給客戶發認證證書),建議用反向代理服務器如Nginx等Web服務器去處理證書。