Node.js學習
目錄
學習node之前,首先要安裝node,去node官網安裝比較node.js 8的穩定版本,然後學習
1.檔案操作
在工作中會遇到檔案操作,比如讀檔案,寫檔案,檔案重新命名,刪除檔案,以檔案操作作為學習Node.js的起點。接下來會建立一些實用的非同步操作檔案的工具。
1.1 Node.js事件迴圈程式設計
1.1.1 監聽檔案變化
開啟命令列視窗,建立目錄filesystem,在該目錄下面新建target.txt檔案,
mkdir filesystem
cd filesystem
touch target.txt
在filesystem目錄下新建watcher.js檔案,檔案內容如下:
'use strict'; const fs = require('fs'); fs.watch('target.txt', () => { console.log('File Changed!') }); console.log('Now watching target.txt for changes.....');
上面程式碼,第一行的'use strict'表示讓程式碼在嚴格模式下執行。嚴格模式是ES5的新特性。
require()函式用於引入Node.js模組並將這個模組作為返回值。在本例中,執行require('fs')是為了引入Node.js內建的檔案模組。
在Node.js中,模組是一段獨立的Javascript程式碼,它提供的功能可以用於其他地方。require()的返回值通常是Javascript物件或者是函式
。模組還可以依賴別的模組,類似於其他模組語言中庫的概念,其他程式語言中的庫也可以是import或#include其他庫。然後呼叫fs模組的watch()方法,這個方法接收2個引數,一個是檔案路徑,一個是檔案變化時需要執行的回撥函式。在Javascript中,函式是一等公民,也就是說,函式可以被賦值給變數,或者作為引數傳遞給別的引數。
接著在命令列試著執行,使用node啟動這個監聽程式
node watcher.js
Now watching target.txt for changes.....
程式啟動之後,Node.js會安靜地等待目標檔案內容的變化。修改target.txt檔案的內容,看命令列的輸出,看到輸出了File Changed!,然後監聽程式會繼續等待檔案內容的變化。
如果看到了幾條重複的輸出訊息,並不是程式碼出現了bug,原因與作業系統對檔案變化的處理方式有關。
node watcher.js
Now watching target.txt for changes.....
File Changed!
File Changed!
File Changed!
File Changed!
1.1.2 看得見的事件迴圈
上一節的例子展示了node.js事件迴圈的工作。Node.js按照如下方式來執行
- 載入程式碼,從開始執行到最後一行,在命令列輸出Now watching target.txt for changes.....
- 由於呼叫了fs.watch,所以node.js不會退出
- 它等待著fs模組監聽目標檔案的變化
- 當目標檔案發生變化時,執行回撥函式
- 程式繼續等待,繼續監聽,還不能退出
事件迴圈會一直持續下去,直到沒有任何程式碼需要執行,沒有任何事件需要等待,或程式因為其他因素退出。比如程式執行時候發生錯誤丟擲異常,異常有沒有被正確捕獲到,通常會導致程序 退出。
1.1.3 接收命令列引數
改進監聽程式,讓它能夠接收引數,在引數中指定我們要監聽哪個檔案。用到process全域性物件,還有如何捕獲異常,如下:
'use strict';
const fs = require('fs');
const filename = process.argv[2];
if (!filename) {
throw Error('A file to watch must be specified!')
}
fs.watch(filename, () => { console.log('File ${filename} Changed!') });
console.log(`Now watching ${filename} for changes.....`);
按照下面方式執行
node watcher.js target.txt
Now watching target.txt for changes.....
輸出的內容和上一小節一致,通過process.argv訪問命令列輸入的引數。argv是argument vector的簡寫,它的值是陣列,其中輸出的前2項分別是node 和target.txt的絕對路徑,陣列的第3項就是目標檔案的檔名target.txt。
'File ${filename} Changed!')輸出資訊是由反引號(``)包裹起來的字串,稱為模板字串。
如果輸入node watcher.js命令,就會丟擲異常退出。所以未捕獲的異常都會導致Node.js執行程序退出。錯誤資訊一般包含拋錯的檔名、拋錯的行數和具體的錯誤位置。
node watcher.js
watcher.js:5
throw Error('A file to watch must be specified!')
^
Error: A file to watch must be specified!
程序是node.js中非常重要的概念,在開發中常見做法是不同的工作放在不同的獨立程序中執行,而不是所有程式碼都塞進一個巨無霸Node.js程式裡,下一節學習如何在Node.js中建立程序。
1.2 建立子程序
繼續優化監聽程式,讓它在監聽到檔案變化後建立一個子程序,再用這個子程序執行系統該命令,在此過程中,會接觸到child-process模組,Node.js的開發模式和一些內建類,還會學習如何用流進行資料傳送。
編輯如下程式碼,程式碼會執行ls命令並加上-l和-h引數,這樣就能看到目標檔案的修改時間。
'use strict';
const fs = require('fs');
const spawn = require('child_process').spawn;
const filename = process.argv[2];
if (!filename) {
throw Error('A file to watch must be specified!')
}
fs.watch(filename, () => {
const ls = spawn('ls', ['-l', '-h', filename]);
ls.stdout.pipe(process.stdout);
});
console.log(`Now watching ${filename} for changes.....`);
執行下面命令執行它
node watcher.js target.txt
Now watching target.txt for changes.....
當修改target.txt檔案後,監聽程式會輸出類似這樣的資訊,有使用者名稱,使用者組,檔案屬性資訊
-rw-r--r-- 1 ff ff 6 7月 13 15:48 target.txt
程式碼的開始部分有新的require()語句,require('child_process')語句將返回child_process模組。目前我們只關心其中的spawn()方法,所以把spawn()方法賦值給一個常量且暫時忽略模組中的其他功能,在javascript中,函式是一等公民,可以直接賦值給另外一個變數。
const spawn = require('child_process').spawn;
spawn()的第一個引數是需要執行命令的名稱,在本例中就是ls。第二個引數是命令列的引數陣列,包括ls命令本身的引數和目標檔名。
spawn()返回的物件是ChildProcess。它的stdin、stdout、stderr屬性都是Stream,可以用作輸入和輸出。使用pipe()方法把子程序的輸出內容直接傳送到標準輸出流。
有些場景下,我們需要讀取輸出的資料而不是直接傳送,怎麼做呢?
1.3 使用EventEmitter獲取資料
EventEmitter是Node.js中非常重要的一個類,可以通過它觸發事件或者響應事件。Node.js中的很多物件都繼承自EventEmitter,例如上一節提到的Stream類。
修改上節的例子,通過監聽stream的事件來獲取子程序的輸出內容。如下:
'use strict';
const fs = require('fs');
const spawn = require('child_process').spawn;
const filename = process.argv[2];
if (!filename) {
throw Error('A file to watch must be specified!')
}
fs.watch(filename, () => {
const ls = spawn('ls', ['-l', '-h', filename]);
let output = '';
ls.stdout.on('data', chunk => output += chunk);
ls.on('close', () => {
const parts = output.split(/\s+/);
console.log(parts[0], parts[4], parts[8]);
})
});
console.log(`Now watching ${filename} for changes.....`);
執行命令,然後修改target.txt檔案內容,控制檯輸出內容如下:
node watcher.js target.txt
Now watching target.txt for changes.....
-rw-r--r-- 12 target.txt
這個新的回撥函式,會像之前一樣被呼叫,它會建立一個子程序並把子程序賦值給ls變數。函式內也會宣告output變數,用於把子程序輸出的內容暫存起來。
接下來新增事件監聽函式。當特定型別的事件發生時,這個監聽函式就會被呼叫。Stream類繼承自EventEmitter,所以能夠監聽到子程序標準輸出流的事件
ls.stdout.on('data', chunk => output += chunk);
上面這行程式碼的資訊量比較大,我們拆看來看。
這裡的箭頭函式接收chunk引數。on()方法用於給指定事件新增事件監聽函式,本例中監聽的是data事件,因為我們要獲得輸出流的資料。
事件發生後,可以通過回撥函式的引數獲取跟事件相關的資訊,比如本例中的data時間會將buffer物件作為引數傳給回撥函式,然後每拿到一部分資料,我們將把這個引數裡的資料新增到output變數。
Node.js中使用Buffer描述二進位制資料。它指向一段記憶體中的資料,這個資料由Node.js核心管理,而不在javascript引擎中。Buffer不能修改,並且需要解碼和編碼的過程才能裝換成JavaScript字串。
在javascript裡,把非string值新增到string中,都會隱式呼叫物件的toString()方法。具體到Buffer物件,當它跟一個string相加時,會把這個二進位制資料複製到Node.js堆疊中,然後使用預設方式(UTF-8)編碼。
把資料從二進位制複製到Node.js的操作非常耗時,所以儘管string操作更加敏捷,但還是應該儘可能直接操作Buffer。在這個例子中,由於資料量很小,相應的耗時也很少,影響不大。但是希望大家今後在使用Buffer時,腦子有這個印象,
儘可能直接操作Buffer
。
child_process類也繼承自EventEmitter,也可以給它新增事件監聽函式
ls.on('close', () => {
const parts = output.split(/\s+/);
console.log(parts[0], parts[4], parts[8]);
})
當子程序退出時,會觸發close事件。回撥函式將資料按照空白符切割,然後打印出第1,5,9個欄位,這3個欄位分別對應許可權、大小和檔名。
1.4非同步讀/寫檔案
Node.js中有多種讀/寫檔案的方式,其中最簡單直接的是一次性讀取或寫入整個檔案,這種方式對小檔案很有效。另外的方式通過Stream 讀/寫流和使用Buffer儲存內容,下面是一次性讀/寫整個檔案的例子
新建檔案read-simple.js檔案,如下:
'use strict'
const fs = require('fs');
fs.readFile('target.txt', (err, data) => {
if (err) {
throw err;
}
console.log(data.toString())
})
執行命令
node read-simple.js
aaafffssssss
ffffffffeeff
注意readFile()的回撥函式的第一個引數是err,如果readFile()執行成功,則err的值為null。如果readFile()執行失敗,則err會是一個Error物件,這是Node.js中統一的錯誤處理方式,尤其是內建模組一定會按這種方式處理錯誤。本例中,如果有錯誤,我們直接就將錯誤丟擲,未捕獲的異常會導致Node.js直接中斷退出。
回撥函式的第2個引數是Buffer物件,如上節例子中的那樣。
一次寫入整個檔案的做法也是類似的, 如下:
'use strict'
const fs = require('fs');
fs.writeFile('target.txt', 'hello world', (err) => {
if (err) {
throw err;
}
console.log('File saved!');
})
這段程式碼的功能是將hello world寫入target.txt檔案(如果這個檔案不存在,則建立一個新的;如果已經存在,則覆蓋它)。如果有任何因素導致寫入失敗,則err引數會包含一個Error例項物件。
1.4.1 建立讀/寫流
分別用fs.createReadStream()和fs.createWriteStream()來建立讀/寫流。
'use strict'
require('fs').createReadStream(process.argv[2]).pipe(process.stdout);
執行下面命令,輸出結果如下:
node cat.js target.txt
hello world
也可以通過監聽檔案流的data事件來達到同樣效果,如下
'use strict'
require('fs').createReadStream(process.argv[2])
.on('data', chunk => process.stdout.write(chunk))
.on('error', err => process.stderr.write(`ERROR: ${err.message}\n`));
這裡使用process.stdout.write()輸出資料,替換原來的console.log。輸入資料chunk中已經包含檔案中的所有換行符,因此不再需要console.log來增加換行。
更更變的是.on()返回的是emitter物件,因此可以直接在後面鏈式得新增事件處理函式。
當使用EventEmitter時,最方便的錯誤處理方式就是直接監聽它的error事件。現在人為觸發錯誤,看看輸出。
node read-stream.js target.tx
ERROR: ENOENT: no such file or directory, open '\ilesystem\target.tx'
由於監聽了error事件,所以Node.js呼叫了錯誤監聽函式(並且正常退出)。如果沒有監聽error事件,並且恰好發生了執行錯誤,那麼node.js會直接丟擲異常,然後導致程序異常退出。
1.4.2 使用同步檔案操作阻塞事件迴圈
到目前為至,我們討論的檔案操作方法都是異常的,他們都是默默地在後臺履行I/O職責,只有事件發生時才會呼叫回撥函式,這是較妥當的I/O處理方式。
同時,fs模組中的很多方法也有相應的同步版本,這些同步方法大多以*Sync結尾,比如readFileSync。
當呼叫同步方法時,Nodejs程序會被堵塞,直到I/O處理完畢。也就是說,這時Node.js不會執行其他程式碼,不會呼叫任何回撥函式,不會觸發 任何事件,會完全停止下來,等待操作完成。如下程式碼:
'use strict'
const fs = require('fs');
const data = fs.readFileSync('target.txt');
process.stdout.write(data.toString());
1.4.3 檔案操作的其他方法
Node.js的fs模組還有方法,比如可以使用copy()方法複製檔案,使用unlink()方法刪除檔案,可以使用chmod方法變更許可權,可以使用mkdir()方法建立資料夾。
這些函式的用法類似,他們的回撥函式都接受相同的引數。
1.5 Node.js程式執行的2個階段
Node.js執行的2個階段
第一個階段是初始化階段,程式碼會做一些準備工作,匯入依賴的庫,讀取配置引數等。如果這個階段發生了錯誤,沒有太多辦法,最好是儘早丟擲錯誤並退出。
第二個階段是程式碼執行階段,事件迴圈機制開始工作。相當多的Node.js應用是網路應用,也就是會建立連線、傳送請求或者等待其他型別的I/O事件。這個階段不要使用同步的檔案操作,否則會阻塞其他的事件。