解析webpack运行时(一)

273 阅读8分钟

webpack是个bundler,每一个文件都是一个模块。写代码的时候,无论你使用的是CommonJS模块规范、ES模块规范还是混合使用两者,webpack最终打包出来的产物,都能在浏览器/NodeJS环境中正常运行。下面将会由一个简单的示例,来讲解一下webpack打包出来的代码产物,运行时是如何运行的。

配置简单的示例

文件夹目录

index.js

const test = require('./test');

function selfFuc(){};

selfFuc();
test();

test.js

function test() {
  const a = '1';
  return a;
};

module.exports = test;

index.es.js

import test from './test.es';

function selfFuc(){};

selfFuc();
test();

test.es.js

export default function test() {
  const a = '1';
  return a;
};

webpack.config.js

module.exports = {
  mode: 'development', // 建议配置为development,默认是production,会有代码压缩,很难阅读
  entry: './src/index.es.js', // 根据入口文件改 entry
  output: {
    filename: 'index.es.js' // 增加es后缀方便区分 不同的打包产物
  },
  devtool: false,
  };

package.json

{
  "name": "webpack-demos",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  }
}

执行命令npm i安装完依赖后,

执行npm run build 就可以用webpack打包了

CommonJS模块规范的打包产物

dist/index.js

/******/ (() => { // webpackBootstrap
  /******/ 	var __webpack_modules__ = ({
    
    /***/ "./src/test.js":
    /*!*********************!*\
    !*** ./src/test.js ***!
    *********************/
    /***/ ((module) => {
      
      function test() {
        const a = '1';
        return a;
      };
      
      module.exports = test;
      
      /***/ })
    
    /******/ 	});
  /************************************************************************/
  /******/ 	// The module cache
  /******/ 	var __webpack_module_cache__ = {};
  /******/ 	
  /******/ 	// The require function
  /******/ 	function __webpack_require__(moduleId) {
    /******/ 		// Check if module is in cache
    /******/ 		var cachedModule = __webpack_module_cache__[moduleId];
    /******/ 		if (cachedModule !== undefined) {
      /******/ 			return cachedModule.exports;
      /******/ 		}
    /******/ 		// Create a new module (and put it into the cache)
    /******/ 		var module = __webpack_module_cache__[moduleId] = {
      /******/ 			// no module.id needed
      /******/ 			// no module.loaded needed
      /******/ 			exports: {}
      /******/ 		};
    /******/ 	
    /******/ 		// Execute the module function
    /******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    /******/ 	
    /******/ 		// Return the exports of the module
    /******/ 		return module.exports;
    /******/ 	}
  /******/ 	
  /************************************************************************/
  var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    /*!**********************!*\
    !*** ./src/index.js ***!
    **********************/
    const test = __webpack_require__(/*! ./test */ "./src/test.js");
    
    function selfFuc(){};
    
    selfFuc();
    test();
  })();
  
  /******/ })()
;

整体结构

先整体看一下这个产物文件,其实是一个立即执行函数,就是IIFE(Immediately Invoked Function Expression)。

再往里看一层,进行了一些变量和函数的定义、最后又是一个IIFE。

其实比较容易得发现,最后的IIFE的代码内容实际上就是我们entry文件的代码内容,只是把require函数换成了,webpack生成的__webpack_require__函数。

webpack_modules

变量会定义除了入口文件外的所有模块,key为模块的路径,value是模块函数,模块函数的函数体就是源模块的代码。在__webpack_require__函数中会使用到。

webpack_module_cache

缓存对象,用于缓存已经执行过的模块函数。在__webpack_require__函数中会使用到。

webpack_require

这个函数是webpack运行时中比较关键的函数了。入参moduleId是一个模块的模块路径。

具体的逻辑:

  • 根据moduleId从__webpack_modules__中找到对应的模块函数,创建module对象,执行并返回module.exports
  • 如果moduleId 在 __webpack_require__中存在,则直接返回module.exports,不重复执行模块函数。

样例解析

现在再回去看dist/index.js的代码,进行解析。

  • 定义__webpack_modules__,除了入口文件./src/index.js之外,只有./src/test.js模块,所以,__webpack_modules__对象只有一个key-value对,去掉一些注释和括号会比较好看一点:
var __webpack_modules__ = {
  "./src/test.js": (module) => {
    function test() {
      const a = '1';
      return a;
    };
    module.exports = test;
  },
};
  • 定义了__webpack_module_cache__对象、__webpack_require__函数。
  • 执行最后的IIFE。
    • test 的值等于__webpack_require__函数的返回值。这个时候的入参是./src/test.js
      • 先判断__webpack_module_cache__缓存中是否存在./src/test.js模块。不存在,则重新创建一个module,并塞到__webpack_module_cache__缓存中。
      • 执行__webpack_modules__./src/test.js的模块函数。
      • ./src/test.js的模块函数返回值是test函数,所以在IIFE中,test变量就被赋值为./src/test.js模块中的test函数
    • 然后执行selfFuc()test()

ES模块规范的打包产物

dist/index.es.js

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "./src/test.es.js":
/*!************************!*\
  !*** ./src/test.es.js ***!
  ************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (/* binding */ test)
/* harmony export */ });
function test() {
    const a = '1';
    return a;
};

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/*!*************************!*\
  !*** ./src/index.es.js ***!
  *************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test_es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test.es */ "./src/test.es.js");


function selfFuc(){};

selfFuc();
(0,_test_es__WEBPACK_IMPORTED_MODULE_0__["default"])();
})();

/******/ })()
;

整体结构

对比CommonJS模块规范的产物,使用ES模块规范的打包产物,整体结构上是一样的。但是比CommonJS规范产物多了一些代码。

  • 新增定义了__webpack_require__.d``__webpack_require__.o``__webpack_require__.r函数
  • 每个模块的,模块函数中多进行了一下两个操作:
    • 调用__webpack_require__.r函数,入参是module.exports
    • 调用__webpack_require__.d函数,入参是module.exportsdefinition模块输出定义对象。

webpack_require.r

这个函数使用时,入参是module.exports。作用就是给module.exports对象增加一个属性__esModule,值为true,标识这是一个ES模块规范的模块输出。

webpack_require.d

在解释这个函数的作用时,可以先结合着看一下ES模块规范产物中的./src/test.js模块函数和__webpack_require__函数。

./src/test.es.js模块函数中,虽然我们写代码的时候指明了需要把test函数export出去,但是产物中并没有把test函数往module.exports上挂载(CommonJS规范是有相关操作的)。但是当./src/index.es.js模块引用./src/text.es.js模块时,使用的__webpack_require__函数是没有发生改变的,return的还是module.exports_test_es__WEBPACK_IMPORTED_MODULE_0__["default"]是怎么样拿到./src/test.es.js模块中的test函数的呢?起作用的就是__webpack_require__.d函数。

这个函数有两个入参,第一个就是module.exports,第二个是definition--我把他叫做“模块输出定义对象”,webpack会根据本模块的export情况生成这个对象,对象里的key是export 的变量名(例子中export default 写法 key就是 default),value是一个getter函数,返回模块代码中的需要export的变量。

__webpack_require__.d函数的逻辑就是,循环遍历definition对象中的key,如果definition对象中存在,但是module.exports对象不存在(证明是输出的变量是通过ES模块规范输出的),就使用Object.defineProperty方法给module.exports对象定义这个key,值就是definition中对应的getter函数definition[key]。这样的话,当./src/index.es.js模块,通过_test_es__WEBPACK_IMPORTED_MODULE_0__["default"]语句,就可以获取到./src/test.es.js模块的test函数了。

webpack_require.o

这个函数实现实际上就是Object.prototype.hasOwnProperty这个方法的调用。这个方法就不在这里具体的介绍,直接看MDN吧。

方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)

样例解析

ES模块规范产物其实跟CommonJS模块规范产物基本上是一致的,只需要结合__webpack_require__.d函数,理解_test_es__WEBPACK_IMPORTED_MODULE_0__["default"]语句是如何获取到./src/test.es.js模块的test函数。

  • 定义了__webpack_module_cache__对象、__webpack_require____webpack_require__.d__webpack_require__.o__webpack_require__.r函数。
  • 执行最后的IIFE。
    • 执行__webpack_require__函数,这个时候的入参是./src/test.es.js_test_es__WEBPACK_IMPORTED_MODULE_0__ 的值等于__webpack_require__(./src/test.es.js)的返回值。
      • 先判断__webpack_module_cache__缓存中是否存在./src/test.es.js模块。不存在,则重新创建一个module,并塞到__webpack_module_cache__缓存中。
      • 执行__webpack_modules__./src/test.es.js的模块函数。
        • 调用__webpack_require__.r函数,声明 ESM 模块标识。
        • 调用__webpack_require__.d函数,实现将通过ES模块规范导出的内容附加到module.exports对象上。
        • 其他代码执行,返回module.exports
    • 然后执行selfFuc()
    • _test_es__WEBPACK_IMPORTED_MODULE_0__["default"]的值就是__webpack_require__.d调用时指定的getter函数的返回值--test函数,所以在IIFE中,_test_es__WEBPACK_IMPORTED_MODULE_0__["default"]()就是调用了./src/test.es.js模块中的test函数。

混合使用两种规范

新建一个index.mix.js文件。

混用两种规范有四种可能性:

  • 以ESM的语法引入CommonJS的模块
  • 以ESM的语法引入ESM的模块
  • 以CommonJS的语法引入CommonJS的模块
  • 以CommonJS的语法引入ESM的模块

index.mix.js(ESM) x test.js(CommonJS)

./src/index.mix.js文件

import test from './test';

function selfFuc(){};

selfFuc();
test();

稍微修改一下./src/test.js文件

function test() {
    const a = '1';
    return a;
};

module.exports = {
    test,
};

打包产物./dist/index.mix.js

/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/test.js":
/*!*********************!*\
  !*** ./src/test.js ***!
  *********************/
/***/ ((module) => {

function test() {
    const a = '1';
    return a;
};

module.exports = {
    test,
};

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/compat get default export */
/******/ 	(() => {
/******/ 		// getDefaultExport function for compatibility with non-harmony modules
/******/ 		__webpack_require__.n = (module) => {
/******/ 			var getter = module && module.__esModule ?
/******/ 				() => (module['default']) :
/******/ 				() => (module);
/******/ 			__webpack_require__.d(getter, { a: getter });
/******/ 			return getter;
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be in strict mode.
(() => {
"use strict";
/*!**************************!*\
  !*** ./src/index.mix.js ***!
  **************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ "./src/test.js");
/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_test__WEBPACK_IMPORTED_MODULE_0__);
// import test from './test.es';
// const test = require('./test.es');


function selfFuc(){};

selfFuc();
(0,_test__WEBPACK_IMPORTED_MODULE_0__.test)();
})();

/******/ })()
;

整体结构

整体结构还是差不多的。这次的变动是:

  • 新增了__webpack_require__.n函数。
  • 新增了一个没有用到的变量var _test__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_test__WEBPACK_IMPORTED_MODULE_0__)

webpack_require.n

函数的入参是模块函数执行结果,也就是module.exports

这个函数的作用就是判断模块的module.expots上时候有__esModule值,如果有,就返回一个getter函数() => module.exports.default,否则就返回另一个getter函数() => module.exports

概括起来就是,这个函数会返回一个getter函数,当模块被标识为ESMdodule是,返回ES模块的export default。当模块不是ESModule,返回module.exports

为什么需要用这样的一个函数呢?

我们可以先把上面我提到的四种混用的情况都试一遍,观察一下四份产物。四份产物这里就不一一列举了,直接给结论:

只有当使用ESM语法来引入CommonJS模块的时候,__webpack_require__.n函数才会被调用,入参是ComminJS模块的模块函数执行的返回值(module.exports)。

因为,在ESM的import语法中,有这样的一个语法糖:

import xx from 'xxx',这样是与import { default as xx } from 'xxx'等价的,在CommonJS的import语法中没有这样的用法,require的左边都是等于引入模块的module.exports。并且,webpack在对CommonJS的模块进行处理时,没有在CommonJS模块的模块函数代码中处理default的逻辑,在对ESM的模块进行处理时,在ESM模块的模块函数代码中用__webpack_require__.d函数对default进行了处理。以ES模块规范的打包产物为例子:

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (/* binding */ test)
/* harmony export */ });

为了兼容两种规范的混用,也就是使用ESM语法 import xx from 'xx' 来引入CommonJS模块时,此时,xx的值就是 CommonJS模块的module.exports