我最近開始學習 Node.js,而我最初接觸到的概念之一就是 模組。我已經擔任前端開發者幾年了,雖然我對模組表面上的概念已有了解,但我從未深入探討它們。直到現在。

模組本質上只是程式碼檔案。這是最簡單的說法。但如果我們想更深入探討模組的真正意義、它們存在的原因,以及如何使用它們,那還有更多內容需要探討。

Node.js 中的模組是任何應用程式的基石。為什麼?因為它們:

  1. 將程式碼組織成可管理的檔案
  2. 提供封裝
  3. 防止全域命名空間污染
  4. 改善程式碼的可維護性和可重用性

讓我們逐一詳細說明。

1. 將程式碼組織成可管理的檔案

其核心是關於 職責分離,即每段程式碼應負責一件事,且僅負責一件事。

讓我們看一個簡單的範例。假設我們有一些基本的數學運算,都存在於單一檔案 app.js 中:

function add(x, y) {
  return x + y;
}

function subtract(x, y) {
  return x - y;
}

function multiply(x, y) {
  return x * y;
}

console.log(`Sum is: ${add(5, 3)}`); // Output: 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // Output: 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // Output: 15

Enter fullscreen mode Exit fullscreen mode

這是一個小而簡單的範例,單獨看起來並不特別混亂。但透過使用模組,我們可以讓它更乾淨、更組織化,並且隨著成長更容易維護。

為此,我們將建立一個名為 math.js 的新檔案,其唯一目的是處理數學運算:

function add(x, y) {
  return x + y;
}

function subtract(x, y) {
  return x - y;
}

function multiply(x, y) {
  return x * y;
}

module.exports = { add, subtract, multiply };

Enter fullscreen mode Exit fullscreen mode

我們所做的只是將數學運算移到這個新檔案中。為了在應用程式的其他地方使用它們,我們需要匯出它們,而在 Node.js 中有不同的方式可以做到這一點。在這裡,我們使用 CommonJS,這是 Node.js 從一開始就預設使用的模組系統(相對於較新的標準 ES Modules,本文不會涵蓋)。

現在,為了在 app.js 中使用這些函式,我們使用 Node 內建的 require() 函式匯入它們:

const { add, subtract, multiply } = require('./math');

console.log(`Sum is: ${add(5, 3)}`); // 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // 15

Enter fullscreen mode Exit fullscreen mode

就這樣,app.js 不再需要知道數學是如何完成的,只需要知道它可以 require 這些函式並使用它們。數學邏輯存在於一個地方,具有一個明確的職責。

2. 封裝

封裝意味著將某事物內部運作的細節隱藏起來,只暴露程式碼其他部分實際需要的內容。使用模組的人不應該需要知道或觸及它的內部變數或輔助邏輯,只需要知道它的公開介面。

讓我們來看一個簡單的案例:一個計數器模組直接暴露其內部狀態,因此任何匯入它的內容都可以直接更改該狀態,而不是透過適當的函式。

// counter.js
let count = 0;

module.exports = { count };

Enter fullscreen mode Exit fullscreen mode

// app.js
const counter = require('./counter');

counter.count = 100; // Oops. Nothing stopped this.
console.log(counter.count); // 100

Enter fullscreen mode Exit fullscreen mode

沒有任何東西能保護 count 免於被外部直接且錯誤地更改。

封裝版本

// counter.js
let count = 0;

function increment() {
  count++;
  return count;
}

function getCount() {
  return count;
}

module.exports = { increment, getCount };

Enter fullscreen mode Exit fullscreen mode

// app.js
const counter = require('./counter');

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2

Enter fullscreen mode Exit fullscreen mode

現在 count 完全無法直接存取。與之互動的唯一方式是透過 increment()getCount(),這些是模組明確選擇暴露的函式。

這可以保護內部狀態免於以意外或無效的方式被更改,也意味著內部實作(如何儲存或更新 count)可以在不破壞任何依賴模組的內容的情況下稍後更改,只要 increment()getCount() 的行為保持不變即可。

3. 防止全域命名空間污染

如果沒有模組,您在指令碼中宣告的每個變數和函式最終都可能共享相同的全域作用域。如果兩個檔案(或兩個函式庫)碰巧使用相同的變數名稱,其中一個可能會默默覆寫另一個,導致難以追蹤的錯誤。

例如,假設兩個獨立的檔案都使用名為 data 的變數,而沒有任何模組系統:

// file1.js
var data = 'user info';

Enter fullscreen mode Exit fullscreen mode

// file2.js
var data = 'product info';

Enter fullscreen mode Exit fullscreen mode

如果這兩個檔案都在相同的全域作用域中執行(例如網頁上的兩個 <script> 標籤,或在舊式 Node 設定中一起載入的兩個檔案),第二個 data 會覆寫第一個。最後執行的檔案「獲勝」,而且沒有任何警告表示出現問題。

Node.js 模組透過為每個檔案提供自己的作用域來解決這個問題。在模組內部宣告的任何內容,除非明確匯出,否則都會對該檔案保持私有:

// file1.js
const data = 'user info';
module.exports = { data };

Enter fullscreen mode Exit fullscreen mode

// file2.js
const data = 'product info';
module.exports = { data };

Enter fullscreen mode Exit fullscreen mode

// app.js
const file1 = require('./file1');
const file2 = require('./file2');

console.log(file1.data); // user info
console.log(file2.data); // product info

Enter fullscreen mode Exit fullscreen mode

兩個 data 變數都可以安全共存,因為每個模組都有自己的獨立作用域。除非您明確匯出,否則任何內容都不會洩漏到全域作用域。

4. 改善可維護性和可重用性

一旦程式碼按職責分割為模組,兩件事自然會變得更容易:維護重用

可維護性得到改善,因為每個模組都有明確的單一目的。如果價格計算方式出現錯誤,您確切地知道該去哪裡找,即定價模組,而不是掃描整個應用程式。

可重用性得到改善,因為編寫良好的模組不在乎誰在使用它。回到我們之前 math.js 的範例:該檔案可以匯入到完全不同的專案中,它會以完全相同的方式運作,因為它不依賴於原始應用程式的任何特定內容。

// Could be reused in any project, unchanged
const { add, subtract, multiply } = require('./math');

Enter fullscreen mode Exit fullscreen mode

這是迄今為止所涵蓋一切的回報:組織程式碼、封裝內部細節以及避免全域作用域衝突,都會導致模組更容易單獨維護,也更容易跨專案重用。

結論

來自前端背景,我一直認為我對模組有足夠的了解,它們只是檔案,對吧?但深入探討它們存在的原因改變了我對程式碼結構的思考。模組不僅僅是為了組織而分割檔案;它們是關於職責分離、保護內部狀態、避免命名衝突,以及建立更容易維護和隨時間重用的程式碼。

這只是觸及表面,還有更多內容可以探索,例如 CommonJS 和 ES Modules 之間的差異、Node 中模組快取的工作原理(無論您 require 多少次,模組只會載入和執行一次),或者 Node 如何內部解析模組路徑。但在深入探討如何之前,先了解為什麼模組很重要,這讓我學習其他內容的速度快了很多。

如果您也在學習 Node.js,我很想聽聽您對模組的看法,以及對您來說什麼有或沒有產生共鳴。