写在前面的
1. 版本说明
使用的 webpack 版本 3.0.0。
2. webpack 的核心思想
- 一切皆“模块(module)”
- 按需加载
一、开始
1. 准备
当前只创建一个文件 index.js,该文件作为入口文件,代码如下。
console.log(‘hello, world‘);
接着使用 webpack 来进行打包,执行的命令如下。
webpack index.js index.bundle.js
2. 分析
打包文件生成的一大堆代码,实际上就是一个自执行函数,仅传入一个参数为 modules,且该对象为一个数组。
(function (modules) { // ... })([function (module, exports) { function sayHello () { console.log(‘hello‘); } }])
该函数的作用就是管理模块,它的内部定义了两个主要的对象 installedModules 对象和 __webpack_require__(moduleId) 函数对象。
installedModules 对象初始化代码如下。
var installedModules = {};
来看一下函数对象 __webpack_require__ 的定义,它的作用与 require 函数相同。
// require 函数
function __webapck_require__(moduleId) {
// 如果模块在缓存中 if (installedModules[moduleId]) { return installedModules[moduleId].exports; }
// 创建一个新的模块(并将它放在缓存中) var module = installedModules[moduleId] = { i: moduleId, l: false,// l是loaded的缩写,指代是否已经加载 exports: {} };
// 执行模块的函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块已经加载 module.l = true;
// 返回模块的 exports 对象 return module.exports; }
看到这里,我们可以看出 installedModules 对象其实是被当作字典使用,key 是模块的 id,value 是代表模块状态和导出的一个对象,如下。
{ i: moduleId, // 模块的 id l: false, // l是loaded的缩写,指代是否已经加载 exports: {} // 模块的导出对象 }
__webpack_require__ 还被定义了许多的静态方法和静态对象。
// 外放 modules 对象(__webpack_modules__) __webpack_require__.m = modules; // 外放模块的缓存 __webpack_require__.c = installedModules; // 定义 getter 函数以便友好的加载模块 __webpack_require__.d = function (exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: 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;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
在函数的最后,结果是导入 moduleId = 0 的模块,就是执行了入口文件。
return __webpack_require__(__webpack_require__.s = 0);
你定义的入口文件中的内容被转换为。
(function (module, __webpack_exports__, __webpack_require__) { console.log(‘hello, world!‘); });
传入立即函数表达式中的参数为一个数组对象,而入口文件转换的函数为数组中的第一个元素。注意 webpack 添加的注释 /* 0 */ 代表该模块的 moduleId 为 0,而 webpack 其实是将每一个资源文件看作一个模块,并将其指定一个标识 moduleId。
[ /* 0 */ (function(module, __webpack_exports__, __webpack_require__) { // ... }) ]
二、添加依赖
1. 准备
添加一个新的 js 文件,命名为 util.js,代码如下。
export function add (a, b) {
return a + b; }
入口文件 index.js 代码修改如下。
import util from ‘./util.js‘
console.log(‘1 + 2 = ‘ + util.add(1, 2));
执行之前的命令,使用 webpack 构建工具进行打包。
2. 分析
此时查看 index.bundle.js 下的代码,之后生成的立即执行函数的传入参数发生变化,还记得之前说过该参数为一个数组,此时的数组如下。
[ /* 0 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1); console.log(‘1 + 2 = ‘ + __WEBPACK_IMPORTED_MODULE_0__util_js__["a" /* default */].add(1 + 2)); }), /* 1 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony default export */ __webpack_exports__["add"] = add;
function (a, b) { return a + b; } }) ]
可见,webpack 将每个文件视为一个独立的模块,它分析模块之间的依赖关系,将模块中 import export 相关的内容做一次替换,比如。
import b from ‘./b‘ export default { name: ‘c‘ } // 最后被转化为 var __WEBPACK_IMPORTED_MODULE_0_b__ = __webpack_require__(0) // 这里需要特别注意一点,webpack 将 a 属性作为某块的 default 值 __webpack_exports__["a"] = ({ name: ‘c‘ })
再给所有模块外面加一层包装函数,使其成为模块初始化函数,接着把所有模块初始化函数合成一个数组,赋值给 modules 变量。
三、模块的动态导入
1. 准备
修改入口文件 index.js 的代码如下。
import(‘./util.js‘).then(function (util) { console.log(‘1 + 2 = ‘ + util.add(1 + 2)); })
执行之前的命令,使用 webpack 构建工具进行打包。
2. 分析
这次打包后不仅获取了 index.bundle.js 文件,还产生了一个 0.index.bundle.js 文件,接下来先分析 o.index.bundle.js。
webpackJsonp([0],[ /* 0 */, /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["add"] = add; function add (a, b) { return a + b; } /***/ }) ]);
之后可以看到该块代码以 JSONP 的形式被加载,这里可以看出该代码被加载后立即执行 webpackJsonp 这个方法,这个方法为 index.bundle.js 中新增的方法。在看这个方法之前,先看一下 index.bundle.js 中新增的一个对象。
var installedChunks = {
1: 0
};
该对象记录 chunk 的安装状态,而不记录 chunk 相关的具体内容,具体内容仍保存到 installedModules 对象中。installedChunks 也是被当作一个字典使用,key 为 chunk 的 id(此处要注意与 mouleId 的区别,每个 chunk 中至少会拥有一个 module),value 可为 0(已加载)、[resolve, reject](正在加载)、undefined(未加载),可以在 requireEnsure(chunkId) 方法中观察到这种状态的变化。
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
if (installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
// 一个 Promise 意味“正在加载”
if (installedChunkData) {
return installedChunkData[2];
}
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
var head = document.getElementsByTagName(‘head‘)[0];
var script = document.createElement(‘script‘);
script.type = ‘text/javascript‘;
script.charset = ‘utf-8‘;
script.async = true;
script.timeout = 120000;
if(__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js";
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error(‘Loading chunk ‘ + chunkId + ‘ failed.‘));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
最后看 webpackJsonp 函数,如下。
var parentJsonpFunction = window["webpackJsonp"]; window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // 添加 “moreModules”到 modules 对象上 // 然后所有的 “chunkIds” 已经加载并调用 callback var moduleId, chunkId, i = 0, resolves = [], result; for(; i<chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while(resolves.length) { resolves.shift()(); } };
该方法有三个参数,chunkIds 为该模块的先行 chunk,moreModules 为此次新添加的模块,executeModules 暂时不知道其作用。该方法先对正在加载的先行 chunk 进行处理,还记的么?正在加载的 chunk 代表其状态的对象的值为 [resolve, reject],这里将它们的 resolve 方法保存到局部定义的 resolves 数组中。还记得 modules 对象么,该对象为 webpack 生成的最外层立即执行函数的参数,这里继续将 moreModules 安装到 modules 对象中。最后执行保存在 resolves 数组中的 resolve 方法。
可见异步加载模块时主要是使用 Promise 来包装 jsonp 请求需要的 chunk 文件,之后将其添加到 modules 参数中,随后的调用与同步的步骤的相同的。