如何實現 node module 模組匯入

tenor發表於2018-06-27

今天,我們來聊聊 node 的模組,主要內容分別有:

1.什麼是模組化?模組化都有哪些規範?

2.node 模組匯入具體是如何實現的?

模組化歷史

單例模式

  • 保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

  • 實現單例核心思想 用一個變數來標誌當前是否已經為某個類建立過物件,如果是,則在下一次獲取該類的例項時,直接返回之前建立的物件。

  • 單例模式抽象,分離建立物件的函式和判斷物件是否已經建立

var getSingle = function (fn) {
    var result;
    return function () {
        return result || ( result = fn.apply(this, arguments) );
    }
};
複製程式碼

形參 fn 是我們的建構函式,我們只要傳入任何自己需要的建構函式,就能生成一個新的惰性單例。

閉包

  • 函式執行後返回一個引用空間這個空間被外部引用,此空間無法銷燬。這就叫閉包 函式執行的時候也會產生一個閉包

AMD

  • 非同步模組定義
  • 並非JavaScript原生支援,使用AMD規範進行頁面開發需要用到對應的庫函式,也就是大名鼎鼎RequireJS,實際上AMD 是 RequireJS 在推廣過程中對模組定義的規範化的產出。

requireJS主要解決兩個問題

  • 1、多個js檔案可能有依賴關係,被依賴的檔案需要早於依賴它的檔案載入到瀏覽器。
  • 2、js載入的時候瀏覽器會停止頁面渲染,載入檔案越多,頁面失去響應時間越長。

CMD

  • 通用模組定義
  • CMD有個瀏覽器的實現SeaJS,SeaJS要解決的問題和requireJS一樣,只不過在模組定義方式和模組載入(可以說執行、解析)時機上有所不同。

AMD和CMD區別:

  • 1、AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組。
  • 2、CMD推崇就近依賴,只有在用到某個模組的時候再去require。

Commonjs

  • 1.模組實現(一個 js 檔案就是一個模組)為了實現模組化的功能每個檔案外面都包含一個閉包。
  • 2.規定如何匯出一個模組 module.exports。
  • 3.如果匯入一個模組 require。

node 模組匯入實現步驟

node 匯入模組的方式

const fs = require("fs");
複製程式碼

我們通過這個入口,一步步的看下 node 是如何實現模組的匯入的,斷點除錯,走起!

首先我們看到,在 node 核心檔案 module.js 中,定義了一個 Module 類,並且在類的原型上定義了一個 require 方法,而這個方法就是在給定的檔案路徑下載入模組,並且返回該模組的"exports"物件。

如何實現 node module 模組匯入

而 require 呼叫了 Module 類上的靜態方法 _load,那我們進去看看這個 _load 方法是如何實現的吧:

如何實現 node module 模組匯入

很明顯,這裡就node module匯入的核心程式碼,那麼這裡都做了些什麼事情呢,我們一一來分析:

  • 1.如果快取中已經存在了將要匯入的模組,則直接返回其"exports"物件。

  • 2.如果這個模組是原生的模組,那麼則呼叫NativeModule.require()來返回對應的結果。

  • 3.如果以上2種都沒有,則執行以下操作:

    • 1.根據檔名,解析出檔案的絕對路徑,其對應的是這個操作:

      如何實現 node module 模組匯入

    • 2.如果快取中已經有這個模組,則直接從快取中獲取並返回"exports"物件:

      如何實現 node module 模組匯入

    • 3.如果快取中沒有,則檔案的絕對路徑建立一個模組,並且將這個模組放入到快取中:

      如何實現 node module 模組匯入

    • 4.最後讀取檔案模組中的內容,將內容放置在"exports"物件上並最終返回"exports"物件:

      如何實現 node module 模組匯入

步驟小結:

通過上面的分析我們不難發現:

  • Moudle 是一個類。那我們來看看,node裡面的Module類都有哪些屬性呢?
    如何實現 node module 模組匯入
  • 要實現一個 Module._load 方法實現模組的載入。
  • Module._resolveFilename 方法是用來解析檔案,獲取檔案的絕對路徑。
  • Module._cache 模組快取
  • 建立一個模組,放到快取中
  • Module._extensions 不同檔案型別解析方式不同,這點我們在實現中細細講解

如何實現一個自己的模組匯入?

根據上面的邏輯和分析,我們簡單的實現下模組的匯入:

首先,我們要讀取檔案並解析js檔案,所以需要使用node底層一些方法:

const fs = require("fs");
const path = require("path");
const vm = require("vm");

// 定義Module類
function Module(file) {
	this.id = file;//當前模組標識
	this.exports = {};//模組必有屬性,模組匯出時屬性掛載在該物件上
}
複製程式碼

接下來的第二步,

Module類中的靜態方法Module._load:

function req(moduleId) {
	let p = Module._resolveFileName(moduleId);
	// 判斷快取中是否已經存在該模組,如果存在則直接返回模組的"exports"物件
	if (Module._cacheModule[p]) {
		return Module._cacheModule[p].exports;
	}
	// 快取中沒有載入過這個模組,則構建一個模組
	let module = new Module(p);
	// 載入模組
	let content = module.load(p);
	// 將建立出來的模組放入到快取中,下次呼叫時直接從快取中獲取
	Module._cacheModule[p] = module;
	module.exports = content;
	// 最後返回模組的exports物件
	return module.exports;
}
複製程式碼

第三步:

我們需要解析檔案的具體路徑,讓我們一起看看 Module._resolveFileName 方法的實現吧:

// 解析絕對路徑的方法,返回一個絕對路徑
Module._resolveFileName = function (moduleId) {
	let resolvePath = path.resolve(moduleId);
	// 判斷檔案是否有字尾,如果沒有則加上字尾
	if (!path.extname(moduleId)) {
		// 獲取物件所有的key,返回一個物件所有key的集合
		let arr = Object.keys(Module._extensions);
		for (let i = 0; i < arr.length; i++) {
			// 沒有字尾將組成完整的檔案路徑
			let file = resolvePath + arr[i];
			try {
				// 判斷檔案是否存在並且能夠被訪問
				fs.accessSync(file);
				return file;
			} catch (error) {
				console.log(error);
			}
		}
	} else {
		return resolvePath;
	}
};
複製程式碼

第四步:

在拿到檔案的絕對路徑之後呢,我們將檢查模組快取中是否已經載入過這個模組,因此我們在類上定義了一個模組快取物件:

// 模組快取物件,是以模組的絕對路徑作為key來進行快取
Module._cacheModule = {};
複製程式碼

node模組匯入時如何解析要匯入的檔案

如果快取中沒有要載入的模組物件,則構建一個模組,並讀取模組的內容,在這裡要注意的是:

  • 不同檔案模組載入和讀取方式是不一樣的
    如何實現 node module 模組匯入

如上圖所示,對於json型別的檔案,我們需要將檔案的內容讀取出來並解析成JSON物件並掛載在module.exports物件身上即可

第五步:

而對於js檔案型別來說,有著獨特的解析方式,所以我們根據檔案的字尾來使用不同的載入方式:

// 根據不同檔案型別載入模組
Module.prototype.load = function (filePath) {
	// 獲取檔案字尾
	let ext = path.extname(filePath);
	// 根據檔案字尾呼叫不同的檔案模組載入策略,讀取檔案的內容
	let content = Module._extensions[ext](this);
	return content;
}
複製程式碼

而 Module._extensions 這個物件中則存放著真正解析檔案的具體方法:


// js模組包裹陣列
Module._wrapper = ["(function(exports,require,module){", "})"];

// 檔案模組載入策略物件,包括js檔案和json檔案
Module._extensions = {
	".js": function (module) {
		// 讀取js檔案中的js
		let scripts = fs.readFileSync(module.id, "utf8");
		let fn = Module._wrapper[0] + scripts + Module._wrapper[1];
		// 在沙箱中執行js程式碼,將不受上下文環境的影響
		vm.runInThisContext(fn).call(module.exports,module.exports,req,module);
		return module.exports;
	},
	".json": function (module) {
		// json檔案為讀取內容後進行解析
		return JSON.parse(fs.readFileSync(module.id, "utf8"));
	}
}
複製程式碼

最後,我們通過一張gif圖來瀏覽下所有程式碼,並跑下最終的結果:

如何實現 node module 模組匯入

好了,關於node 模組匯入就先提到這裡,歡迎大家多多提問題,謝謝!

相關文章