Node.js 中的 require 是如何工做的?

做爲前端開發者,不可避免天天都要跟 Node.js 打交道。Node 遵循 Commonjs 規範,規範的核心是經過 require 來加載依賴的其餘模塊。咱們已常常習慣於使用社區提供的各類庫,但對於模塊引用的背後原理知之甚少。這篇文章經過源碼閱讀,淺析在 commonjs 規範中 require 背後的工做原理。javascript

require 從哪裏來?

你們都知道,在 node js 的模塊/文件中,有些「全局」變量是能夠直接使用的,好比 require, module, __dirname, __filename, exportshtml

其實這些變量或方法並非「全局」的,而是在 commonjs 模塊加載中, 經過包裹的形式,提供的局部變量。前端

module.exports = function () {
    console.log(__dirname);
}
複製代碼

通過 compile 以後,就有了 module__dirname 等變量能夠直接使用。java

(function (exports, require, module, __filename, __dirname) {
    module.exports = function () {
        console.log(__dirname);
    }
})
複製代碼

這也能夠很好解答初學者經常會困惑的問題,爲何給 exports 賦值,require 以後獲得的結果是 undefined?node

// 直接給 exports 賦值是不會生效的
(function (exports, module) {
    exports = function () {
    }
})(m.exports, m)

return m.exports;
複製代碼

直接賦值只是修改了局部變臉 exports 的值。最終 export 出去的 module.exports 沒有被賦值。git

require 的查找過程

文檔中描述得很是清楚,簡化版 require 模塊的查找過程以下:github

在 Y 路徑下,require(X)

1. 若是X是內置模塊(http, fs, path 等), 直接返回內置模塊,再也不執行
2. 若是 X 以 '/' 開頭,把 Y 設置爲文件系統根目錄
3. 若是 X 以 './''/''../' 開頭
   a. 按照文件的形式加載(Y + X),根據 extensions 依次嘗試加載文件 [X, X.js, X.json, X.node]
      若是存在就返回該文件,再也不繼續執行。
   b. 按照文件夾的形式加載(Y + X),若是存在就返回該文件,再也不繼續執行,若找不到將拋出錯誤
     a. 嘗試解析路徑下 package.json main 字段
     b. 嘗試加載路徑下的 index 文件(index.js, index.json, index.node)
4. 搜索 NODE_MODULE,若存在就返回模塊
   a. 從路徑 Y 開始,一層層往上找,嘗試加載(路徑 + 'node_modules/' + X)
   b. 在 GLOBAL_FOLDERS node_modules 目錄中查找 X
5. 拋出 "Not Found" Error
複製代碼

例如在 /Users/helkyle/projects/learning-module/foo.js 中 require('bar') 將會從/Users/helkyle/projects/learning-module/ 開始逐層往上查找 bar 模塊(不是以 './', '/', '../' 開頭)。npm

'/Users/helkyle/projects/learning-module/node_modules',
'/Users/helkyle/projects/node_modules',
'/Users/helkyle/node_modules',
'/Users/node_modules',
'/node_modules'
複製代碼

須要注意的是,在使用 npm link 功能的時候,被 link 模塊內的 require 會以被 link 模塊在文件系統中的絕對路徑進行查找,而不是 main module 所在的路徑。json

舉個例子,假設有兩個模塊。後端

/usr/lib/foo
/usr/lib/bar
複製代碼

經過 link 形式在 foo 模塊中 link bar,會產生軟連 /usr/lib/foo/node_modules/bar 指向 /usr/lib/bar,這種狀況下 bar 模塊下 require('quux') 的查找路徑是 /usr/lib/bar/node_modules/ 而不是 /usr/lib/foo/node_modules

我以前踩過的坑

Cache 機制

在實踐過程當中能瞭解到,實際上 Node module require 的過程會有緩存。也就是兩次 require 同一個 module 會獲得同樣的結果。

// a.js
module.exports = {
    foo: 1,
};

// b.js
const a1 = require('./a.js');
a1.foo = 2;

const a2 = require('./a.js');

console.log(a2.foo); // 2
console.log(a1 === a2); // true
複製代碼

執行 node b.js,能夠看到,第二次 require a.js 跟第一次 require 獲得的是相同的模塊引用。

源碼上看,require 是對 module 經常使用方法的封裝。

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

  let require;
  // 簡化其餘代碼
  require = function require(path) {
    return mod.require(path);
  };

  function resolve(request, options) {
    validateString(request, 'request');
    return Module._resolveFilename(request, mod, false, options);
  }

  require.resolve = resolve;

  function paths(request) {
    validateString(request, 'request');
    return Module._resolveLookupPaths(request, mod);
  }

  resolve.paths = paths;
  require.main = process.mainModule;
  require.extensions = Module._extensions;
  require.cache = Module._cache;

  return require;
}
複製代碼

跟蹤代碼看到,require() 最終調用的是 Module._load 方法:

// 忽略代碼,看看 load 的過程發生了什麼?
Module._load = function(request, parent, isMain) {
  // 調用 _resolveFilename 得到模塊絕對路徑
  const filename = Module._resolveFilename(request, parent, isMain);

  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    // 若是存在緩存,直接返回緩存的 exports 對象
    return cachedModule.exports;
  }
  // 內建模塊直接返回
  const mod = loadNativeModule(filename, request, experimentalModules);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // 建立新的 module 對象
  const module = new Module(filename, parent);

  // main module 特殊處理
  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }
  // 緩存 module
  Module._cache[filename] = module;
  
  // 返回 module exports 對象
  return module.exports;
};
複製代碼

到這裏,module cache 的原理也很清晰,模塊在首次加載後,會以模塊絕對路徑爲 key 緩存在 Module._cache 屬性上,再次 require 時會直接返回已緩存的結果以提升 效率。在控制檯打印 require.cache 看看。

// b.js
require('./a.js');
require('./a.js');

console.log(require.cache);
複製代碼

緩存中有兩個key,分別是 a.js, b.js 文件在系統中的絕對路徑。value 則是對應模塊 load 以後的 module 對象。因此第二次 require('./a.js') 的結果是 require.cache['/Users/helkyle/projects/learning-module/a.js'].exports 和第一次 require 指向的是同一個 Object

{ 
    '/Users/helkyle/projects/learning-module/b.js': 
       Module {
         id: '.',
         exports: {},
         parent: null,
         filename: '/Users/helkyle/projects/learning-module/b.js',
         loaded: false,
         children: [ [Object] ],
         paths: 
          [ '/Users/helkyle/projects/learning-module/node_modules',
            '/Users/helkyle/projects/node_modules',
            '/Users/helkyle/node_modules',
            '/Users/node_modules',
            '/node_modules' ] },
  '/Users/helkyle/projects/learning-module/a.js': 
       Module {
         id: '/Users/helkyle/projects/learning-module/a.js',
         exports: { foo: 1 },
         parent: 
          Module {
            id: '.',
            exports: {},
            parent: null,
            filename: '/Users/helkyle/projects/learning-module/b.js',
            loaded: false,
            children: [Array],
            paths: [Array] },
         filename: '/Users/helkyle/projects/learning-module/a.js',
         loaded: true,
         children: [],
         paths: [ 
            '/Users/helkyle/projects/learning-module/node_modules',
            '/Users/helkyle/projects/node_modules',
            '/Users/helkyle/node_modules',
            '/Users/node_modules',
            '/node_modules' 
        ]
   }
}
複製代碼

應用——實現 Jest 的 mock module 效果

jest 是 Facebook 開源的前端測試庫,提供了不少很是強大又實用的功能。mock module 是其中很是搶眼的特性。使用方式是在須要被 mock 的文件模塊同級目錄下的 __mock__ 文件夾添加同名文件,執行測試代碼時運行 jest.mock(modulePath)jest 會自動加載 mock 版本的 module

舉個例子,項目中有個 apis 文件,提供對接後端 api

// /projects/foo/apis.js
module.export = {
    getUsers: () => fetch('api/users')
};
複製代碼

在跑測試過程當中,不但願它真的鏈接後端請求。這時候根據 jest 文檔,在 apis 文件同級目錄建立 mock file

// /projects/foo/__mock__/apis.js
module.exports = {
    getUsers: () => [
        {
            id: "1",
            name: "Helkyle"
        },
        {
            id: "2",
            name: "Chinuketsu"
        }
    ]
}
複製代碼

測試文件中,主動調用 jest.mock('./apis.js') 便可。

jest.mock('./apis.js');
const apis = require('./apis.js');

apis.getUsers()
  .then((users) => {
    console.log(users);
    // [ { id: '1', name: 'Helkyle' }, { id: '2', name: 'Chinuketsu' } ]
  })
複製代碼

瞭解 require 的基礎原理以後,咱們也來實現相似的功能,將加載 api.js 的語句改寫成加載 __mock__/api.js

使用 require.cache

因爲緩存機制的存在,提早寫入目標緩存,再次 require 將獲得咱們指望的結果。

// 提早 require mock apis 文件,產生緩存。
require('./__mock__/apis.js');

// 給即將 require 的文件路徑寫入緩存
const originalPath = require.resolve('./apis.js');
require.cache[originalPath] = require.cache[require.resolve('./__mock__/apis.js')];

// 獲得的將是緩存版本
const apis = require('./apis.js');

apis.getUsers()
  .then((users) => {
    console.log(users);
    // [ { id: '1', name: 'Helkyle' }, { id: '2', name: 'Chinuketsu' } ]
  })
複製代碼

魔改 module._load

基於 require.cache 的方式,須要提早 require mock module。👆提到了,因爲最終都是經過 Module._load 來加載模塊,在這個位置進行攔截便可完成按需 mock

const Module = require('module');
const originalLoad = Module._load;

Module._load = function (path, ...rest) {
  if (path === './apis.js') {
    path = './__mock__/apis.js';
  }
  return originalLoad.apply(Module, [path, ...rest]);
}

const apis = require('./apis.js');
apis.getUsers()
  .then((users) => {
    console.log(users);
  })
複製代碼

注意:以上內容僅供參考。從實際運行結果上看,Jest 有本身實現的模塊加載機制,跟 commonjs 有出入。好比在 jestrequire module 並不會寫入 require.cache

程序啓動時的 require

查閱 Node 文檔發現,在 Command Line 章節也有一個 --require ,使用這個參數能夠在執行業務代碼以前預先加載特定模塊。

舉個例子,編寫 setup 文件,往 global 對象上掛載 it, assert 等方法。

// setup.js
global.it = async function test(title, callback) {
  try {
    await callback();
    console.log(`✓ ${title}`);
  } catch (error) {
    console.error(`✕ ${title}`);
    console.error(error);
  }
}
global.assert = require('assert');
複製代碼

給啓動代碼添加 --require 參數。引入 global.assert, global.it,就能夠在代碼中直接使用 assert, it 不用在測試文件中引入。

node --require './setup.js' foo.test.js
複製代碼
// foo.test.js
// 不須要 require('assert');
function sum (a, b) {
    return a + b;
}

// 沒有 --require 會報 it is not defined
it('add two numbers', () => {
    assert(sum(2, 3) === 5);
})
複製代碼

相關閱讀

相關文章
相關標籤/搜索