Day4 - Node.js 內置模組

內置模組

前言

上一篇我們講了一大推東西,終於撰寫了第一個簡單的 Node.js 專案(雖然範例是透過 Node.js 官方提供的),接下來我們會針對 Node.js 的內置模組來做一些介紹。

Node.js 內置模組

首先我們在前面寫了一點小範例(也不算寫,根本是把官方文件範例拿來用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('node:http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

在前面的範例中,我們並沒有很深刻的去認識它,剛好這一篇我們要來認識一下 Node.js 中的內置模組,所以就順便來解釋一下這個範例。

首先什麼是內置模組?簡單來說就是 Node.js 中內建的模組,也就是說你不需要額外安裝任何東西,就可以直接使用的模組,而這些模組都是由 Node.js 官方所提供的,因此我們可以直接使用,這就是「內置模組」的意思。

在上面的範例中,我們就使用到了 Node.js 的內置模組,也就是 http 模組,透過這個模組我們可以很快速的建立一個簡單的伺服器,而這個模組就是 Node.js 官方所提供的,因此我們可以直接使用。

通常內置模組大多都是這樣寫 require('node:<模組名稱>')

有個大概簡單觀念之後,我們就來逐行解釋一下前面的範例吧!

  • 第一行:const http = require('node:http');
    • 使用 require 語法來引入 http 模組,並且將它指派給 http 這個常數,這樣我們就可以透過 http 來使用 http 模組了。
  • 第二行:const hostname = '127.0.0.1';
    • 定義了一個 hostname 常數,並且將 '127.0.0.1' 指派給它,後面我們會使用到。
  • 第三行:const port = 3000;
    • 定義了一個 port 常數,並且將 3000 指派給它,也是後面會使用到。
  • 第五行:const server = http.createServer((req, res) => { ... }
    • 我們使用 http 模組中的 createServer 方法,並且將一個函式作為參數傳入,這樣就做好一個簡單的伺服器,但是此時還沒有啟動。
  • 第六行:res.statusCode = 200;
    • 我們將 res 物件中的 statusCode 屬性指派為 200,這樣就可以讓瀏覽器知道這個請求是成功的。
  • 第七行:res.setHeader('Content-Type', 'text/plain');
    • 用了 res 物件中的 setHeader 方法,將 Content-Type 設定為 text/plain,這樣瀏覽器就會知道我們接下來的回應會是一個純文字。
  • 第八行:res.end('Hello, World!\n');
    • 使用 res 物件中的 end 方法,並且將 Hello, World!\n 作為回應的內容,這樣瀏覽器就會看到 Hello, World! 這個字串。
  • 第十行:server.listen(port, hostname, () => { ... }
    • 最後 server 中有一個 listen 方法,我們可以透過這個方法來啟動伺服器,並且指定 porthostname,這樣瀏覽器才能夠連線到伺服器。

透過上面的程式碼,我們就可以建立一個簡單的伺服器,只需要在專案啟動後,就可以在瀏覽器中輸入 http://127.0.0.1:3000/ 來看到 Hello, World! 這個字串。

這邊也額外補充一下 res 代表著回應(Response),而 req 代表著請求(Request),這兩個物件都是由 http 模組所提供的,我們可以透過這兩個物件來操作請求與回應;由於當使用者請求時,我們必須回應給使用者我們即將回傳的格式,因此我們使用 res.setHeader('Content-Type', 'text/plain'); 來告訴瀏覽器我們即將回傳的格式是 text/plain,這樣瀏覽器就會知道我們接下來的回應會是一個純文字。

偷懶

(可惡,我以為把前面範例講完就好了)

你以為 Node.js 的內置模組只能用來架設一個簡單的伺服器嗎?

錯!Node.js 的內置模組還有很多很多,這邊我也列出一些內置模組:

  • http:用於建立 HTTP 伺服器。
  • https:用於建立 HTTPS 伺服器。
  • fs:用於操作檔案,例如:讀取檔案、寫入檔案、刪除檔案等等。
  • path:處理檔案路徑的工具。
  • os:用於操作作業系統的工具。
  • crypto:提供加密和解密功能的模組。
  • zlib:提供壓縮和解壓縮功能的模組。
  • util:提供一些實用工具的模組。
  • stream:用於處理 Stream 的模組,可以處理大型檔案或資料的讀取和寫入。
  • querystring:用於處理 URL 查詢字串的模組。

那麼由於內置模組並不會每一個都介紹到,因為篇幅實在太多了,因此這邊我只會拉幾個比較常見的來介紹,那麼前面已經有介紹過 http 模組了,所以就不會再額外說明 http 模組囉。

往下閱讀之前,我想先再一次重複,基本上你只要看到 require('node:<模組名稱>'),就代表著它是使用 Node.js 的內置模組,因此你不需要額外安裝,什麼是額外安裝呢?也就是你不用再額外輸入 npm install <模組名稱> 來安裝,因為 Node.js 官方已經幫你準備好了。

fs 模組

fs 完整名稱是 File system 的縮寫,也就是檔案系統的意思,這個模組可以讓我們操作作業系統的檔案,例如:讀取檔案、寫入檔案、刪除檔案等等。

首先先請你依照以下指令建立一個檔案以及資料夾

1
mkdir fs-example
1
touch fs-example/text.txt
1
echo "Hello, World" > fs-example/text.txt

接下來請你建立一個 index.js 檔案,然後我們就要來認識最基本的 fs 模組了,底下是一個簡單的範例:

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


fs.readFile('./text.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}

console.log(data);
});

Note
請不要忘記輸入 npm init -y 來初始化專案,這樣才能夠使用 npm install 來安裝套件以及增加 "start": "node index.js" 指令唷。

在上面的程式碼中,我們使用了 fs 模組中的 readFile 方法,這個方法可以讀取特定檔案,並且將檔案內容回傳給我們,這邊我們將 text.txt 這個檔案讀取出來,並且將內容回傳給我們,這樣我們就可以在終端機中看到 Hello, World 這個字串

readFile

有讀取檔案的語法,當然就會有新增、刪除跟寫入的語法啦~

但是因為 JavaScript 本身在檔案處理這一塊是屬於非同步的,因此我們必須使用 callback 來處理,但是使用 callback 的方式就會發生所謂的「Callback Hell」問題發生

callback hell

那麼 Callback Hell 會導致程式碼過於巢狀,讓程式碼變得難以閱讀,因此 Node.js 在 10.0.0 版本之後,提供了 node:fs/promises 模組可以使用,因此以剛剛的讀取檔案範例來講,我們可以改成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('node:fs/promises');

const readFile = async () => {
try {
const data = await fs.readFile('./text.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
};

readFile();

Note
Callback 的概念就是將函式做為另一個函式的參數傳入並呼叫,也就是當某件事情完成後,我們才會去執行這個函式,這個函式就是 callback,而 Callback Hell 就是 Callback 的巢狀結構,這個巢狀結構會讓程式碼變得難以閱讀。

那麼往下介紹之前,如果你對於 Promise 沒有概念或者不熟悉的話,我會推薦你閱讀我先前寫文章

如果沒有任何問題的話,這邊我就即將繼續介紹其他檔案操作的方法囉~

有讀取語法,當然就會有寫入語法,而寫入的語法就是 fs.writeFile

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('node:fs/promises');

const updateFile = async () =>{
try {
await fs.writeFile('./text.txt', '這是更新後的檔案內容。');
console.log('檔案編輯成功!');
} catch (err) {
console.error('無法編輯檔案 :', err);
}
}

updateFile();

這時候你應該會發現一個問題,原本我們在前面是使用 echo "Hello, World" > fs-example/text.txt 這個指令來新增檔案的內容,但是使用 fs.writeFile 這個方法之後,卻沒有新增檔案內容,而是直接覆蓋掉原本的檔案內容,變成了 '這是更新後的檔案內容。',如果這是你預期的結果那麼就沒問題,但如果不是的話,那麼我們就需要使用 fs.appendFile 這個方法來新增檔案內容才對:

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('node:fs/promises');

const appendUpdateFile= async () => {
try {
await fs.appendFile('./text.txt', '這是新增的內容。');
console.log('檔案編輯成功!');
} catch (err) {
console.error('無法編輯檔案 :', err);
}
}

appendUpdateFile();

fs.appendFile 會直接在原本的檔案內容後面新增內容,而不是覆蓋掉原本的檔案內容,這樣就不會發生覆蓋掉原本檔案內容的問題了。

Note
你會發現 fs.appendFile 會是在最後一行的字去新增,如果你是希望新增時同時換行的話,可以在字串前後加上 \n,這樣就可以換行了,ex:await fs.appendFile('./text.txt', '\n這是新增的內容。\n');

接下來就是刪除檔案的語法,刪除檔案的語法就是 fs.unlink 格外簡單許多:

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('node:fs/promises');

const deleteFile = async () => {
try {
await fs.unlink('./text.txt');
console.log('檔案刪除成功!');
} catch (err) {
console.error('無法刪除檔案 :', err);
}
}

deleteFile();

基本上我們已經認識了幾個語法

  • fs.readFile:讀取檔案
  • fs.writeFile:寫入檔案(我認為應該叫覆蓋檔案比較好)
  • fs.appendFile:新增檔案內容
  • fs.unlink:刪除檔案

當然 fs 底下還有很多功能可以使用,如果一一介紹的話,我看這一篇除了直接爆掉之外,腦袋應該也會爆掉吧?

BOOM

path 模組

path 是一個專門處理檔案路徑的模組,很有趣吧?竟然還有一個專門處理檔案路徑的模組,路徑不是就 ../ or ./ 嗎?為什麼還需要一個專門處理檔案路徑的模組呢?

其實原因是出在不同的操作系統上,路徑的表示方式是不同的,例如:在 Windows 上路徑是這樣的 C:\Users\user\Desktop\test.txt,而在 Mac 上路徑是這樣的 /Users/user/Desktop/test.txt,因此我們需要一個專門處理檔案路徑的模組,這樣我們才能夠確保在不同的操作系統上都能夠正確的處理檔案路徑。

那麼我們就來看看 path 模組的使用方式?

1
2
3
4
5
const path = require('node:path');

const filePath = path.join(__dirname, 'text.txt');

console.log(filePath);

Note
請記得在專案中建立一個 text.txt 檔案,並且在檔案中輸入任意內容。

有沒有很簡單呢?上面我們使用了 path 模組中的 join 方法,這個方法可以將多個路徑組合起來,並且回傳一個組合後的路徑,這邊我們將 __dirnametext.txt 組合起來,這樣就可以得到一個完整的路徑了。

那麼這邊有趣的事情來了 __dirname 是什麼東西?當你直接輸出 __dirname 你會得到當前檔案的路徑,例如:/Users/user/Desktop/fs-example,所以你應該是想著…

__dirname 是哪裡來的?你又沒有宣告這個變數?為什麼他能夠取得當前專案的目錄?」

在 Node.js 中有一個叫做 global 的物件,這個物件可以讓我們在任何地方都可以使用,而 __dirname 就是 global 物件中的一個屬性,這個屬性可以讓我們取得當前檔案的路徑,因此我們可以透過 __dirname 來取得當前檔案的路徑。

Note
Node.js 的全域物件是叫 global,而瀏覽器則是 window,這兩個物件都是可以在任何地方使用的,但是在 Node.js 中,我們不需要使用 global 來取得全域物件,因為 Node.js 預設就會將 global 設定為全域物件,因此我們可以直接使用 __dirname 來取得當前檔案的路徑;你可以將 global 當作是瀏覽器的 window 概念。

那麼只有這樣嗎?不,其實你可以透過 path 取得檔案的檔名或副檔名,例如:

1
2
3
4
5
6
const path = require('node:path');

const filePath = path.join(__dirname, 'text.txt');

console.log(path.basename(filePath));
console.log(path.extname(filePath));

透過 path.basename 我們可以取得檔案的檔名,透過 path.extname 我們可以取得檔案的副檔名,這樣就可以讓我們更方便的取得檔案的檔名或副檔名了。

os 模組

os 模組全名是 Operating System 的縮寫,也就是作業系統的意思,這個模組可以讓我們取得作業系統的資訊,例如:作業系統的名稱、作業系統的版本、作業系統的記憶體等等。

我目前是使用 Mac,因此我們就來看看 os 模組可以取得哪些資訊~

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

const osName = os.platform();
console.log('操作系統名稱:', osName);

const osVersion = os.release();
console.log('操作系統版本:', osVersion);

const cpuInfo = os.cpus();
console.log('CPU 資訊:', cpuInfo);

const networkInterfaces = os.networkInterfaces();
console.log('網路介面資訊:', networkInterfaces);

const totalMemory = os.totalmem();
console.log('總內存量:', totalMemory);

基本上你應該輸入完畢後,並執行是可以在終端機看到相關資訊的,而我這邊就不額外提供了,那麼因為 os 模組的相關操作非常多,所以我這邊就不一一介紹了。

Note
Mac 的作業系統名稱是 darwin,而 Windows 的作業系統名稱是 win32

crypto 模組

crypto 主要跟加解密有關,比較常見的場景就是在使用者註冊時,我們會將使用者的密碼進行加密,這樣就可以確保使用者的密碼不會被外洩,而當使用者登入時,我們就會將使用者輸入的密碼進行加密,然後再與資料庫中的密碼進行比對,這樣就可以確保使用者輸入的密碼是正確的。

crypto 也提供相當多的方式,例如:sha256、sha512、md5 等等,這邊我們就來看個簡易的範例吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
const crypto = require('node:crypto');

const password = '123456';

const hashPassword = (data) => {
const hash = crypto.createHash('sha256');
hash.update(data);
return hash.digest('hex');
};

const hashedPassword = hashPassword(password);

console.log(hashedPassword);

簡單來講,我們使用了一個 sha256 進行雜湊,將使用者的密碼變成一串亂碼,這樣子就可以確保使用者的密碼不會被外洩,什麼意思呢?我們在開發系統時總是會需要記錄使用者的帳號及密碼,但為了資安上的考量,我們並不會將使用者的密碼以明碼的方式儲存,而是會將密碼進行雜湊,這樣就可以確保使用者的密碼不會被外洩,因為即使是系統管理員也無法知道使用者的密碼是什麼。

Note
明碼的意思就是沒有做任何處理的密碼,例如密碼是 12345678 儲存時也就是 12345678,這樣就是明碼。

但使用 sha256 只是一個基本的方式,通常還會搭配 Salt(加鹽)的方式,只是這就不是這篇的重點了,因此我就不多做介紹了。

回頭來講 sha256 的部分,當我們使用了 sha256 進行雜湊後,我們該如何在使用者登入時比對呢?基本上只要使用者輸入的密碼進行雜湊,然後再與資料庫中的雜湊後的密碼進行比對就可以囉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const crypto = require('node:crypto');

const password = '123456';

const hashPassword = (data) => {
const hash = crypto.createHash('sha256');
hash.update(data);
return hash.digest('hex');
};

const hashedPassword = hashPassword(password);

// 假設這是資料庫中存儲的使用者密碼雜湊值
const storedHashedPassword = hashedPassword;

// 使用者輸入的密碼
const userInputPassword = '123456';

const hashedInputPassword = hashPassword(userInputPassword);

// 將雜湊後的使用者輸入密碼與資料庫中存儲的密碼雜湊值進行比較
if (hashedInputPassword === storedHashedPassword) {
console.log('密碼正確,驗證成功!');
} else {
console.log('密碼錯誤,驗證失敗!');
}

很簡單吧?但這只是簡單的範例,實際上我們還需要搭配 Salt(加鹽)的方式,這樣才能夠更加確保使用者的密碼不會被外洩唷(很重要)。

那麼這一篇我想要先來做個結尾了,因為 Node.js 內建模組實在有點多,如果每個都介紹的話,量其實有點大 QQ

因此這一篇我主要介紹的還是以那幾個比較常見的為主,其餘的部分可以依照自己開發需求來使用哩~

我們下一篇見囉~

關於 node: 後續補充

剛好有人提問了以下…

「請問範例中 node: 這個前綴好像有加沒加都可以正常運作?那這樣這有什麼差嗎?」

這個問題問得很好,確實以我們前面的範例來講:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('node:http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

就算改成了以下:

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

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

這件事情需要先提到一個東西,也就是 Node.js 的 require 緩存機制,基本上模組在引入時都會被緩存起來(放在 require.cache 中),這樣就可以避免重複引入模組,因此當我們在引入模組時,Node.js 會先檢查模組是否已經被緩存,如果有的話就會直接使用緩存的模組,如果沒有的話就會重新載入模組,這樣除了可以大大的提高效能之外,也可以避免重複引入模組,並共用某些狀態。

而這個 node: 前綴詞是大約在 Node.js 14 左右引入的(吧?),基本上當你使用 node: 前綴詞時,Node.js 將會繞過 require 的快取機制,什麼意思呢?就算 require.cache 中有這個模組,也依然會被忽略掉,直接返回並重新載入模組,這樣就可以確保你使用的是最新的模組,而不是快取的模組。

因此,使用 node: 就是在跟 Node.js 講說…

「嘿!你給我直接去找 node: 後面的模組,不要去找快取的模組!」

這也是為什麼有無使用 node: 前綴詞都可以正常運作的原因哩~

碎碎念

最近發生了一點事情,差點鐵人賽文章就沒辦法寫下去了,還好用肝的把它給肝出來了!