在CommonJs規格提出之前,Javascript是沒有模組系統的,這意味著我們很難開發大型的應用,因為程式碼的組織會比較困難。
首先CommonJS不是Node獨有的東西,CommonJs是一種模組規範,定義如何引用和導出模組,Nodejs只是實作了這個規範,CommonJS模組規範主要分成模組引用、模組定義和模組識別三個部分。
模組引用
模組引用就是我們可以透過require
引入其它的模組。
const { add } = require('./add'); const result = add(1 ,2);
模組定義
一個檔案就是一個模組,模組裡會提供兩個變量,分別為module和exports。 module為目前模組本身,exports為要導出的內容,同時exports為module的一個屬性,即exports為module.exports。其他模組透過require導入的內容即為module.exports的內容。
// add.js exports.add = (a, b) => { return a + b; }
模組識別
模組標識即為require裡面的內容,例如require('./add')
,則模組標識為./add
。
透過CommonJS建構的這套模組導入導出機制使得使用者完全無需考慮變數污染,可以方便的建構大型應用。
Node的模組實現
Node實作了CommonJs規範,並且增加了一些自己需要的特性。 Node為了實作CommonJs規範主要做了以下三件事:
路徑分析
檔定位
編譯執行
路徑分析
當執行require()的時候,require接收的參數即為模組標識符,node透過模組標識符來進行路徑分析。路徑分析的目的就是為了透過模組標識符找到這個模組所在的路徑。首先,node的模組分為兩類,分別是核心模組和檔案模組。核心模組是node自帶的模組,檔案模組是使用者編寫的模組。同時文件模組又分為相對路徑形式的文件模組、絕對路徑形式的文件模組和非路徑形式的文件模組(如express)。
當node找到一個檔案模組之後,會將這個模組編譯執行並且快取起來,大致原理是將這個模組的完整路徑作為key,編譯後的內容作為值,後續再第二次引入這個模組的時候就不需要再進行路徑分析檔定位編譯執行這幾個步驟了,可以直接從快取讀取編譯好的內容。
// 快取的模組示意: const cachedModule = { '/Usr/file/src/add.js': 'add.js編譯後的內容', 'http': 'Node自帶的http模組編譯後的內容', 'express': '非路徑形式自訂檔案模組express編譯後的內容' // ... }
當要尋找require導入的模組時,查找模組的順序是先查看快取裡是否已經有該模組,如果快取裡面沒有再查看核心模組,然後再找找檔案模組。其中路徑形式的文件模組比較好查找,根據相對或絕對路徑就可以得到完整的文件路徑。非路徑形式的自訂檔案模組查找起來會相對麻煩一些,Node會從node_modules這個資料夾裡去查找是否有這個檔案。
node_modules這個目錄在哪裡呢,比如說我們目前執行的檔案為/Usr/file/index.js;
/** * /Usr/file/index.js; */ const { add } = require('add'); const result = add(1, 2);
這個模組裡我們有引入了一個add模組,這個add不是一個核心模組也不是一個路徑形式的檔案模組,那麼這時候如何找到這個add模組呢。
module有一個paths的屬性,找尋add模組的路徑在paths這個屬性裡,我們可以把這個屬性打出來看一下:
/** * /Usr/file/index.js; */ console.log(module.paths);
我們在file目錄下執行node index.js可以印出paths的值。 paths裡的值是一個數組,如下:
[ '/Usr/file/node_modules', '/Usr/node_modules', '/node_modules', ]
即Node會依序從上面的目錄裡尋在是否包含add這個模組,原理和原型鏈類似。先從目前執行的檔案的同級目錄的node_modules資料夾裡開始找,如果沒找到或沒有node_modules這個目錄,則繼續往上級查找。
文件定位
路徑分析和文件定位是搭配一起使用的,文件標識符可以是不帶後綴的,也可能透過路徑分析找到的是一個目錄或一個包,這個時候要定位到具體的文件需要一些額外的處理。
檔案副檔名分析
const { add } = require('./add');
例如上面這段程式碼,檔案識別碼是不帶副檔名的,這個時候node會依序找出是否存在.js、.json、.node文件。
目錄和套件分析
同樣是上面這段程式碼,透過./add
查找到的可能不是一個文件,可能是一個目錄或套件(透過判斷add資料夾下是否有package.json文件來判斷是目錄還是套件)。這時候檔案定位的步驟是這樣的:
如果package.json裡沒有main字段,那麼也會將index作為文件,然後進行擴展名分析找到對應後綴的文件。
模組編譯
我們開發中主要遇到的模組為json模組和js模組。
json模組編譯
當我們require一個json模組的時候,實際上Node會幫我們使用fs.readFilcSync去讀取對應的json文件,得到json字串,然後呼叫JSON.parse解析得到json對象,再賦值給module. exports,然後給到require。
js模組編譯
當我們require一個js模組的時候,像是
// index.js const { add } = require('./add');
// add.js exports.add = (a, b) => { return a + b; }
這時候發生了什麼事呢,為什麼我們可以直接在模組裡使用module、exports、require這些變數。這是因為Node在編譯js模組的時候對模組的內容進行了首尾的包裝。
例如add.js這個模組,實際編譯的時候是會被包裝成類似這樣的結構:
(function(require, exports, module) { exports.add = (a, b) => { return a + b; } return module.exports; })(require, module.exports, module)
即我們寫的js檔案是會被包裝成一個函數,我們寫的只是這個函數裡的內容,Node後續的包裝的過程對我們隱藏了。這個函數支援傳入一些參數,其中就包含require、exports和module。
當編譯完js檔案後,就會執行這個文件,node會將對應的參數傳給這個函數然後執行,並且傳回module.exports值給到require函數。
以上就是Node實作CommonJs規範的基本流程。