深刻理解webpack的require.context

前言

require.context 實際上是一個很是實用的 api。可是 3-4 年過去了,卻依舊還有不少人不知道如何使用。javascript

而這個 api 主要爲咱們作什麼樣的事情?它能夠幫助咱們動態加載咱們想要的文件,很是靈活和強大(可遞歸目錄)。 能夠作 import 作不到的事情。今天就帶你們一塊兒來分析一下,webpack 的 require.context是如何實現的。java

準備工做

在分析這個 api 以前呢,咱們須要先了解一下一個最簡單的文件,webpack 會編譯成啥樣。webpack

-- src
    -- index.ts
複製代碼
// index.ts
console.log(123)
複製代碼

編譯以後,咱們能夠看見 webpack 會編譯成以下代碼git

// 源碼 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/bundle-only-index.js
 (function(modules) { // webpackBootstrap
 	// The module cache
 	var installedModules = {};
 	// The require function
 	function __webpack_require__(moduleId) {
 		// Check if module is in cache
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		// Flag the module as loaded
 		module.l = true;
 		// Return the exports of the module
 		return module.exports;
 	}
 	// expose the modules object (__webpack_modules__)
 	__webpack_require__.m = modules;
 	// expose the module cache
 	__webpack_require__.c = installedModules;
 	// define getter function for harmony exports
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, {
 				configurable: false,
 				enumerable: true,
 				get: getter
 			});
 		}
 	};
 	// define __esModule on exports
 	__webpack_require__.r = function(exports) {
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};
 	// getDefaultExport function for compatibility with non-harmony modules
 	__webpack_require__.n = function(module) {
 		var getter = module && module.__esModule ?
 			function getDefault() { return module['default']; } :
 			function getModuleExports() { return module; };
 		__webpack_require__.d(getter, 'a', getter);
 		return getter;
 	};
 	// Object.prototype.hasOwnProperty.call
 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
 	// __webpack_public_path__
 	__webpack_require__.p = "";
 	// Load entry module and return exports
 	return __webpack_require__(__webpack_require__.s = "./src/index.ts");
 })
 ({
 "./src/index.ts": (function(module, exports) {
      console.log('123');
    })
 });
複製代碼

初次一看是很亂的,因此爲了梳理結構,我幫你們去除一些跟本文可有可無的。其實主要結構就是這樣而已,代碼很少爲了以後的理解,必定要仔細看下每一行github

// 源碼地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/webpack-main.js

(function(modules) {
  // 緩存全部被加載過的模塊(文件)
  var installedModules = {};
  // 模塊(文件)加載器 moduleId 通常就是文件路徑
  function __webpack_require__(moduleId) {
    // 走 cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache) 解釋比我清楚
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });
    // 執行咱們的模塊(文件) 目前就是 ./src/index.ts 而且傳入 3 個參數
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded 解釋比我清楚
    module.l = true;
    // Return the exports of the module 解釋比我清楚
    return module.exports;
  }
  // 開始加載入口文件
  return __webpack_require__((__webpack_require__.s = './src/index.ts'));
})({
  './src/index.ts': function(module, exports, __webpack_require__) {
    console.log('123');
  }
});
複製代碼

__webpack_require__ 就是一個模塊加載器,而咱們全部的模塊都會以對象的形式被讀取加載web

modules = {
    './src/index.ts': function(module, exports, __webpack_require__) {
       console.log('123');
    }
}
複製代碼

咱們把這樣的結構先暫時稱之爲 模塊結構對象api

正片

瞭解了主體結構以後咱們就能夠寫一段require.context來看看效果。咱們先新增 2 個 ts 文件而且修改一下咱們的 index.ts,以便於測試咱們的動態加載。promise

--- src
    --- demos
        --- demo1.ts
        --- demo2.ts
    index.ts
複製代碼
// index.ts
// 稍後咱們經過源碼分析爲何這樣寫
function importAll(contextLoader: __WebpackModuleApi.RequireContext) {
  contextLoader.keys().forEach(id => console.log(contextLoader(id)));
}

const contextLoader = require.context('./demos', true, /\.ts/);
importAll(contextLoader);
複製代碼

查看咱們編譯後的源碼,發現多了這樣一塊的 模塊結構對象緩存

// 編譯後代碼地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/contex-sync.js#L82-L113
{
'./src/demos sync recursive \\.ts': function(module, exports, __webpack_require__) {
  var map = {
    './demo1.ts': './src/demos/demo1.ts',
    './demo2.ts': './src/demos/demo2.ts'
  };

  // context 加載器,經過以前的模塊加載器 加載模塊(文件) 
  function webpackContext(req) {
    var id = webpackContextResolve(req);
    var module = __webpack_require__(id);
    return module;
  }
  
  // 經過 moduleId 查找模塊(文件)真實路徑
  // 我的在這不喜歡 webpack 內部的一些變量命名,moduleId 它都會編譯爲 request
  function webpackContextResolve(req) {
    // id 就是真實文件路徑
    var id = map[req];
    // 說實話這波操做沒看懂,目前猜想是 webpack 會編譯成 0.js 1.js 這樣的文件 若是找不到誤加載就出個 error
    if (!(id + 1)) {
      // check for number or string
      var e = new Error('Cannot find module "' + req + '".');
      e.code = 'MODULE_NOT_FOUND';
      throw e;
    }
    return id;
  }
  
  // 遍歷獲得全部 moduleId
  webpackContext.keys = function webpackContextKeys() {
    return Object.keys(map);
  };
  // 獲取文件真實路徑方法
  webpackContext.resolve = webpackContextResolve;
  // 該模塊就是返回一個 context 加載器
  module.exports = webpackContext;
  // 該模塊的 moduleId 用於 __webpack_require__ 模塊加載器
  webpackContext.id = './src/demos sync recursive \\.ts';
}
複製代碼

我在源碼中寫了很詳細的註釋。看完這段代碼就不難理解文檔中所說的require.context 會返回一個帶有 3 個API的函數(webpackContext)了。ide

接着咱們看看編譯後 index.ts 的源碼

'./src/index.ts': function(module, exports, __webpack_require__) {
  function importAll(contextLoader) {
    contextLoader.keys().forEach(function(id) {
      // 拿到全部 moduleId,在經過 context 加載器去加載每個模塊
      return console.log(contextLoader(id));
    });
  }
  var contextLoader = __webpack_require__(
    './src/demos sync recursive \\.ts'
  );
  importAll(contextLoader);
}
複製代碼

很簡單,能夠發現 require.context 編譯爲了 __webpack_require__加載器而且加載了 id 爲./src/demos sync recursive \\.ts 的模塊,sync代表咱們是同步加載這些模塊(以後咱們在介紹這個參數),recursive 表示須要遞歸目錄查找。自此,咱們就徹底能明白 webpack 是如何構建全部模塊而且動態加載的了。

進階深刻探究 webpack 源碼

咱們知道 webpack 在 2.6 版本後,在加載模塊時,能夠指定 webpackMode 模塊加載模式,咱們能使用幾種方式來控制咱們要加載的模塊。經常使用的 mode通常爲sync lazy lazy-once eager

因此在 require.context 是同樣適用的,咱們若是查看一下@types/webpack-env就不難發現它還有第四個參數。

簡要來講

  • sync 直接打包到當前文件,同步加載並執行
  • lazy 延遲加載會分離出單獨的 chunk 文件
  • lazy-once 延遲加載會分離出單獨的 chunk 文件,加載過下次再加載直接讀取內存裏的代碼。
  • eager 不會分離出單獨的 chunk 文件,可是會返回 promise,只有調用了 promise 纔會執行代碼,能夠理解爲先加載了代碼,可是咱們能夠控制延遲執行這部分代碼。

文檔在這裏 webpack.docschina.org/api/module-…

這部分文檔很隱晦,也多是文檔組沒有跟上,因此若是咱們去看 webpack 的源碼的話,能夠發現真正實際上是有 6 種 mode。

mode類型定義 github.com/webpack/web…

那 webpack 究竟是如何作到可遞歸獲取咱們的文件呢?在剛剛上面的源碼地址裏咱們能發現這樣一行代碼。

這一看就是去尋找咱們所須要的模塊。因此咱們跟着這行查找具體的源碼。

這就是 require.context 是如何加載到咱們文件的具體邏輯了。其實就是fs.readdir而已。最後獲取到文件以後在經過 context 加載器來生成咱們的模塊結構對象。好比這樣的代碼就是負責生成咱們sync類型的context加載器。你們能夠具體在看別的5種類型。

6種類型加載邏輯而且生成 context 加載器的模塊結構對象 github.com/webpack/web…

總結

1.學習瞭解 webpack 是如何組織加載一個模塊的,webpack 的加載器如何運做,最後如何生成編譯後的代碼。

2.原本僅僅只是想了解 require.context 如何實現的,卻發現了它第三個參數有 6 種mode,這部分卻也是 webpack 文檔上沒有的。

3.從一個實用的 API 出發,探索了該 api 的實現原理,而且一塊兒閱讀了部分 webpack 源碼。

4.探索本質遠比你成爲 API 的搬運工更重要。只有你不斷地探尋本質,才能夠發現世界的奧祕。

最後拋磚引玉,能夠按照這樣的思路再去學習另外 6 種 mode 編譯後的代碼。

文章裏編譯後的代碼,都在這裏 >>> github.com/MeCKodo/req…

我的網站 >>> www.meckodo.com

最後常年招人

廈門 RingCentral 外企,福利待遇廈門頂尖

5點半下班 5點半下班 5點半下班

有須要的聯繫我~

相關文章
相關標籤/搜索