[Node.js] 基礎與檔案系統

全域物件

window 是瀏覽器的 global object 全域物件
但在 Node.js 中,全域物件不是 window,而是 global

可以試著在 js 檔中輸入

1
console.log(global);

並透過終端執行該 js 檔,可以看到如下畫面

可以發現有些與瀏覽器的 window 有一樣的 methods

這裡再試著將 js 檔改成如下

1
2
3
global.setTimeout(() => {
console.log("時間到!");
}, 3000);

執行 3 秒後,顯示如下


也可以省略 global,效果仍舊一樣

1
2
3
setTimeout(() => {
console.log("時間到!");
}, 3000);

到這裡可以發現 globalwindow 很相似,但還是有不同的地方,例如無法使用 DOM 的 functions,因為 documentwindow 中存在,在 global 卻不存在,不過這不是什麼大問題,一個負責前端網頁,一個負責後端操作,各司其職。

補充
Node.js 有 __dirname__filename 的關鍵字,用於表示當前檔案的目錄路徑及絕對路徑

1
2
console.log(__dirname); // 目錄路徑
console.log(__filename); // 檔案絕對路徑

執行結果


模組

在了解什麼是模組前,先直接看以下範例

建立 people.js 檔案


people.js 設置一個陣列常數,並將其列印出

1
2
const name = ['Joe', 'Peter', 'Mary'];
console.log(name);

test.js 則撰寫如下,require() 參數為想要導入的文件路徑

1
const x = require('./people');

分別執行兩個檔案,解果如下


註解掉 people.js 的 console.log(name);

1
2
const name = ['Joe', 'Peter', 'Mary'];
// console.log(name);

再次執行兩個檔案,結果什麼也沒發生


修改 test.js 列印出 x

1
2
const x = require('./people');
console.log(x);

結果為空物件

這樣的導入文件無法正確的訪問該文件的內容,我們必須手動的導出想要的數據或屬性。

將 people.js 改成如下

1
2
const name = ['Joe', 'Peter', 'Mary'];
module.exports = 'test';

test.js 維持不變印出 x

1
2
const x = require('./people');
console.log(x);

執行結果為印出 test,因為我們導出了 "test" 這個字串,並由 x 接收


現在將 people.js 導出項目改為 name 陣列

1
2
const name = ['Joe', 'Peter', 'Mary'];
module.exports = name;

test.js 的執行結果,x 接收 name 陣列的數據了


但萬一 people.js 多了一個陣列也想導出怎麼辦?
我們可以導出一個物件,將想要導出的數據作為該物件的屬性一起導出即可

1
2
3
4
5
6
7
const name = ['Joe', 'Peter', 'Mary'];
const age = [15, 23, 35];

module.exports = {
name, // or name:name
age // or age:age
}

(name 及 age 縮寫寫法與註解的寫法是同樣效果的,不了解者可以搜尋關於「使用 ES6 的物件屬性值簡寫」)

再次執行 test.js,x 變成包含以下數據的物件了


可以將 test.js 改成如下

1
2
3
const x = require('./people');
console.log('name:', x.name);
console.log('age:', x.age);

執行結果,現在可以自由的訪問導入文件其中的數據了


導入文件時,可以搭配使用 ES6 解構賦值的技巧

1
2
3
const { name, age } = require('./people');
console.log('name:', name);
console.log('age:' age);

達到一樣的效果


到這裡已經了解如何導出和導入自己製作的「modules 模組」。
上述的檔案中,people.js 即為 module 模組,使用 exports 導出、require 導入。
那麼什麼是模組呢? 簡單來說就像 JavaScript library,讓我們能夠在想要的應用程式中加入一些想要使用的功能。

內建模組

前面介紹了如何導出導入自己的模組,但 Node.js 其實本身就有內建的模組了,使用方式也與前面所說相同,只是不需要手動導出,只要導入就可以使用了。

檔案系統

使用 JavasScript 創建、讀取和刪除在電腦上文件,沒有 Node.js 是做不到的,這裡使用名為 fs (File System)的內建模組來達成這些功能。

首先將資料夾結構調整如下,docs 資料夾包含一個 note.txt 並隨意輸入些內容,作為稍後要操作的檔案


note.txt 內容

接著在 test.js 導入模組,由於 fs 是內建模組所以不需要手動導出

1
const fs = require('fs');

讀取文件

使用 fs.readFile(),第一個參數為要讀取的檔案路徑,第二個參數為 callback function,在 fs.readFile() 結束後執行,處理 error 及 data

1
2
3
4
5
6
7
8
const fs = require('fs');

fs.readFile('./docs/note.txt', (err, data) => {
if (err) {
console.log(err);
}
console.log(data);
});

執行結果,data 是關於一些緩衝區的訊息


可以改成 data.toString() 來顯示想看到的字串訊息

1
2
3
4
5
6
7
8
const fs = require('fs');

fs.readFile('./docs/note.txt', (err, data) => {
if (err) {
console.log(err);
}
console.log(data.toString());
});

執行


可以將要讀取的檔名改為不存在的檔案,觀察 err 參數

1
2
3
4
5
6
7
8
const fs = require('fs');

fs.readFile('./docs/note123.txt', (err, data) => {
if (err) {
console.log(err);
}
console.log(data.toString());
});

執行顯示找不到該檔案


補充
注意! fs.readFile() 是非同步函數,可以執行以下的 code 觀察

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

fs.readFile('./docs/note123.txt', (err, data) => {
if (err) {
console.log(err);
}
console.log(data.toString());
});

console.log('我是最後一行 code');

執行


寫入檔案

使用 fs.writeFile(),參數有三個,第一個為要寫入的檔案路徑,第二個為要寫入的內容,第三個為 callback function,但只帶一個 err 參數

1
2
3
4
5
const fs = require('fs');

fs.writeFile('./docs/note.txt', 'Hello World!', (err) => {
console.log('已寫入檔案!');
});

執行

note.txt 原本的內容不見了,變成寫入的內容

這次將檔案路徑修改成不存在的檔案 note2.txt

1
2
3
4
5
const fs = require('fs');

fs.writeFile('./docs/note2.txt', 'Hello World!', (err) => {
console.log('已寫入 note2 檔案!');
});

執行

多了 note2.txt 檔案

note2.txt 內容

可以發現當寫入檔案的路徑不存在時,會自動創建檔案並寫入內容,若是存在則覆寫原本存在的檔案

創建資料夾

與 cmd 的 mkdir 指令相像。
使用 fs.mkdir(),第一個參數為要創建的資料夾路徑,第二個參數為 callback function

1
2
3
4
5
6
7
8
const fs = require('fs');

fs.mkdir('./source', (err) => {
if (err) {
console.log(err);
}
console.log('資料夾已創建!');
});

執行

新增了 source 資料夾

再執行一次,顯示 err 訊息,表示該資料夾已存在了

我們可以結合 fs.existsSync() 的使用來確認檔案或資料夾是否存在,如果資料夾存在回傳 true,不存在則回傳 false
而以下的 code 會在資料夾不存在時創建,存在時則將其刪除,刪除目錄(資料夾)使用 fs.rmdir()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

if (!fs.existsSync('./source')) {
fs.mkdir('./source', (err) => {
if (err) {
console.log(err);
}
console.log('資料夾已創建!');
});
} else {
fs.rmdir('./source', (err) => {
if (err) {
console.log(err);
}
console.log('資料夾已刪除!');
})
}

執行

剛剛創建的 source 資料夾刪除了

反之現在再執行一次

source 資料夾又創建了


刪除文件

使用 fs.unlink() 刪除指定的文件,第一個參數為要刪除的檔案路徑,第二個參數為 callback function
這裡一樣結合 fs.existsSyns() 確認檔案是否存在,若存在則執行刪除檔案的動作

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

if (fs.existsSync('./docs/delete.txt')) {
fs.unlink('./docs/delete.txt', (err) => {
if (err) {
console.log(err);
}
console.log('檔案已刪除!');
});
}

在執行上面程式前,先在 docs 資料夾創建 delete.txt 檔案

執行

delete.txt 檔案刪除了


Streams 流

當我們有一筆很大的 data 數據要使用時,必須先將其全部一次載下來才能使用,這個過程可能會花很長的時間,但使用 streams (流) 的概念,讓我們可以在 data 完全載下來前就可以開始使用部分的 data 了,立即使用而不是等待。

首先準備一個要讀取的數據 data.txt

data.txt 內容


使用 fs.createReadStream() 建立一個 stream 物件,命名為 readStream
並對 readStream 監聽 'data' 事件,表示每當獲取新的 data chunk (數據塊)就會觸發,執行 callback function

1
2
3
4
5
6
7
const fs = require('fs');
const readStream = fs.createreadStream('./docs/data.txt');

readStream.on('data', (chunk) => {
console.log('----- NEW CHUNK -----'); // 將數據塊區分
console.log(chunk.toString());
});

執行結果可以看到區分的數據塊


注意上面的 chunk 使用 toString() 才能正確顯示內容,也可以對 readSteam 物件設定 {encoding: 'utf8'} 會自動編碼,效果一樣

1
2
3
4
5
6
7
const fs = require('fs');
const readStream = fs.createreadStream('./docs/data.txt', {encoding: 'utf8'});

readStream.on('data', (chunk) => {
console.log('----- NEW CHUNK -----'); // 將數據塊區分
console.log(chunk);
});

可以使用 stream 讀取檔案,同樣的也可以用於寫入檔案
使用 fs.createWriteStream() 創建物件,結合前面讀取檔案,並將內容寫入另一個檔案中

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');
const readStream = fs.createreadStream('./docs/data.txt');
const writeStream = fs.createWriteStream('./docs/data2.txt');

readStream.on('data', (chunk) => {
// console.log('----- NEW CHUNK -----'); // 將數據塊區分
// console.log(chunk.toString());
writeStream.write('\nNEW CHUNK\n');
writeStream.write(chunk);
});

這裡寫入的檔案為 data2.txt,並沒有先建立,執行後會自動建立

執行結果,data2.txt 內容


此外 stream 還有 pipe 管子的概念,有些像是接水的管道,這裡只做簡單的示範

1
2
3
4
5
const fs = require('fs');
const readStream = fs.createReadStream('./docs/data.txt', {encoding: 'utf8'});
const writeStream = fs.createWriteStream('./docs/data2.txt');

readStream.pipe(writeStream);

執行後可以發現 data2.txt 的內容與 data.txt 內容完全一樣,能用更少的程式碼達到一樣的效果,關於 pipe 概念更深入的內容不是筆者目前筆記學習的重點,所以就不多介紹了。


參考資料
The Net Ninja | Node.js Crash Course Tutorial #2 - Node.js Basics