Node.js 中的模塊

module 在 nodejs 裏是一個很是核心的內容,本文經過結合 nodejs 的源碼簡單介紹 nodejs 中模塊的加載方式和緩存機制。若是有理解錯誤的地方,請及時提醒糾正。javascript

ppt 地址:http://47.93.21.106/sharing/m...html

CommonJS

提到 nodejs 中的模塊,就不能不提到 CommonJS。大部分人應該都知道 nodejs 的模塊規範是基於 CommonJS 的,但其實 CommonJS 不只僅定義了關於模塊的規範,完整的規範在這裏:CommonJS。內容很少,感興趣的同窗能夠瀏覽一下。固然重點是在 模塊 這一章,若是仔細讀一下 CommonJS 中關於模塊的規定,能夠發現和 node 中的模塊使用是很是吻合的。java

Contract

CommonJS 中關於模塊的規定主要有三點:node

  • Requiregit

    模塊引入的方式和行爲,涉及到經常使用的 `require()`。
  • Module Contextgithub

    模塊的上下文環境,涉及到 `module` 和 `exports`。
  • Module Identifiers面試

    模塊的標識,主要用於模塊的引入

Usage

在 node.js 裏使用模塊的方式很簡單,通常咱們都是這麼用的:json

// format.js
const moment = require('moment');
/* 格式化時間戳 */
exports.formatDate = function (timestamp) {
    return moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
}

上面是一個 format.js 文件,內容比較簡單,引入了 moment 模塊,並導出了一個格式化時間的方法供其餘模塊使用。bootstrap

可是你們有沒有考慮過,這裏的 requireexports 是在哪裏定義的,爲何咱們能夠直接拿來使用呢?api

實際上,nodejs 加載文件的時候,會在文件頭尾分別添加一段代碼:

  • 頭部添加

(function (exports, require, module, filename, dirname) {
  • 尾部添加

});

最後處理成了一個函數,而後才進行模塊的加載:

(function (exports, require, module, __filename, __dirname) {
    // format.js
    const moment = require('moment');
    /* 格式化時間戳 */
    exports.formatDate = function (timestamp) {
        return moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
    }
});

因此 exports, require, module 其實都是在調用這個函數的時候傳進來的了。

這裏還有兩個比較細微的點,也是在不少面試題裏面會出現的

  • 經過 varletconst 定義的變量變成了局部變量;沒有經過關鍵字聲明的變量會泄露到全局

  • exports 是一個形參,改變 exports 的引用不會起做用

第一點是做用域的問題,第二點能夠問到 js 的參數傳遞是值傳遞仍是引用傳遞。

證實

固然,若是隻是這樣講,好像只是個人一面之詞,怎麼證實 nodejs 確實是這樣包裝的呢,這裏能夠用兩個例子來證實:

➜  echo 'dvaduke' > bad.js
➜  node bad.js
/Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1
(function (exports, require, module, __filename, __dirname) { dvaduke
                                                              ^

ReferenceError: dvaduke is not defined
    at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1:63)
    at Module._compile (module.js:569:30)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
    at Function.Module.runMain (module.js:605:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:575:3

我在 bad.js 裏面隨便輸入了一個單詞,而後運行這個文件,能夠看到運行結果會拋出異常。在異常信息裏面咱們會驚訝地發現 node 把那行函數頭給打印出來了,而在 bad.js 裏面是隻有那個單詞的。

➜  echo 'console.log(arguments)' > arguments.js
➜  node arguments.js
{ '0': {},
  '1':
   { [Function: require]
     resolve: [Function: resolve],
     main:
      Module {
        id: '.',
        exports: {},
        parent: null,
        filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js',
        loaded: false,
        children: [],
        paths: [Array] },
     extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
     cache: { '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js': [Object] } },
  '2':
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js',
     loaded: false,
     children: [],
     paths:
      [ '/Users/sunhengzhe/Documents/learn/node/modules/demos/node_modules',
        '/Users/sunhengzhe/Documents/learn/node/modules/node_modules',
        '/Users/sunhengzhe/Documents/learn/node/node_modules',
        '/Users/sunhengzhe/Documents/learn/node_modules',
        '/Users/sunhengzhe/Documents/node_modules',
        '/Users/sunhengzhe/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  '3': '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js',
  '4': '/Users/sunhengzhe/Documents/learn/node/modules/demos' }

在 arguments.js 這個文件裏打印出 argumens 這個參數,咱們知道 arguments 是函數的參數,那麼打印結果能夠很好的說明 node 往函數裏傳入了什麼參數:第一個是 exports,如今固然是空,第二個是 require,是一個函數,第三個是 module 對象,還有兩個分別是 __filename__dirname

源碼

發現這個地方以後我相信你們都會對 nodejs 的源碼感興趣,而 nodejs 自己是開源的,咱們能夠在 github 上找到 nodejs 的源碼:node

實際上包裝模塊的代碼就在 /lib/module.js 裏面:

Module.prototype._compile = function(content, filename) {

  content = internalModule.stripShebang(content);

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  
  // ...
}

_compile 函數是編譯 nodejs 文件會執行的方法,函數中的 content 就是咱們文件中的內容,能夠看到調用了一個 Module.wrap 方法,那麼 Module.wrap 作了什麼呢?這裏須要找到另外一個文件,包含內置模塊定義的 /lib/internal/bootstrap_node.js,裏面有對 wrap 的操做:

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

確實是前面說到的,添加函數頭尾的內容。

彩蛋?

其實知道這個處理以後,咱們能夠開一些奇怪的腦洞,好比寫一段好像會報錯的文件:

// inject.js
});

(function () {
    console.log('amazing');

這個文件看起來沒頭沒尾,可是通過 nodejs 的包裝後,是能夠運行的,會打印出 amazing,看起來頗有意思。

Core

上面只是帶你們看了一下 module.js 裏的一小段代碼,實際上若是要搞明白 nodejs 模塊運做的機制,有三個文件是比較核心的:

  • /lib/module.js 加載非內置模塊

  • /lib/internal/module.js 提供一些相關方法

  • /lib/internal/bootstrap_node 定義了加載內置模塊的 NativeModule,同時這也是 node 的入口文件

咱們知道 node 的底層是 C 語言編寫的,node 運行是,會調用 node.cc 這個文件,而後會調用 bootstrap_node 文件,在 bootstrap_node 中,會有一個 NativeModule 來加載 node 的內置模塊,包括 module.js,而後經過 module.js 加載非內置模塊,好比用戶自定義的模塊。(因此說模塊是多麼基礎)

調用關係以下:

invoking

Module

下面重點介紹一下 module。在 nodejs 裏面,一般一個文件就表明了一個模塊,而 module 這個對象就表明了當前這個模塊。咱們能夠嘗試打印一下 module:

echo "console.log(module)" > print-module.js
node print-module.js

打印結果以下:

Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/sunhengzhe/Documents/learn/node/modules/demo-1.js',
  loaded: false,
  children: [],
  paths:
   [ '/Users/sunhengzhe/Documents/learn/node/modules/node_modules',
     '/Users/sunhengzhe/Documents/learn/node/node_modules',
     '/Users/sunhengzhe/Documents/learn/node_modules',
     '/Users/sunhengzhe/Documents/node_modules',
     '/Users/sunhengzhe/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }

能夠看到 module 這個對象有不少屬性,exports 咱們先不說了,它就是這個模塊須要導出的內容。filename 也不說了,文件的路徑名。paths 很明顯,是當前文件一直到根路徑的全部 node_modules 路徑,查找第三方模塊時會用到它。咱們下面介紹一下 id、parent、children 和 loaded。

module.id

在 nodejs 裏面,模塊的 id 分兩種狀況,一種是當這個模塊是入口文件時,此時模塊的 id 爲 .,另外一種當模塊不是入口文件時,此時模塊的 id 爲模塊的文件路徑。

舉個例子,當文件是入口文件時:

➜  echo 'console.log(module.id)' > demo-1-single-file.js
➜  node demo-1-single-file.js
.

此時 id 爲 .

當文件不是入口文件時:

➜  cat demo-2-require-other-file.js
const other = require('./demo-1-single-file');
console.log('self id:', module.id);
➜  node demo-2-require-other-file.js
/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-1-module-id/demo-1-single-file.js
self id: .

運行 demo-2-require-other-file.js,首先打印出 demo-1-single-file 的內容,能夠發現此時 demo-1-single-file 的 id 是它的文件名:由於它如今不是入口文件了。而做爲入口文件的 demo-2-require-other-file.js 的 id 變成了 .

module.parent & module.children

這兩個含義很明確,是模塊的調用方和被調用方。

若是咱們直接打印一個入口文件的 module,結果以下:

➜  echo 'console.log(module)' > demo-1-single-file.js
➜  node demo-1-single-file.js
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js',
  loaded: false,
  children: [],
  paths:
   [...] }

篇幅限制,就不顯示 paths 了。能夠看到 parent 爲 null:由於沒有人調用它;children 爲空:由於它沒有調用別的模塊。那麼咱們再新建一個文件引用一下這個模塊:

➜  cat demo-2-require-other-file.js
require('./demo-1-single-file');
console.log(module);
➜  node demo-2-require-other-file.js
Module {
  id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js',
  exports: {},
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [...] },
  filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js',
  loaded: false,
  children: [],
  paths:
   [...] }
------------------------
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js',
  loaded: false,
  children:
   [ Module {
       id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js',
       exports: {},
       parent: [Circular],
       filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [...] }

上面輸出了兩個 module,爲了方便閱讀,我用分割線分隔了一下。第一個 module 是 demo-1-single-file 打印出來的,它的 parent 如今有值了,由於 demo-2-require-other-file.js 引用它了。它的 children 依舊是空,畢竟它沒有引用別人。

而 demo-2-require-other-file.js 的 parent 爲 null,children 有值了,能夠看到就是 demo-1-single-file。

注意裏面還出現了 [Circular],由於 demo-1-single-file 的 parent 的 children 就是它本身,爲了防止循環輸出,nodejs 在這裏省略掉了,應該很好理解。

module.loaded

loaded 從字面意思上也好理解,表明這個模塊是否已經加載完了。但咱們會發如今上面的全部輸出中,loaded 都是 false。

➜  cat demo-1-print-sync.js
console.log(module.loaded);
➜  node demo-1-print-sync.js
false

咱們能夠在 node 的下一個 tick 裏面去輸出,就能獲得正確的 loaded 的值了:

➜  cat demo-2-print-next-tick.js
setImmediate(function () {
    console.log(module.loaded);
});
➜  node demo-2-print-next-tick.js
true

模塊的加載

模塊究竟是如何加載的?在 /lib/module.js 裏,能夠找到模塊加載的函數 _load,這裏 node 的註釋很好地描述了加載的次序:

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
//...

翻譯一下,大概就是這個流程:

  1. 有緩存(二次加載)

直接讀取緩存內容

  1. 無緩存(首次加載或清空緩存以後)

    1. 路徑分析

    2. 文件定位

    3. 編譯執行

無緩存

首先看一下無緩存的狀況。nodejs 首先須要對文件進行定位,找到文件才能進行加載,其實全部的細節都隱藏在了 require 方法裏面,咱們調用 require,nodejs 返回模塊對象,那麼 require 是怎麼找到咱們須要的模塊的呢?

簡單來說,大體是:

  • 嘗試加載核心模塊

  • 嘗試以文件形式加載

    • X

    • X.js

    • X.json

    • X.node

  • 嘗試做爲目錄查找,尋找 package.json 文件,嘗試加載 main 字段指定的文件

  • 嘗試做爲目錄查找,尋找 index.js、index.json、index.node

  • 嘗試做爲第三方模塊進行加載

  • 拋出異常

這裏涉及的代碼細節比較複雜,建議先直接閱讀 nodejs 的官方文檔,文檔對定位的順序描述的很是詳細:https://nodejs.org/dist/latest-v8.x/docs/api/modules.html#modules_all_together

緩存

若是有緩存的話,會直接返回緩存內容。好比這裏有個文件,內容就是打印一行星號:

➜  cat print.js
console.log('********');

若是咱們在另外一個文件裏引入這個文件兩次,那麼會輸出兩行星號嗎?

➜  demo-5-cache cat demo-1-just-print-multiply.js
require('./print');
require('./print');
➜  demo-5-cache node demo-1-just-print-multiply.js
********

答案是不會的,由於第一次 require 後,nodejs 會把文件緩存起來,第二次 require 直接取得緩存的內容,參考 /lib/module.js 中的代碼:

Module._load = function(request, parent, isMain) {
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
      // 更新 parent 的 children
      updateChildren(parent, cachedModule, true);
      return cachedModule.exports;
  }

  //...

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  //...
}

清空緩存

那麼,若是咱們要清空緩存,勢必須要清除 Module._cache 中的內容。然而在文件裏,咱們只能拿到 module 對象,拿不到 Module 類:

➜  cat demo-2-get-Module.js
console.log(Module)
➜  node demo-2-get-Module.js
/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1
(function (exports, require, module, __filename, __dirname) { console.log(Module)
                                                                          ^

ReferenceError: Module is not defined
    at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1:75)
    at Module._compile (module.js:569:30)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
    at Function.Module.runMain (module.js:605:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:575:3

可是是否沒有辦法去清空緩存了呢?固然是有的。這裏咱們先看 require 是怎麼來的。

以前提到,require 是經過函數參數的方式傳入模塊的,那麼咱們能夠看一下,傳入的 require 的究竟是什麼?回到 _compile 方法:

Module.prototype._compile = function(content, filename) {
  content = internalModule.stripShebang(content);

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  
  // ...
  
  var require = internalModule.makeRequireFunction(this);
  
  result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  // ...
  
  return result;
}

簡化後的代碼如上,函數內容通過包裝以後生成了一個新的函數 compiledWrapper,而後把一些參數傳了進去。咱們能夠看到 require 是從一個 makeRequireFunction 的函數中生成的。

makeRequireFunction 函數是在 /lib/internal/module.js 中定義的,看下代碼:

function makeRequireFunction(mod) {
  const Module = mod.constructor;

  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

  function resolve(request) {
    return Module._resolveFilename(request, mod);
  }

  require.resolve = resolve;

  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;

  require.cache = Module._cache;

  return require;
}

若是咱們直接打印 require,其實就和這裏面定義的 require 是同樣的:

➜  cat demo-1-require.js
console.log(require.toString());
➜  node demo-1-require.js
function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

其實這個 require 也沒有作什麼事情,又調用了 mod 的 require,而 mod 是經過 makeRequireFunction 傳進來的,傳入的是 this,因此歸根到底,require 是 module 原型上的方法,也就是 module.prototype.require,參考 /lib/module.js 中的代碼。

固然這裏咱們先不用追究 require 的實現方式,而是注意到 makeRequireFunction 中對 require 的定義,咱們能夠發現一行關於 _cache 的代碼:

function makeRequireFunction(mod) {
  // ...

  function require(path) {}

  require.cache = Module._cache;
  
  //..

  return require;
}

因此 nodejs 很貼心地,把 Module._cache 返回給咱們了,其實只要清空 require.cache 便可。而根據上面的代碼,Module._cache 是經過 filename 來做爲緩存的 key 的,因此咱們只須要清空模塊對應的文件名。

針對上面提到的例子,清空 print.js 的緩存:

require('./print');
// delete cache
delete require.cache[require.resolve('./print')];
require('./print');

而後再打印一下

➜  node demo-1-just-print-multiply.js
********
********

就是兩行星號了。

這裏用到了 require 的一個 resolve 方法,它和直接調用 require 方法比較像,都能找到模塊的絕對路徑名,但直接 require 還會加載模塊,而 require.resolve() 只會找到文件名並返回。因此這裏利用文件名將 cache 裏對應的內容刪除了。

調試 nodejs 的源碼

本文介紹了一些 nodejs 中的源碼內容,在學習 nodejs 的過程當中,若是想查看 nodejs 的源碼(我以爲這是一個必備的過程),那麼就須要去調試源碼,打幾個 log 看一下是否是和你預期的一致,這裏說一下怎麼調試 nodejs 的源碼。

  1. 下載 node 源碼 git@github.com:nodejs/node.git

  2. 進入源碼目錄執行 ./configure & make -j

  3. 上一步以後會在 ${源碼目錄}/out/Release/node 裏生成一個執行文件,將這個文件做爲你的 node 執行文件。

  4. 每次修改源碼後從新執行 make 命令。

好比修改代碼以後,運行 make,而後這樣運行文件便可:

➜ /Users/sunhengzhe/Documents/project/source-code/node/out/Release/node demo-1-single-file.js

參考

  1. 樸靈《深刻淺出 Node.js》

  2. Node.js Documentation

  3. requiring-modules-in-node-js-everything-you-need-to-know

相關文章
相關標籤/搜索