Day5 - Node.js Modules

Modules

前言

前一篇我們認識了一些內置模組,接下來這一篇我們要來認識一個有點特別但又好像不怎麼特別的 Modules

Modules 是什麼?

如果你是從前端跳過來的開發者並且有一定開發經驗,那麼應該會對於 Modules 比較有一點概念,如果是剛入門開發者,可能就沒有那麼熟悉 Modules 的觀念,但這一段也沒關係,我特別挪出這一篇來說明 Modules 是什麼,因為在實戰開發上是很常看到這個東西的。

首先 Node.js 支援兩種 Modules,分別是 CommonJS 和 ECMAScript Modules(ES Modules)。

那麼什麼是 CommonJS 以及 ECMAScript 呢?這個就會有一點歷史問題了,這兩個是一個完全不同的模組系統及規範。

有趣的事情來了,為什麼 Node.js 不直接支援 ECMAScript 就好,還要再支援一套 CommonJS 呢?

思考恐龍

主要原因是出在 JavaScript 本身在早期時,並沒有所謂的模組化概念,畢竟 JavaScript 在歷史上來講是被設計用於前端網頁的腳本語言,因此並沒有提供原生的模組化系統,這就導致了早期開發者在開發程式碼時,被迫將一推程式碼寫在一個檔案裡面,這樣的開發方式導致了程式碼的可讀性及可維護性大大降低,因此開發者們開始思考如何將程式碼模組化,那麼身為運行在伺服器上的 Node.js 基本上是不允許這種事情發生的,因此就自己定義了一套模組系統,也就是 CommonJS,所以認真來講 CommonJS 可是 ECMAScript 的老祖宗呢。

圖源網路

Note
目前業界上比較常見的模組化系統為 CommonJS 及 ES Modules,而這些也稱之為模組化規範,當然也有另一個 AMD(Asynchronous Module Definition),只是以我自己開發經驗來講,我也沒用過 AMD 就是了。

接下來聊一下何謂模組化吧!

模組化就是將程式碼分割成一個個的模組,你可以想像成原本是一個一體成型的物品或東西,透過模組化變成了類似積木一樣的東西,每個模組都有自己的功能,並且可以被其他模組所引用,這樣就可以達到程式碼的可讀性及可維護性,單看片面的文字描述可能會有點抽象,我們來看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

const multiply = (a, b) => {
return a * b;
};

console.log(add(1, 2));
console.log(subtract(1, 2));
console.log(multiply(1, 2));

以上面的範例來講,我們可以看到我們將各自的計算方法封裝成了函式,但當功能越來越多跟龐大時,我們的 index.js 就會變得很臃腫且難以維護,因此我們就可以透過模組化的觀念將計算的相關函式,抽離出來做為一個模組

1
2
3
4
5
6
7
8
9
10
// index.js
const {
add,
subtract,
multiply,
} = require('./calculator');

console.log(add(1, 2));
console.log(subtract(1, 2));
console.log(multiply(1, 2));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// calculator.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

const multiply = (a, b) => {
return a * b;
};

exports.add = add;
exports.subtract = subtract;
exports.multiply = multiply;

上面的範例中,可以看到我們將原有在 index.js 中的程式碼,拆成了兩個檔案,分別是

  • index.js:主要的程式碼
  • calculator.js:封裝計算相關函式的模組

透過這樣子的模組化,我們 index.js 就變得很簡潔,並且可以讓我們的程式碼變得更加可讀性及可維護性,也比較不用擔心程式碼會變得很大,上面就是一個實際的模組化概念。

對於模組化有概念之後,我們可以來看一下 CommonJS 及 ES Modules 的差異。

CommonJS

首先前面有提到 CommonJS 是 Node.js 為了解決模組化而出現的模組化系統,那麼 CommonJS 就變成不能不認識的東西了。

CommonJS 的使用方式很簡單,基本上就是 requiremodule.exports or exports 兩者一起搭配,在前面的範例中我們其實已經有示範了,所以一樣就拿前面的範例來回憶就好

1
2
3
4
5
6
7
8
9
10
// index.js
const {
add,
subtract,
multiply,
} = require('./calculator');

console.log(add(1, 2));
console.log(subtract(1, 2));
console.log(multiply(1, 2));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// calculator.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

const multiply = (a, b) => {
return a * b;
};

exports.add = add;
exports.subtract = subtract;
exports.multiply = multiply;

雖然 CommonJS 使用上是沒有什麼太大問題,但是有幾個要點要注意…

一個檔案只能有一個 module.exports 或是 exports,如果有多個的話,只會回傳最後一個

1
2
3
4
5
6
7
8
// moduleA.js
// 你可以試著註解任一行,看看結果會是什麼
exports.a = 1;
module.exports = { b: 2 };

// app.js
const moduleA = require('./moduleA');
console.log(moduleA); // { b: 2 }

因此你可以有多個 exports,但 module.exportsexports 只能擇一。

引入模組時要注意引入方式,如果是引入自己寫的模組,要加上 ./ or ../,如果是引入內置模組,則不需要加上 ./ or ../(也就是所謂的相對及絕對路徑),Node.js 會假設這是一個第三方模組 (可能是你使用 npm 安裝的模組),並從 node_modules 目錄中尋找相應的模組檔案

1
2
3
4
5
6
7
8
// 引入自己寫的模組
const moduleA = require('./moduleA');

// 引入內置模組
const fs = require('fs');

// 引入第三方模組
const _ = require('lodash');

Node.js 在搜尋時,是依序從「自訂模組」、「內置模組」、「第三方模組」的順序來尋找。

基本上如果你引入的模組名稱以是 ./../ 開頭,Node.js 會視它為一個「自訂模組」,它會根據相對路徑或絕對路徑來尋找該模組檔案,如果沒有使用相對路徑或絕對路徑的話,則會認為你是在引入一個「內置模組」或「第三方模組」,並且會從 node_modules 目錄中尋找相應的模組檔案。

另外還有一點是 CommonJS 在引入是採用的是「同步」的方式,也就是說當你引入一個模組時,會等到該模組載入完成後才會繼續往下執行,什麼意思呢?讓我們看個範例:

1
2
3
4
// moduleA.js
console.log('模組A開始載入');
exports.message = 'Hello, World!';
console.log('模組A載入完成');
1
2
3
4
5
6
// index.js
console.log('程式開始執行');
const moduleA = require('./moduleA');
console.log('模組A已經載入');
console.log('訊息:', moduleA.message);
console.log('程式執行結束');

執行後你會得到以下結果

1
2
3
4
5
6
程式開始執行
模組A開始載入
模組A載入完成
模組A已經載入
訊息: Hello, World!
程式執行結束

在某些狀況下,這種同步引入的問題會導致在處理大型模組時,會造成程式的執行時間變長,因此後來隨著 ES Modules 的出現,也就有了非同步引入的方式。

Note
CommonJS 最早的名稱其實是 ServerJS,而且是由 Mozilla 的工程師提出的,後來才改名為 CommonJS,因此 CommonJS 也可以稱之為 ServerJS;其實早在 2013 年就有人提出廢棄 CommonJS,但至今 CommonJS 仍然是 Node.js 預設的模組系統,只是未來可能會被 ES Modules 取代。

ES Modules

那麼前面有提到 ES Modules 是 ECMAScript 的模組化系統,由於我先前已經有寫過文章介紹過,所以好奇的話建議直接閱讀我之前寫的文章 什麼是 ESM(ES6 Modules or JavaScript Modules) 呢?,這邊就不會花太多時間去深入介紹了。

雖然之前已經介紹過 ES Modules,但不代表我這邊就這樣留空帶過,而是要來講一下 ES Modules 在 Node.js 中的使用方式。

如果你想要在 Node.js 中使用 ES Modules,你必須在你的 package.json 中加上以下設定

1
2
3
4
5
{
// ...略過其他設定
"type": "module",
// ...略過其他設定
}

這樣子 Node.js 才會知道你要使用 ES Modules,而不是 CommonJS,因為預設狀況下 Node.js 是使用 CommonJS 的,所以你必須要告訴 Node.js 你要使用 ES Modules。

接下來這邊就簡單帶一下將前面 calculator.js 改寫成 ES Modules 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// calculator.js
const add = (a, b) => {
return a + b;
};

const subtract = (a, b) => {
return a - b;
};

const multiply = (a, b) => {
return a * b;
};

export { add, subtract, multiply };
1
2
3
4
5
6
// index.js
import { add, subtract, multiply } from './calculator.js';

console.log(add(1, 2));
console.log(subtract(1, 2));
console.log(multiply(1, 2));

另外剛剛有提到 CommonJS 在引入是採用的是「同步」的方式,這種方式對於引入大型模組時,會造成程式的執行時間變長,因此這邊就可以使用 ES Modules 的「非同步」引入方式來解決這個問題

1
2
3
4
5
6
7
// index.js
console.log('程式開始執行');
import('./moduleA.js').then((moduleA) => {
console.log('模組A已經載入');
console.log('訊息:', moduleA.message);
});
console.log('程式執行結束');

執行後你會得到以下結果

1
2
3
4
5
6
程式開始執行
程式執行結束
模組A開始載入
模組A載入完成
模組A已經載入
訊息: Hello, World!

這就是 ES Modules 的威力。

Note
實戰上還是比較常見 CommonJS 的,畢竟是 Node.js 預設模組系統。

.mjs & .cjs 檔案

最後來介紹一個更有趣的東西,也就是 .mjs.cjs 這兩個副檔名。

OMG,.mjs.cjs 又是什麼鬼東西?我們都知道 .js 是代表著 JavaScript 的檔案,那麼 .mjs.cjs 又是什麼呢?

難不成是 Meow JavaScript 跟 Cat JavaScript?

Cat

當然不是好嗎!

.mjs.cjs 的正確名稱分別是 ECMAScript Modules 跟 CommonJS Modules。

Note
也有人戲稱 .mjs 是 Michael Jackson Script,但這個只是開玩笑的,不要當真。

那麼 .mjs.cjs 與原本的 .js 有什麼樣的不同?其實如同前面所講的,主要是明確的讓開發者以及 Node.js 知道我們目前這個檔案所使用的模組系統,也就是說當你使用 .mjs 時,Node.js 會知道你目前使用的是 ES Modules,而使用 .cjs 時,Node.js 會知道你目前使用的是 CommonJS。

因此這個副檔名的出現,主要是為了讓開發者可以明確的知道目前這個檔案所使用的模組系統,而不是像以前一樣,只能透過 package.json 中的 type 來判斷目前使用的模組系統,你也可以思考一下,如果我們在 Node.js 中將 type 改成 esm 的話,單看 .js 你會知道這個檔案是使用 ES Modules 還是 CommonJS 嗎?因此這個副檔名就是在這個場合上使用的。

最後這邊針對副檔名的部分,我們可以簡單的歸納一下

  • .js:Node.js 預設使用 CommonJS,除非你在 package.json 中設定 typemodule,才會使用 ES Modules
  • .mjs:明確的告訴 Node.js 這個檔案使用 ES Modules
  • .cjs:明確的告訴 Node.js 這個檔案使用 CommonJS

透過上面的歸納,你就可以知道目前這個檔案使用的模組系統,而不用再去看 package.json 中的 type 了。

那麼這這一篇也差不多要告一個段落了,我們下一篇見。

碎碎念

其實以我開發到現在,還是主要以 CommonJS 為主,畢竟是 Node.js 預設的模組系統,而且有些框架在轉用 ESM 時,還是會有一些問題,因此我個人還是會以 CommonJS 為主。