1. 程式人生 > >深入解析node.js的模組載入機制

深入解析node.js的模組載入機制

在node.js中,模組使用CommonJS規範,一個檔案是一個模組

  • node.js中的模組可分為三類
  • 內部模組 - node.js提供的模組如 fs,http,path等
  • 自定模組 - 我們自己寫的模組
  • 第三方模組 - 通過npm安裝的模組
    node.js提供了大量的模組供我們使用,比如 想解析一個檔案的路徑,可以使用path模組下的相應方法實現:
const path = require('path');
//返回目標檔案的絕對路徑
console.log(path.resolve('./1.txt'));

執行結果:

/Users/cuiyue/workspace/test/1.txt

使用require引入相應的模組,即可使用。
__dirname和__filename


node.js的每個模組都有這兩個引數,它們都是一個絕對路徑的地址,區別是__filename存放了從根目錄到當前檔名的路徑,__dirname只存放從根目錄到模組的所在目錄:

console.log(__dirname);
console.log(__filename);

執行結果:

/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/module.js

vm模組
vm模組是node.js提供在V8虛擬機器中編譯和執行的工具,node.js中的模組內部實現就是通過此模組完成。
說說vm的基本用法。
在js環境中有一個eval函式,它可以執行js的程式碼字串,比如:

eval('console.log("Hello javascript.")'); //輸出Hello javascript.

可以看到,eval函式的引數是一段字串,它可以執行字串形式的js程式碼,但它可以使用上下文環境中的變數:

var num=100;
eval('console.log(num)'); //輸出100

以上是可以正確訪問num的值。
vm模組提供了方法建立一個安全的沙箱,在指定的上下文環境中執行程式碼,不受外界干擾。

const vm = require('vm');
var num = 100;
vm.runInThisContext('console.log(num)');

執行結果:

console.log(num)
            ^
ReferenceError: num is not defined

可以看到程式碼報錯了,說明在vm建立了指定的上下文環境中,拿不到外界的參量。
CommonJS規範
在以前,由於javascript的歷史原因導致它的模組機制很差,由於這些缺點使得javascript不太善於開發大型應用,於是提出了CommonJS規範以彌補javascript的不足。
CommonJS規範主要分為三塊內容:模組匯入匯出、模組定義、模組標識。
模組匯入匯出
CommonJS中使用require()函式進行模組的引入。

const mymodule = require('mymodule');

使用exports匯出模組

module.exports = {
  name: 'Tom'
};//歡迎加入全棧開發交流圈一起學習交流:864305860

引用的名稱可以不帶路徑,若不帶路徑表示引入的是node提供的模組或是npm安裝的第三方模組(node_modules)
模組定義

  • module物件:在每一個模組中,module物件代表該模組自身。
  • export屬性:module物件的一個屬性,它向外提供介面。

模組標識

  • 模組標識指的是傳遞給require方法的引數,必須是符合小駝峰命名的字串,或者以 .、…、開頭的* 相對路徑,或者絕對路徑。

node中模組解析流程

  • 首先接收引數,把傳入的模組名稱解析成絕對路徑
  • 若沒有後綴名稱,依次拼接.js .json .node嘗試載入,仍到不到模組則報錯
  • 取得正確的路徑後判斷快取中是否存在此模組,若有則取出
  • 若快取中不存在則載入此檔案,在外包裹一層閉包並執行它

以上為大致流程,下面嘗試著寫一下模組。
程式碼的基本結構:

/**
 * Module類,用於處理模組載入
 */
function Module() {}
 
//模組的快取
Module._cacheModule = {};
 //歡迎加入全棧開發交流圈一起學習交流:864305860
//不同副檔名的載入策略
Module._extensions = {};
 
//根據moduleId解析絕對路徑,
Module._resolveFileName = function(moduleId) {};
 
//入口函式
function req(moduleId) {}

附上全部程式碼:

const path = require('path');
const fs = require('fs');
const vm = require('vm');
 
/**
 * Module類,用於處理模組載入
 */
function Module(file) {
 this.id = file; //當前模組的id,它使用完整的絕對路徑標識,因此是唯一的
 this.exports = {}; //匯出
 this.loaded = false; //模組是否已載入完畢
}
 
//模組的快取
Module._cacheModule = {};
 
Module._wrapper = ['(function(exports,require,module,__dirname,__filename){', '});'];
 
//不同副檔名的載入策略
Module._extensions = {
 '.js': function(currentModule) {
  let js = fs.readFileSync(currentModule.id, 'utf8'); //讀取出js檔案內容
  let fn = Module._wrapper[0] + js + Module._wrapper[1];
  vm.runInThisContext(fn).call(
   currentModule.exports,
   currentModule.exports,
   req,
   currentModule,
   path.dirname(currentModule.id),
   currentModule.id);
  return currentModule.exports;
 },
 '.json': function(currentModule) {
  let json = fs.readFileSync(currentModule.id, 'utf8');
  return JSON.parse(json); //轉換為JSON物件返回
 },
 '.node': ''
};
 
//載入模組(例項方法)
Module.prototype.load = function(file) {
 let extname = path.extname(file); //獲取字尾名
 return Module._extensions[extname](this);
};//歡迎加入全棧開發交流圈一起學習交流:864305860
 
//根據moduleId解析絕對路徑,
Module._resolveFileName = function(moduleId) {
 let p = path.resolve(moduleId);
 
 if (!path.extname(moduleId)) { //傳入的模組沒有後綴
  let arr = Object.keys(Module._extensions);
 
  //迴圈讀取不同副檔名的檔案
  for (var i = 0; i < arr.length; i++) {
   let file = p + arr[i]; //拼接上字尾名成為一個完整的路徑
   try {
    fs.accessSync(file);
    return file; //若此檔案存在返回它
   } catch (e) {
    console.log(e);
   }
  }
 } else {
  return p;//歡迎加入全棧開發交流圈一起學習交流:864305860
 }//面向1-3年前端人員
}; //幫助突破技術瓶頸,提升思維能力
function req(moduleId) {
 let file = Module._resolveFileName(moduleId);
 
 if (Module._cacheModule[file]) { //若快取中存在此模組
  return Module._cacheModule[file];
 } else {
  let module = new Module(file);
  module.exports = module.load(file);
  return module.exports;
 }
}//歡迎加入全棧開發交流圈一起學習交流:864305860
console.log(req('./a.js')());

a.js的檔案內容:

module.exports = function() {
 console.log('This message from a.js');
 console.log(__dirname);
 console.log(__filename);
}

最終執行結果:

This message from a.js
/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/a.js

重要程式碼說明
_resolveFileName
_resolveFileName方法的主要作用是把傳入的模組解析成絕對路徑,這樣才可以進行下一步,根據完整的路徑載入模組。
因此要進行判斷,如果傳入的模組不存在,則要報錯;如果傳入的模組已經有副檔名了,就不要拼接了;若沒有副檔名,依次以.js .json .node的順序拼接成完成的模組進行載入。
_extensions
此物件中封裝了載入不同型別模組的處理方法,其中若是.json型別則使用fs讀取檔案直接轉換成JSON物件並返回。
若是.js檔案則讀取後,拼接閉包,將exports,require,module,__dirname,__filename五大引數拼接好,使用vm模組的沙箱機制執行,得到的結果放入module.exports返回。
總結
以上就是node.js的模組載入的簡單邏輯,實際上node.js的原始碼遠遠比上面的程式碼複雜,光是處理模組路徑、判斷合法等操作就寫了N行。而且我這裡沒有寫快取以及其它的複雜邏輯,但核心差不多就是這些,核心的核心就是用fs.readFileSync讀取js檔案,把內容拼接到一個大大的閉包中,這也解釋了為什麼我們自己寫的所有node模組中都會有require方法,exports匯出,以及__dirname和__filename引數。
結語

感謝您的觀看,如有不足之處,歡迎批評指正。

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。