webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。
无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。
假设现在有如下两个文件:
// index.js
const test2 = require('./test2')
function test() {}
test()
test2()
// test2.js
function test2() {}
module.exports = test2
以上两个文件使用 CommonJS 规范来导入导出文件,打包后的代码如下(已经删除了不必要的注释):
(function(modules) { // webpackBootstrap
// The module cache
// 01 定义对象用来存放被加载过的模块
var installedModules = {};
// The require function
// 02 下面方法用来替换 require 和 import加载操作, webpack 实现的 require() 函数
// 下面的这个方法就是 webpack 当中自定义的,它的核心作用就是返回模块的 exports
function __webpack_require__(moduleId) {
// Check if module is in cache
// 02-1判断当前缓存中是否已经存在当前加载模块内容,如果模块已经加载过,直接返回缓存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 02-2 如果缓存中不存在当前加载模块,则创建一个新模块,执行模块内容,并加载模块,并放入缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
// 02-3 调用当前moduleId对应的模块函数,并完成内容的加载
// call() 为了将this指向module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
// 02-4 当上述方法调用完成后,将 l 的值设置为 true, 表示当前模块内容已加载完成了
module.l = true;
// Return the exports of the module
// 02-5 加载工作完成后,要将拿到的内容返回至调用位置
return module.exports;
}
// expose the modules object (__webpack_modules__)
// 03 定义 m 属性用于保存modules ,将所有的模块挂载到 require() 函数上
__webpack_require__.m = modules;
// expose the module cache
// 04 定义 c 属性, 用于保存catch, 将缓存对象挂载到 require() 函数上
__webpack_require__.c = installedModules;
// Object.prototype.hasOwnProperty.call
// 05 定义 0 方法,用于判断对象的身上是否存在指定的属性
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// define getter function for harmony exports
// 06 定义 d 方法,用来在对象上添加指定的属性,同时给该属性提供一个getter
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
// 07 定义 r 方法,用来标识当前模块是否是ESModule类型
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
// 08 定义一个 n 方法,用来设置具体的getter
__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;
};
// __webpack_public_path__
// 09 定义 p 属性,用于保存静态资源访问路径
__webpack_require__.p = "";
// Load entry module and return exports
// 10 调用 _webapck_require_方法执行 模块导入 和 模块加载,加载入口模块,并返回模块对象
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/test2.js": (function(module, exports) {
eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
})
});
可以看到 webpack 实现的模块加载系统非常简单,仅仅只有一百行代码。
1: 打包后的代码其实是一个立即执行函数,传入的参数是一个对象。
这个对象 key:当前被加载模块名文件路径,以文件内容为 value,它包含了所有打包后的模块。
{
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/test2.js": (function(module, exports) {
eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
})
}
将这个立即函数化简一下,相当于:
(function(modules){
// ...
})({
path1: function1,
path2: function2
})
再看一下这个立即函数做了什么:
installedModules
,作用是缓存已经加载过的模块。__webpack_require__()
。__webpack_require__()
加载入口模块。其中的核心就是 __webpack_require__()
函数,它接收的参数是 moduleId
,其实就是文件路径。
它的执行过程如下:
export
对象,即 module.exports
。module
,并放入缓存。exports
对象。 // The require function
// webpack 实现的 require() 函数
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;
}
从上述代码可以看到,在执行模块函数时传入了三个参数,分别为 module
、module.exports
、__webpack_require__
。
其中 module
、module.exports
的作用和 CommonJS 中的 module
、module.exports
的作用是一样的,而 __webpack_require__
相当于 CommonJS 中的 require
。
在立即函数的最后,使用了 __webpack_require__()
加载入口模块。并传入了入口模块的路径 ./src/index.js
。
__webpack_require__(__webpack_require__.s = "./src/index.js");
我们再来分析一下入口模块的内容。
(function(module, exports, __webpack_require__) {
eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
})
入口模块函数的参数正好是刚才所说的那三个参数,而 eval 函数的内容美化一下后和下面内容一样:
const test2 = __webpack_require__("./src/test2.js")
function test() {}
test()
test2()
//# sourceURL=webpack:///./src/index.js?
将打包后的模块代码和原模块的代码进行对比,可以发现仅有一个地方发生了变化,那就是 require
变成了 __webpack_require__
。
再看一下 test2.js
的代码:
function test2() {}
module.exports = test2
//# sourceURL=webpack:///./src/test2.js?
从刚才的分析可知,__webpack_require__()
加载模块后,会先执行模块对应的函数,然后返回该模块的 exports
对象。而 test2.js
的导出对象 module.exports
就是 test2()
函数。所以入口模块能通过 __webpack_require__()
引入 test2()
函数并执行。
到目前为止可以发现 webpack 自定义的模块规范完美适配 CommonJS 规范。
将刚才用 CommonJS 规范编写的两个文件换成用 ES6 module 规范来写,再执行打包。
// index.js
import test2 from './test2'
function test() {}
test()
test2()
// test2.js
export default function test2() {}
使用 ES6 module 规范打包后的代码和使用 CommonJS 规范打包后的代码绝大部分都是一样的。
一样的地方是指 webpack 自定义模块规范的代码一样,唯一不同的是上面两个文件打包后的代码不同。
{
"./src/index.js":(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/test2.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
})
}
可以看到传入的第二个参数是 __webpack_exports__
,而 CommonJS 规范对应的第二个参数是 exports
。将这两个模块代码的内容美化一下:
// index.js
__webpack_require__.r(__webpack_exports__);
var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
function test() {}
test()
Object(_test2__WEBPACK_IMPORTED_MODULE_0__["default"])()
//# sourceURL=webpack:///./src/index.js?
// test2.js
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
function test2() {}
//# sourceURL=webpack:///./src/test2.js?
可以发现,在每个模块的开头都执行了一个 __webpack_require__.r(__webpack_exports__)
语句。并且 test2.js
还多了一个 __webpack_require__.d()
函数。
我们先来看看 __webpack_require__.r()
和 __webpack_require__.d()
是什么。
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
原来 __webpack_require__.d()
是给 __webpack_exports__
定义导出变量用的。例如下面这行代码:
__webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
它的作用相当于 __webpack_exports__["default"] = test2
。这个 "default"
是因为你使用 export default
来导出函数,如果这样导出函数:
export function test2() {}
它就会变成 __webpack_require__.d(__webpack_exports__, "test2", function() { return test2; });
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.r()
函数的作用是给 __webpack_exports__
添加一个 __esModule
为 true
的属性,表示这是一个 ES6 module。
添加这个属性有什么用呢?
主要是为了处理混合使用 ES6 module 和 CommonJS 的情况。
例如导出使用 CommonJS module.export = test2
导出函数,导入使用 ES6 module import test2 from './test2
。
打包后的代码如下:
// index.js
__webpack_require__.r(__webpack_exports__);
var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
var _test2__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_test2__WEBPACK_IMPORTED_MODULE_0__);
function test() {}
test()
_test2__WEBPACK_IMPORTED_MODULE_0___default()()
//# sourceURL=webpack:///./src/index.js?
// test2.js
function test2() {}
module.exports = test2
//# sourceURL=webpack:///./src/test2.js?
从上述代码可以发现,又多了一个 __webpack_require__.n()
函数:
__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;
};
先来分析一下入口模块的处理逻辑:
__webpack_exports__
导出对象标识为 ES6 module。test2.js
模块,并将该模块的导出对象作为参数传入 __webpack_require__.n()
函数。__webpack_require__.n
分析该 export
对象是否是 ES6 module,如果是则返回 module['default']
即 export default
对应的变量。如果不是 ES6 module 则直接返回 export
。按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。
在 webpack 中可以使用 import
和 require.ensure
来引入需要动态导入的代码,例如下面这个示例:
// index.js
function test() {}
test()
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
// 动态导入模块(懒加载单个文件)
import('./test2').then((test2) => {
console.log(test2)
})
})
// test2.js
export default function test2() {return "懒加载导出内容"}
index.html