CommonJS 是一種模塊化的標準,而 NodeJS 是這種標準的實現,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。json
在實現模塊加載以前,咱們須要清除模塊的加載過程:數組
A
文件夾下有一個 a.js
,咱們要解析出一個絕對路徑來;.js
、.json
;由於咱們只是實現 CommonJS 的模塊加載方法,並不會去實現整個 Node,在這裏咱們須要依賴一些 Node 的模塊,因此咱們就 「不要臉」 的使用 Node 自帶的 require
方法把模塊加載進來。緩存
// 依賴模塊 // 操做文件的模塊 const fs = require("fs"); // 處理路徑的模塊 const path = require("path"); // 虛擬機,幫咱們建立一個黑箱執行代碼,防止變量污染 const vm = require("vm");
其實 CommonJS 中引入的每個模塊咱們都須要經過 Module
構造函數建立一個實例。閉包
// 建立 Module 構造函數 /* * @param {String} p */ function Module(p) { this.id = p; // 當前文件的表示(絕對路徑) this.exports = {}; // 每一個模塊都有一個 exports 屬性,用來存儲模塊的內容 this.loaded = false; // 標記是否被加載過 }
// Module 靜態變量 // 函數後面須要使用的閉包的字符串 Module.wrapper = [ "(function (exports, require, module, __dirname, __filename) {", "\n})" ]; // 根據絕對路徑進行緩存的模塊的對象 Module._cacheModule = {}; // 處理不一樣文件後綴名的方法 Module._extensions = { ".js": function() {}, ".json": function() {} };
爲了防止和 Node 自帶的 require
方法重名,咱們將模擬的方法重命名爲 req
。app
// 引入模塊方法 req /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數處理成絕對路徑 let p = Module._resolveFileName(moduleId); // 生成一個新的模塊 let module = new Module(p); }
在上面代碼中,咱們先把傳入的參數經過 Module._resolveFileName
處理成了一個絕對路徑,並建立模塊實例把絕對路徑做爲參數傳入,咱們如今實現一下 Module._resolveFileName
方法。模塊化
這個方法的功能就是將 req
方法的參數根據是否有後綴名兩種方式處理成帶後綴名的文件絕對路徑,若是 req
的參數沒有後綴名,會去按照 Module._extensions
的鍵的後綴名順序進行查找文件,直到找到後綴名對應文件的絕對路徑,優先 .js
,而後是 .json
,這裏咱們只實現這兩種文件類型的處理。函數
// 處理絕對路徑 _resolveFileName 方法 /* * @param {String} moduleId */ Module._resolveFileName = function(moduleId) { // 將參數拼接成絕對路徑 let p = path.resolve(moduleId); // 判斷是否含有後綴名 if (!/\.\w+$/.test(p)) { // 建立規範規定查找文件後綴名順序的數組 .js .json let arr = Object.keys(Module._extensions); // 循環查找 for (let i = 0; i < arr.length; i++) { // 將絕對路徑與後綴名進行拼接 let file = p + arr[i]; // 查找不到文件時捕獲異常 try { // 並經過 fs 模塊同步查找文件的方法對改路徑進行查找,文件未找到會直接進入 catch 語句 fs.accessSync(file); // 若是找到文件將該文件絕對路徑返回 return file; } catch (e) { // 當後綴名循環完畢都沒有找到對應文件時,拋出異常 if (i >= arr.length) throw new Error("not found module"); } } } else { // 有後綴名直接返回該絕對路徑 return p; } };
// 完善 req 方法 /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數處理成絕對路徑 let p = Module._resolveFileName(moduleId); // 生成一個新的模塊 let module = new Module(p); // ********** 下面爲新增代碼 ********** // 加載模塊 let content = module.load(p); // 將加載後返回的內容賦值給模塊實例的 exports 屬性上 module.exports = content; // 最後返回 模塊實例的 exports 屬性,即加載模塊的內容 return module.exports; // ********** 上面爲新增代碼 ********** }
上面代碼實現了一個實例方法 load
,傳入文件的絕對路徑,爲模塊加載文件的內容,在加載後將值存入模塊實例的 exports
屬性上最後返回,其實 req
函數返回的就是模塊加載回來的內容。ui
// load 方法 // 模塊加載的方法 Module.prototype.load = function(filepath) { // 判斷加載的文件是什麼後綴名 let ext = path.extname(filepath); // 根據不一樣的後綴名處理文件內容,參數是當前實例 let content = Moudule._extensions[ext](this); // 將處理後的結果返回 return content; };
還記得前面準備的靜態屬性中有 Module._extensions
就是用來存儲這兩個方法的,下面咱們來完善這兩個方法。this
// 處理後綴名方法的 _extensions 對象 Module._extensions = { ".js": function(module) { // 讀取 js 文件,返回文件的內容 let script = fs.readFileSync(module.id, "utf8"); // 給 js 文件的內容增長一個閉包環境 let fn = Module.wrap(script); // 建立虛擬機,將咱們建立的 js 函數執行,將 this 指向模塊實例的 exports 屬性 vm.runInThisContext(fn).call( module.exports, module.exports, req, module ); // 返回模塊實例上的 exports 屬性(即模塊的內容) return module.exports; }, ".json": function(module) { // .json 文件的處理相對簡單,將讀出的字符串轉換成對象便可 return JSON.parse(fs.readFileSync(module.id, "utf8")); } };
咱們這裏使用了 Module.wrap
方法,代碼以下,其實幫助咱們加了一個閉包環境(即套了一層函數並傳入了咱們須要的參數),裏面全部的變量都是私有的。spa
// 建立閉包 wrap 方法 Module.wrap = function(content) { return Module.wrapper[0] + content + Module.wrapper[1]; };
Module.wrapper
的兩個值其實就是咱們須要在外層包了一個函數的前半段和後半段。
這裏咱們要劃重點了,很是重要:
一、咱們在虛擬機中執行構建的閉包函數時利用執行上/下文 call
將 this
指向了模塊實例的 exports
屬性上,因此這也是爲何咱們用 Node 啓動一個 js
文件,打印 this
時,不是全局對象 global
,而是一個空對象,這個空對象就是咱們的 module.exports
,即當前模塊實例的 exports
屬性。
二、仍是第一條的函數執行,咱們傳入的第一個參數是改變 this
指向,那第二個參數是 module.exports
,因此在每一個模塊導出的時候,使用 module.exports = xxx
,其實直接替換了模塊實例的值,即直接把模塊的內容存放在了模塊實例的 exports
屬性上,而 req
最後返回的就是咱們模塊導出的內容。
三、第三個參數之因此傳入 req
是由於咱們還可能在一個模塊中導入其餘模塊,而 req
會返回其餘模塊的導出在當前模塊使用,這樣整個 CommonJS 的規則就這樣創建起來了。
咱們如今的程序是有問題的,當重複加載了一個已經加載過得模塊,當執行 req
方法的時候會發現,又建立了一個新的模塊實例,這是不合理的,因此咱們下面來實現一下緩存機制。
還記得以前的一個靜態屬性 Module._cacheModule
,它的值是一個空對象,咱們會把全部加載過的模塊的實例存儲到這個對象上。
// 完善 req 方法(處理緩存) /* * @param {String} moduleId */ function req(moduleId) { // 將 req 傳入的參數處理成絕對路徑 let p = Module._resolveFileName(moduleId); // ********** 下面爲新增代碼 ********** // 判斷是否已經加載過 if (Module._cacheModule[p]) { // 模塊存在,若是有直接把 exports 對象返回便可 return Module._cacheModule[p].exprots; } // ********** 上面爲新增代碼 ********** // 生成一個新的模塊 let module = new Module(p); // 加載模塊 let content = module.load(p); // ********** 下面爲新增代碼 ********** // 存儲時是拿模塊的絕對路徑做爲鍵與模塊內容相對應的 Module._cacheModule[p] = module; // 是否緩存表示改成 true module.loaded = true; // ********** 上面爲新增代碼 ********** // 將加載後返回的內容賦值給模塊實例的 exports 屬性上 module.exports = content; // 最後返回 模塊實例的 exports 屬性,即加載模塊的內容 return module.exports; }
在同級目錄下新建一個文件 a.js
,使用 module.exports
隨便導出一些內容,在咱們實現模塊加載的最下方嘗試引入並打印內容。
// 導出自定義模塊 // a.js module.exports = "Hello world";
// 檢測 req 方法 const a = req("./a"); console.log(a); // Hello world
<hr/>
其實咱們只實現了 CommonJS 規範的一部分,即自定義模塊的加載,其實在 CommonJS 的規範當中關於模塊查找的規則還有不少,具體的咱們就用下面的流程圖來表示。
這篇文章讓咱們瞭解了 CommonJS 是什麼,主要目的在於理解 Node 模塊化的實現思路,想要更深刻的瞭解 CommonJS 的實現細節,建議看一看 NodeJS 源碼對應的部分,若是以爲源碼比較多,不容易找到模塊化實現的代碼,也能夠在 VSCode 中經過調用 require
方法引入模塊時,打斷點調試,一步一步的跟進到 Node 源碼中查看。