細數js模塊機制內涵

ufoe7070 8年前發布 | 11K 次閱讀 Node.js JavaScript開發

說個事, 親,你知道當你運行js的時候,發生錯誤的時候下面的信息代表的是什么嗎?like

SyntaxError: Unexpected token .
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:414:25)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:313:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:136:18)

母雞吧,實際上,這個nodeJS中Module模塊的內容。 說道模塊,這就牽扯到整個js現存的那些優秀的模塊機制呢。首當其沖的要數,AMD,然后是NodeJS的加載模塊.下面,我們來了解一下內部的機制,確定上面的錯誤,到底代表什么.

AMD

AMD 全稱為: Asynchronous Module Definition(異步模塊定義)

AMD規范應該是以前 前端 最常使用的一個模塊化規范. 他通過異步的方式加載js 腳本,并且執行相關的內容.

我們先來看一下基本的AMD格式. 他其實就提供了一個全局函數-define.

define([arr], function(para){//...});

該函數接受兩個參數

  • 第一個參數: 接受的是數組類型, 里面定義的參數是相關的js文件的路徑或者js文件的alias. 路徑或者alias 代表著一個引入的js模塊. 比如:

    ['/arr/script.js','/js/demo.js','jquery']

不管是定義路徑還是定義alias, 最終, 都會被插件解析為 實際的js代碼. 這個具體就涉及路徑的解析了, 我們下文在說一下

  • 第二個參數: 接受的是函數. 并且函數可以帶入參數. 參數的位置, 和前面數組引入的模塊位置一致. 然后我們就可以直接使用模塊內容。

    define(['/arr/isArray.js','jquery'],function(isArray,$){//...})

我們接著就可以直接使用定義好的模塊alias就可以了。

NodeJS 的模塊化

由于AMD 僅僅提出了一個define函數用來異步加載腳本. 但是服務端的場景,這顯然就有點雞肋了. 所以, nodeJS 基本上參考了 CommonJS Modules/1.1 proposal . 他為了更精確的表達 server 端模塊化的機制. 定義了3個全局的變量 exports , require , module . 需要注意的是... 其實exports 并不是單一的一塊,他其實是.

var exports = module.exports = {};

即, 其實nodeJS模塊交互只有require 和 module 在進行。 我們來具體看一下 , nodeJS 端 進行模塊化的機制吧.首先,我們得明白什么叫做模塊?

什么是模塊

A module encapsulates related code into a single unit of code.

看到這段話后,更覺更懵逼了. 能不能說人話~

其實, 模塊就是能夠完成一定工作的函數,對象,甚至基本的數據類型(比如:String,Number等);

來, 我們可以寫一個demo:

var sayHello = function(){
    return 'hello';
}
var move = function(){
    return 'Now, I am moving';
}

上面兩個函數我們就可以說,是模塊.Ok~ 現在我們已經寫了一個簡單的模塊了, 那接下來該怎么導出這個模塊呢?

導出模塊

這里我們就需要使用exports方法進行導出即可.

//dist.js
// var exports = module.exports = {};
exports.sayHello = function(){
    return 'hello';
}
exports.move = function(){
    return 'Now, I am moving';
}

這里,我們需要注意一下:exports = module.exports.由于是對象,我們還可以利用對象本來的特征, 通過字面量形式書寫

//dist.js
//下面的module.exports 不能使用exports代替

module.exports = {
    sayHello : function(){
    return 'hello';
    }
    move : function(){
    return 'Now, I am moving';
    }
}
//如果你寫成如下
exports = {//...} //那么你的exports關鍵字已經和module.exports斷開聯系了.

但是, 現實情況是, 不推薦這樣直接 將 exports 用字面量表達. 因為這樣造成將一開始寫入的內容給覆蓋掉.

exports.getName = function(){
    return "jimmy";
};
exports.flag = "It will be overloaded";
//上面所有的都將會被覆蓋掉
module.exports = {  //這里只能使用
  getName : function(){
    return "sam"
  },
  sayName :function(){
    return "Michael"
  }
}

所以,推薦的兩點:

  • 如果一開始使用 exports.xxx 導出的話, 后面就不要使用 exports = {} 導出.

  • 可以在最后部分直接使用 exports = {} 進行導出, 這樣的, 能夠讓你的代碼更清晰.

OK, 基本模塊樣式我們已經寫完了. 現在就輪到如何引用模塊了.

引用模塊

最后上面3個關鍵字,就只剩下了 require . 那require的工作機制是怎樣的呢?

var require = function(path) {

  // 通過路徑查找文件, 并解析

  return module.exports;
};

所以, 現在模塊機制的難點不在是 模塊是怎么 引用的, 而變成了 路徑解析的問題, 我們可以放到后面再進行討論。 我們現在可以梳理一下, 模塊內容傳遞的 Process.

  • app.js => module.exports => require => 自定義變量

所以,一個模塊 就經歷了以上的流程傳遞到你最后引用的變量里面了。 我們來看一下整體的demo.

//dist.js
module.exports = {  
    sayHello : function(){
    return 'hello';
    }
    move : function(){
    return 'Now, I am moving';
    }
}
//main.js
var action = require('../dist.js');
console.log(action.sayHello()); //hello
console.log(action.move()); //Now, I am moving

通過模塊機制, 我們可以很容易的了解到. require其實就是一個包裝函數. 在函數體內部進行 一些列的路徑轉換. 比如, 路徑解析, 包的緩存,模塊的加載,內置模塊等等。我們稍微膚淺一點,看一下. require是怎樣進行路徑加載的吧.

require 路徑解析規則

這里,我們依照官方的說明.前提是:在Y路徑下,使用require(X) 引用. 會按一下步驟進行解析

  1. 如果X是內置的模塊,比如http,net等. 直接返回. over

  2. 如果X帶上'/'或者'./'或者'../'.

    • 會根據X所在的父目錄,確定X所在的絕對位置.

    • 先假設X是文件,然后按照順序依次查找下列文件

      • x

      • x.js

      • x.json

      • x.node如果找到則返回

    • 如果X是目錄,則依次查找下列文件:

      • X/package.json(main 字段)

      • X/index.js

      • X/index.json

      • X/index.node如果找到則返回.

  3. 如果X不是以'/'或'./'或'../'開頭. 則會根據X所在的父目錄,對node_modules進行回朔遍歷. 接著,通過上述確定X為文件或者目錄的方式,進行查找.

  4. 如果上述的流程都沒有找到則會拋出錯誤(Not Found)

這里,我們具體來看一下 node_modules的查找. 假設在路徑/usr/app/shop 下運行 require('bar'); 之后, 程序遍歷的結果是.

  • 首先, 假設bar是文件,查找路徑為

    • /usr/app/shop/node_modules/bar

    • /usr/app/shop/node_modules/bar.js

    • /usr/app/shop/node_modules/bar.json

    • /usr/app/shop/node_modules/bar.node

  • 如果,在該目錄下沒有找到,則會進行回朔(../).則遍歷路徑為:

    • /usr/app/shop/node_modules

    • /usr/app/node_modules

    • /usr/node_modules

    • /node_modules

  • 如果假設為目錄. 類似,查找為:

    • bar/package.json(main)

    • bar/index.js

    • bar/index.json

    • bar/index.node

  • 同樣,也有路徑回朔(../). 如上,這里就不贅述了

require() 運行的內部機制

實際上, nodeJS的壯大, 其一是其本身的異步機制和事件mode 優勢帶動的, 其二就是其本身優秀的模塊機制. 通過 Modules 模塊, nodeJS將其本身的擴展性,提的老高老高. 上述路徑解析,其實就是nodeJS Modules機制中的一部分. 詳情可以參考一下: modules詳情

其實,我們寫的每一個js文件,在run的時候,都會包裹一層Modules.具體情形就是:

(function (exports, require, module, __filename, __dirname) {
  // 模塊源碼
  return exports;
});

實際上,module其實就是Modules的一個實例,在源碼中定義的Modules函數實際內容,并不復雜:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}
module.exports = Module;

可以說,我們所有的模塊都是建立在Module這一個構造函數上的. 那這些對象,我們應該怎么獲取呢?

實際上,clever的童鞋,已經意識到了, module在運行的時候已經傳進來了,我們可以直接調用.

一個簡單的demo:

app.js

console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);

運行: ndoe app.js
結果,為:

module.id:  .
module.exports:  {}
module.parent:  null
module.filename:  /Users/jimmy_thr/Documents/code/shopping/app/sam.js
module.loaded:  false
module.children:  []
module.paths:  [ //內容過多忽略 ]

有興趣的童鞋,可以自己運行試一試.那每個屬性對應的是什么內容呢?

property effect
id 引用的模塊名--當沒有父模塊時為: . 有則為絕對路徑
exports 就是使用 module.exports 導出的方法或者變量
parent 很簡單,就是父模塊.也就是另外一個module實例
filename 模塊的絕對路徑
loaded 用來表示,模塊是否已經全部加載(沒太多用處)
children 數組類型,表示子模塊
paths 包含模塊可能存在的位置,以備下次require的時候搜索

可以看出,通過run之后, 有3個global對象,分別為,require,module,exports. 那實際上,他們3者的關系是什么呢?

我們來看一下源碼里面是怎么做的吧.

Module內部細節

這是require 方法的具體細節:

Module.prototype.require = function(path) {
  return Module._load(path, this);
};

實際上, require 只是一層皮, 里面套的是Module的_load方法.代碼內有很多debug和alert, 去掉檢測的內容,我們來看一下內部機理.

Module._load = function(request, parent, isMain) {

  //  計算絕對路徑
  var filename = Module._resolveFilename(request, parent);

  //  第一步:如果有緩存,取出緩存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否為內置模塊
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第3.1步:加載模塊,生成模塊實例,存入緩存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第3.2步: 載入模塊內容
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第四步:輸出模塊的exports屬性
  return module.exports;
};

這下大概清楚了,實際上, 在路徑解析之前,其實Module 還會對內置模塊進行其他的檢測.實際順序為:

  • 是否已經緩存

  • 是否為內置模塊

  • 加載模塊

    • 生成模塊實例,存入緩存

    • 路徑解析

  • 最終返回module.exports

這里,我們也可以看到NodeJS 模塊加載的另外一個機制.

只要require過后的模塊都會被保存在緩存當中. 當需要再次引用的時候,則會直接從緩存中獲取.

Module里面自定義了很多路徑的處理和緩存的處理。 我們這里, 只關注一下. module.load的內容. 源碼如下

Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

這里很簡單,用來確定文件后綴的加載:

  • X

  • X.js

  • X.json

  • X.node

首先,在理解內部機制之前,我們需要了解一下關于path 模塊。 該模塊通常使用來處理文件路徑的.

  • path.basename(p[, ext])

返回基本的文件名. 如果ext有參數,則表示不帶指定尾綴返回. 比如: usr/home/app.js => app.js . 如果指定ext為 .js 則返回 app . 更好的理解方式為: p-ext

  • path.dirname(p)返回目錄名. usr/home/app.js => usr/home

  • path.extname(p)

    返回文件名的后綴。通常是最后一個'.'到字符串最后. index.html => .html 。如果沒有'.'則會返回一個空字符. index => ''

  • path.format(pathObject)

    將路徑對象轉化為字符串路徑. 即. path.format({//...}) . Object可以帶的屬性有:

    • root

    • dir

    • base

    • ext

    • name一個簡單的demo:

path.format({
    root : "/",
    dir : "/home/user/dir", //后面不用加`/`系統會自動補充
    base : "file.txt",
    name : "file",
    ext : ".txt"
});
// returns '/home/user/dir/file.txt'

實際上, 我們只需要使用一部分即可。俺,常用的組合為: dir + base. 或者 dir+name+ext

  • path.isAbsolute(path)用來檢查路徑是否為絕對路徑。絕對路徑很好理解, 1. 看你的路徑是否在根目錄上. 2. 看你的路徑的開頭是否是 / 。

/usr/path => true, shop/app.js =>false

  • path.join(path1[, ...])使用 / 來連接多個字符,并對 .. 或者 . 進行路徑轉化. 這是一個比較重要的方法. 常常用在路徑處理.

path.join('/foo', 'bar', 'baz/sam', 'quux', '..');
返回為: '/foo/bar/baz/sam'
  • path.normalize(p)對路徑字符串解釋, 會處理 .. 和 . 。

path.normalize('/usr/home/../sam');
返回: '/usr/sam'
  • path.parse(pathString)

    該方法和path.format相反,是將路徑字符串轉化為路徑對象

    path.parse('/home/user/dir/file.txt')
    // returns
    // {
    //    root : "/",
    //    dir : "/home/user/dir",
    //    base : "file.txt",
    //    ext : ".txt",
    //    name : "file"
    // }
  • path.resolve([from ...], to)組合所有的路徑,找出絕對路徑. 如果路徑中不存在以 / 開頭,或者根目錄的話,則以當前js文件所在的目錄為起始參考路徑. NodeJS官方給出一種更好理解的方式:

path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
// cd foo/bar
// cd /tmp/file/
// cd ..
// cd a/../subfile
最后返回: /tmp/subfile
  • path.relative(fromPath, toPath)計算出,相對于fromPath 到 toPath的相對路徑。兩個參數需要是絕對路徑. 在MAC下面開頭需要為 / . 如果不是, 則會默認以執行的js文件所在目錄進行轉化.

path.relative('/usr/home/sam','/usr/app')
返回: ../../app

總結一下:

回到load方法。 該方法主要就是對尾綴進行不同的處理策略:

var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;

再反觀,源碼對不同后綴的處理

Module._extensions['.js'] = function(module, filename) {...}
Module._extensions['.json'] = function(module, filename) {...}
Module._extensions['.node'] = function(module, filename) {...}

找到文件之后,再通過vm模塊,進行編譯處理.最后, 在_compile函數里, 對scope和sandbox進行處理后,爭取運行文件.

Module.prototype._compile = function(content, filename) {
  var self = this;
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

最后就編譯為,我們前文所述的那樣:

(function (exports, require, module, __filename, __dirname) {
  // 模塊源碼
  return exports;
});

通過上文,我們也能夠很好地理解。 出錯的時候,下面的信息到底意味著什么了.

SyntaxError: Unexpected token .
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:414:25)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:313:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:136:18)

來自: https://segmentfault.com/a/1190000004868777

 本文由用戶 ufoe7070 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!