Node.js 系列 - 模組機制

罐裝汽水_Garrik發表於2018-10-26

作為還在漫漫前端學習路上的一位自學者。我以學習分享的方式來整理自己對於知識的理解,同時也希望能夠給大家作為一份參考。希望能夠和大家共同進步,如有任何紕漏的話,希望大家多多指正。感謝萬分!


什麼是模組?

以程式設計角度來說, "模組" 指的是能夠提供一定功能或資料的程式語句集合. 模組具備和外部聯絡的介面 (其他模組或程式呼叫該模組的方式)

在 Node.js 中, 每個檔案就被視為一個模組. 這個檔案可能是 JavaScript 編寫的檔案、JSON 或者用 C/C++ 編譯的二進位制檔案. 通過對外介面來向外部暴露功能或者資料, 模組之間可以互相呼叫.

為什麼要用模組?

隨著開發複雜度的提升, 將程式碼都寫在一處的傳統開發方式, 顯現出了很多問題:

  • 很容易出現程式碼重複. 開發人員很容易將一個功能的程式碼重複地寫了好多遍. 這導致瞭如果日後功能需求出現了變更, 就要有多處程式碼需要被更改. 隨著應用規模的增大, 程式碼會變得難以維護.
  • 難以確保程式碼質量. 所有程式碼都混在一起, 實現不同功能的程式碼全都被寫在一個檔案中, 使得對於單個功能的獨立測試變得困難.
  • 難以查錯. 所有程式碼都混在一起, 程式執行出現 BUG 了, 很難快速定位.
  • 效能浪費. 因為程式碼都寫在一個檔案中, 在只呼叫檔案中一段程式碼的時候, 也會導致整個檔案都載入一遍. 這會使很多根本用不到的程式碼對效能造成浪費.
  • 難以多人寫協作. 所有程式碼都放在一個檔案中, 使得多人協作變得困難. 開發人員難以確認其他人做了什麼修改, 新增了什麼東西. 很容易一個人出了錯誤, 導致整個程式崩潰.
  • 等等...

模組化開發

通過使用模組機制, 我們可以把一個複雜程式的各個功能拆分, 分別封裝到不同的模組. 每個模組職責單一 (各管一件事, 之間沒交集) 通過開發新模組, 和對已有模組的複用來實現各種功能. 這種開發方式被稱為 "模組化開發".

應用模組化開發, 使得各個功能都封裝在獨立的檔案中, 分而治之, 互不干擾. 使得程式碼易於維護和複用. 同時每個模組中的變數也不會汙染全域性作用域, 避免了命名衝突.

CommonJS

Node.js 參照 CommonJS 標準實現了模組機制. CommonJS 是一套程式碼規範, 目的是為了構建 JavaScript 在瀏覽器之外的生態系統 (伺服器端, 桌面端). JavaScript 誕生之初只是為了寫網頁小指令碼, 並不作為開發大型複雜應用的語言, 其自身有很多不足. 並且, 官方規範 (ECMAScript) 制定的時間較早, 涵蓋範圍較小, 對於後端開發而言, 例如檔案系統, I/O 流, 模組系統, 等等方面都沒有相應的標準. 基於種種的不足, CommonJS 規範致力於彌補 JavaScript 沒有標準的缺陷, 讓 JavaScript 有能力去開發複雜應用, 同時具備跨平臺能力.

下面是一個 Node.js 的模組使用示例:

在程式碼中, 開頭通過 require 方法引入了 Node.js 自帶的 http 模組. 並用此模組實現了一個 HTTP 伺服器.

const http = require('http');

function myNodeServer(req, res){
res.writeHead(200, {'Content-type':'text/plain'});
res.write('Hello World'); 
res.end();
}

http.createServer(myNodeServer).listen(3000); //監聽 3000 埠

console.log('Server is running!'); 
複製程式碼

模組分類

前文說, 在 Node.js 中, 每個檔案就被視為一個模組. 這個檔案可能是 JavaScript 編寫的檔案、JSON 或者用 C/C++ 編譯的二進位制檔案.

模組可以分成三類:

Untitled Diagram(6)

  • 核心模組 』: Node.js 自帶的原生模組. 比如, http, fs, url. 其分為 C/C++ 編寫的和 JavaScript 編寫的兩部分. C/C++ 模組存放在 Node.js 原始碼目錄的 src/ 目錄下. JavaScript 模組存放在 lib/ 目錄下.
  • 檔案模組 』: 開發人員在本地寫的模組. 載入時通過相對路徑, 絕對路徑來定位模組所在位置.
  • 第三方模組 』: 別人編寫的模組, 通過包管理工具, 比如 npm, yarn, 可以將其從網路上引入到本地專案, 供己使用.

NPM 包管理器

NPM 是隨同 Node.js 一起安裝的 "包管理工具". 通過它, 全世界開發者們可以簡單方便地互相分享和借鑑各自的 Node.js 模組. 其讓整個 Node.js 社群生態變得繁榮熱鬧.

1*EiKXXsymsyfZ5X46uFG3Xw

NPM 常見的使用場景有以下幾種:

  • 允許使用者從 NPM 伺服器下載別人編寫的第三方模組到本地使用。
  • 允許使用者從 NPM 伺服器下載並安裝別人編寫的命令列程式到本地使用。
  • 允許使用者將自己編寫的模組或命令列程式上傳到NPM伺服器供別人使用。

具體的使用方法網上有很多教程, 這裡就不贅述了. 不想自行查閱的話, 可以直接參考下面的連結:

npm 官方文件

npm 使用介紹 - 菜鳥教程


使用模組

在瞭解了什麼是模組之後, 讓我們來看看如何在 Node.js 中實際應用模組機制. 在使用上, 可以很簡單的分為三個步驟: 建立, 匯出, 引入. 先建立一個模組, 然後匯出功能或資料, 模組之間可以互相引入匯出的內容.

Node.js 提供了 exportsrequire 兩個物件,其中 exports 用於匯出模組, require 用於從外部引入另一個模組, 即獲取模組的 exports 物件.

建立 & 匯出模組

先讓我們來看看如何建立並把模組的內容匯出. 在 Node.js 中, 一個檔案就是一個模組. 建立模組的方法就是建立一個檔案.

通過 exports 物件來指定一個模組的匯出內容.

示例:

// 檔名: nameModule.js
var name = 'Garrik';

exports.setName = function(newName) {
name = newName;
}

exports.getName = function() {
return name;
}
複製程式碼

在以上示例中, nameModule.js 檔案通過 exports 物件將 setNamegetName 作為模組的訪問介面. 其他的模組可以引入匯出的 exports 物件, 直接訪問 exports 物件的成員函式.

引入模組

在 Node.js 中, 通過 require 函式來引入外界模組匯出的內容. require 函式接受一個字串作為路徑引數, 函式根據這個字串引數來進行模組查詢. 找到後會返回目標模組匯出的 exports 物件.

示例:

// 檔名: showNameModule.js
var nameModule = require('./nameModule.js');

console.log(nameModule.getName()); 
// 顯示: Garrik

nameModule.setName('Xiang');

console.log(nameModule.getName());
// 顯示: Xiang
複製程式碼

上面示例中, 通過 require 引入了當前目錄下 nameModule.js 匯出的 exports 物件, 並讓一個本地變數指向引入模組的 exports 物件. 之後在 showNameModule.js 檔案中就可以使用 getNamesetName 這兩個方法了.

module.exports 和 exports 的區別

在使用 exports 物件匯出內容時, 所有作為對外訪問介面的屬性和方法都是定義在 exports 屬性上的. 上面的例子中 setNamegetName 方法都直接定義在 exports 物件上. 那如果想直接匯出一個物件, 或者基礎型別值可不可以呢?

可能有人會想可不可以這樣寫:

var name = 'Garrik';

exports = name;
複製程式碼

如果你試一下的話會發現, 最後引入的是一個空物件, 而不是你定義在 exports 上的東西.

在使用 exports 的時候只能往這個物件裡新增新的屬性和方法, 而不能對其直接賦值. 如果想直接匯出一個物件, 或者基礎型別值要使用 module.exports 物件. 例如上面例子就可以改寫成:

// 檔名: nameModule.js
var name = 'Garrik';

module.exports = {
    setName: function(newName) {
        name = newName;
    }, 
    getName: function() {
        return name;
    }
} 
複製程式碼

這樣寫的話, 就匯出了一整個物件, setNamegetName 方法是這個物件的成員函式. 而不是之前的 exports 物件了.

除此之外 module.exports 還可以直接匯出基礎型別值:

// 檔名: numMoule.js

var num = 123456;

module.exports = num;
複製程式碼
// 檔名: showNum.js
var getNum = require('./numModule.js'); // showNum.js 和 numModule.js 在同一目錄下

console.log(getNum); // 結果: 123456
複製程式碼

這種方式下, 匯出的就直接是基礎型別的值.

可能還是很多人在疑惑 exportsmodule.exports 區別和關係.

上面我說, 一個檔案被另一個模組引入時, 會被做一些處理. 檔案中程式碼並不被 Node 執行, 而是被打包進一個函式中, 然後 Node 執行這個函式. 打包函式會被傳入 exportsrequiremodule__filename__dirname 這五個引數. 所有的這些引數都在 Node.js 執行函式時賦值, 並且只在當前的函式作用域中有效. 打包函式執行到最後, 返回 module.exports 物件.

其中, exportsmodule.exports 的引用, module 物件代表被打包進去的程式碼本身. moduleexports 物件用於指定一個模組的匯出內容.

在模組中定義外部可訪問介面的時候, 有兩個方法:

exports.name = 'Garrik';
複製程式碼
module.exports = {name: 'Garrik'};
複製程式碼

在使用 exports 的時候只能往這個物件裡新增新的屬性和方法, 而不能對其直接賦值. 因為直接賦值會打破其對 module.exports 的引用.

// 這是可以的:
exports.name = 'Garrik';
exports.gender = 'Male';

// 這是不可以的:
exports = {name: 'Garrik', gender: 'Male'};

// 應該用 module.exports:
module.exports = {name: 'Garrik', gender: 'Male'}
複製程式碼

如果想直接匯出一個物件, 或基本型別值, 應該使用 module.exports.

// 匯出函式
module.exports = function(num) {
return num + 1;
};

// 匯出基本型別值
module.exports = 123;
複製程式碼

require 的路徑引數

在用 require 引入模組時, 路徑引數可能有下面三種形式:

  • 相對路徑: ./ 開頭 或 ../ 開頭
  • 絕對路徑: / 開頭
  • 模組名 (例如: http, fs, url)

根據引數不同, 載入方式也有區別.

絕對路徑, 或相對路徑

在指定了模組路徑的情況下, Node.js 會去指定的位置載入模組. 但因為用 require 來載入模組時可以省略檔案字尾, 在省略的情況下, Node.js 會去猜測檔案的型別.

比方說我要去 ./modules/ 目錄下載入一個 haha 模組.

var haha = require('./modules/haha');
複製程式碼

因為 haha 沒寫檔案字尾, Node.js 將執行的操作順序為:

  • 按 js 檔案來執行(先找對應路徑當中是否有 haha.js 檔案, 有就載入)
  • 按 json 檔案來解析(若上面的 js 檔案找不到時,則找對應路徑當中的 haha.json 檔案來載入)
  • 按照預編譯好的 C++ 模組來執行(還沒有, 尋找對應路徑當中的 haha.node 檔案來載入)
  • 若引數字串為一個目錄的路徑, 就是說 haha 為一個目錄, 則先查詢該資料夾下的 package.json 檔案,然後再載入該檔案當中 main 欄位所指定的入口檔案. 若 package.json 檔案當中沒有 main 欄位,或者根本沒有 package.json 檔案,則再預設查詢該資料夾下的 index.js 檔案, 並作為模組來載入.
  • 要是還沒有就拉倒吧!

無路徑, 直接模組名:

在沒有路徑, 引數值直接為一個模組名的情況下:

var haha = require('haha');
複製程式碼
  • 如果 haha 是 Node.js 核心模組就直接載入.
  • 如果是第三方模組, 則依次從當前目錄中的 node_modules 目錄, 父級目錄中的 node_modules 目錄, 一直到根目錄下的 node_modules 目錄下去查詢 haha 的所在. 若有兩個同名檔案,則遵循就近原則。優先引入目錄順序靠前的模組.
  • 如果找到的 haha 為一個目錄, 則先查詢該資料夾下的 package.json 檔案,然後再載入該檔案當中 main 欄位所指定的入口檔案. 若 package.json 檔案當中沒有 main 欄位,或者根本沒有 package.json 檔案,則再預設查詢該資料夾下的 index.js 檔案, 並作為模組來載入.
  • 要是還沒有就拉倒吧!

? 好啦,今天的分享就告一段落啦。下一篇中,我會介紹如何實現一個 "Hello World" HTTP 伺服器。

傳送門: Node.js 系列 - 搭建 "Hello World" HTTP 伺服器

如果喜歡的話就點個關注吧!O(∩_∩)O 謝謝各位的支援❗️

相關文章