1. 程式人生 > 實用技巧 >Node.js學習

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按照如下方式來執行

  1. 載入程式碼,從開始執行到最後一行,在命令列輸出Now watching target.txt for changes.....
  2. 由於呼叫了fs.watch,所以node.js不會退出
  3. 它等待著fs模組監聽目標檔案的變化
  4. 當目標檔案發生變化時,執行回撥函式
  5. 程式繼續等待,繼續監聽,還不能退出

事件迴圈會一直持續下去,直到沒有任何程式碼需要執行,沒有任何事件需要等待,或程式因為其他因素退出。比如程式執行時候發生錯誤丟擲異常,異常有沒有被正確捕獲到,通常會導致程序 退出。

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事件。這個階段不要使用同步的檔案操作,否則會阻塞其他的事件。