前言
webpack完整的一次初始化、编译、输出的逻辑前几篇文章有了介绍,作为目前最受欢迎的打包工具其内部的实例逻辑必然是复杂,一些细节点我觉得暂不必深究,而且webpack还有一些其他的点值得去探究,例如:
- Tree-shaking
- 模块热替换
- scope hoisting
这些点之后会较为深入的学习了解,本文主要分析最基本的输出文件的结构和逻辑,主要想要了解点如下:
- 输出文件的结构和逻辑
- AMD、CommonJs、ES Module模块在webpack最后输出文件中区别
- 按需加载(异步加载)
输出文件的结构和逻辑
为了研究输出文件的最基本的结构和逻辑,实例如下:
// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');
入口文件main.js中只存在一个show.js的模块,按照这个实例去输出最后的文件。
最后输出文件结构是一个IIFE函数(立即执行函数),基本结构如下:
(function(modules) {
// 相关逻辑
})(模块数组);
立即执行函数中逻辑细节
function(modules) { // webpackBootstrap 启动函数
// 已加载模块缓存
var installedModules = {};
// webpack实现的require函数用于加载模块
function __webpack_require__(moduleId) {
// 已加载模块直接从缓存中取
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并cache
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 调用指定模块的具体逻辑
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块已被加载
module.l = true;
// 暴露模块的输出内容
return module.exports;
}
// 提供获取所有模块对象
__webpack_require__.m = modules;
// 提供获取所有已加载的模块
__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
});
}
};
// 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 = "";
/**
* - 入口模块的导入执行
* - 定义入口文件模块的下标位置__wepack_require__.s
* - 暴露相关模块指定内容
**/
return __webpack_require__(__webpack_require__.s = 0);
}
最基本的实例立即执行函数中的逻辑总结为:
- 定义__webpack_require__函数
- 创建module对象,定义三个属性:模块id、模块是否已加载状态、输出内容对象
- 缓存module
- 执行模块的逻辑
- 暴露模块的输出内容
- 在__webpack_require__函数上定义相关属性和方法
- 执行入口文件的加载开启webpack的启动
模块数组
模块数组参数是指所有的模块对应的函数的集合,数据结构是数组类型。
本部分基于的实例涉及到:main.js入口文件、show.js逻辑文件,所以对应对应这这边的模块数组如下:
([
/* 0 */
(function(module, exports, __webpack_require__) {
const show = __webpack_require__(1);
show('Webpack');
}),
/* 1 */
(function(module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出
module.exports = show;
})
]);
模块数组作为立即执行函数的入参传入,即modules参数对应的值,其中0表示入口文件main.js的逻辑,1表示逻辑文件show.js的具体逻辑。这里需要注意两点:
- 入口文件模块始终作为第一个模块传入
- 模块数组中使用function(module, exports) {}来包裹模块文件中具体逻辑
- 模块中存在对其他模块的依赖,会转换成_webpck_require_(moduleId)这样的形式来加载对应的模块
不同模块规范的模块加载
在分析输出文件的结构和逻辑时的实例是:
- 使用CommonJs模块规范定义show.js中的输出
- 使用CommosJs模块规范定义了main.js中的模块加载
总结下:
- 使用CommonJs规范编写,webpack输出文件中会自实现module.exports和require函数来实现模块的加载和输出
- main.js中使用webpack自定义的require来加载、show.js中使用webpack中的定义module.exports来暴露内容
下面分析不同模块规范的都是基于目前的实例main.js和show.js来,分别使用不同模块规范来重写输出和加载。
ES模块规范的语句加载模块 + 暴露模块内容
入口文件main.js
import show from './show.js';
show('Webpack');
show.js模块
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
export default show;
在当前情况下比较输出文件,webpackBootstrap部分即立即执行函数逻辑保持不变,相对于CommonJs规范不同有:
// main.js模块
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__show_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__show_js__["a" /* default */])('Webpack');
}
// show.js模块
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
__webpack_exports__["a"] = (show);
}
webpack因为是Node.js平台下编写的工具,所以其配置文件等是按照CommonJs规范写的,对于ES模块就需要兼容处理,使用ES模块最后输出文件中不同在于:
- 对于export default暴露的模块内容,webpack exports是默认a属性来表示输出内容
- 加载show模块时也是取a属性,同时定义__esModule来表示是ES模块
如果暴露内容是采用export,相关的show模块的内容与export default相同,只是加载时取值时需解构一层。如果暴露内容是采用export {},则show模块逻辑存在差异,具体如下:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.d(__webpack_exports__, "a", function() { return show; });
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
}
这里值得注意的是_webpack_require_.d方法的执行逻辑,具体如下:
__webpack_require__.d = function(exports, name, getter) {
// hasOwnProperty判断
if(!__webpack_require__.o(exports, name)) {
// 定义相关属性
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
AMD模块规范的语句加载模块 + 定义模块
入口文件main.js
const show = require('./show.js');
show('Webpack');
show.js模块
define(function() {
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
return show;
});
AMD规范下show模块的兼容处理代码如下:
function(module, exports, __webpack_require__) {
// 依赖模块 + 加载后的依赖模块对象
var __WEBPACK_AMD_DEFINE_ARRAY__,
__WEBPACK_AMD_DEFINE_RESULT__;
// 定义__WEBPACK_AMD_DEFINE_RESULT__函数并执行
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [],
__WEBPACK_AMD_DEFINE_RESULT__ = function() {
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
return show;
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
// 将define中暴露的内容赋值给module.exports
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)
)
}
使用CommonJs规范的语句加载模块 + ES模块规范暴露模块内容
入口文件main.js
// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
show('Webpack');
show.js模块
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
export default show;
在这种情况下看看输出文件的不同,首先说说不变的地方:
立即执行函数中内容没有任何变化,即不同的模块系统保持相同
变动的地方是模块数组中,实例涉及到两个模块main.js和show.js,主要是show.js模块职工ES模块的兼容处理,实际上ES模块输出模块内容也存在3种形式:
-
export default 内容
function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } __webpack_exports__["default"] = (show); }
与CommonJs规范下输出不同点有2处:
- Object.defineProperty(_webpack_exports_, “__esModule”, { value: true }):定义__esModule属性
- _webpack_exports_[“default”] = (show);
实际上__webpack_exports实际上就是webpack自己实现的module.exports。需要注意的是,这里输出的内容的default,那对应main.js使用CommonJs规范加载模块只能调用default名称了,即:
const { default: show } = require('./show.js'); show('Webpack');
-
export 内容
function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); const show = function(content) { window.document.getElementById('app').innerText = 'Hello,' + content; }; __webpack_exports__["show"] = show; }
与export default不同的是,这里直接输出show名称,即加载该模块直接可使用暴露的名称。
-
export { 内容 }
function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); __webpack_require__.d(__webpack_exports__, "show", function() { return show; }); function show (content) { window.document.getElementById('app').innerText = 'Hello,' + content; }; }
在ES模块规范时候就分析过__webpack_require__.d方法,该方法实际上该函数将getter定义到指定对象上。此时使用CommonJs规范加载该模块,就无法直接调用,需要解构一层,即:
const { show } = require('./show.js'); show('Webpack');
使用ES模块规范的语句加载模块 + CommonJs规范暴露模块内容
入口文件main.js
import show from './show.js';
show('Webpack');
show.js模块
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
module.exports = show;
在这种情况下比较输出文件的变化:
- 立即执行函数逻辑没有任何变化
- 传参模块数组中main.js模块中存在变化
因为show模块是通过CommonJs规范方式暴露内容的,而加载是使用ES模块形式来的,所以必然存在兼容的逻辑,具体如下:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 定义__esModule属性
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// 加载show模块
var __WEBPACK_IMPORTED_MODULE_0__show_js__ = __webpack_require__(1);
var __WEBPACK_IMPORTED_MODULE_0__show_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__show_js__);
__WEBPACK_IMPORTED_MODULE_0__show_js___default()('Webpack');
需要理解的是_webpack_require_.n的逻辑处理,该函数定义于立即执行函数中,具体逻辑如下:
__webpack_require__.n = function(module) {
// 判断是否是ES模块
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
// 在getter函数上定义a属性
__webpack_require__.d(getter, 'a', getter);
return getter;
};
因为show模块的module中没有定义__esModule属性,则相应的getter则是:
function getModuleExports() { return module; }
在当前情况下,即module为show模块的exports对象,即show函数。
总结
webpack通过自己实现的require和module来兼容支持AMD、CommonJs、ES模块规范,不论是什么模块规范,webpackBootstrap启动函数的逻辑没有变更,唯一的变更就是模块是按照哪种的模块规范去暴露内容和加载依赖模块的,本文主要分析AMD、CommonJs、ES之间的互相兼容,主要分为:
- ES形式暴露内容、CommonJs形式暴露内容、AMD形式暴露内容
- ES形式加载模块:需要通过_webpack_require_.d或.n特殊处理
- 非ES形式加载模块:直接从module.exports直接获取暴露内容
而作为不同规范定义被加载的模块,实际的处理是不同的:
- ES模块:模块逻辑被兼容处理,重新定义getter而且webpack内部定义a属性
- CommonJs模块:模块逻辑原样输出
- AMD模块:模块逻辑被兼容处理,define的回调函数执行结果赋值给module.exports
按需加载
webpack支持通过import(*)的形式按需加载,按需加载会将加载的模块从输出文件中分割出来生成单独一个chunk,那么按需加载输出文件会有什么差异?
需要改造入口文件main.js,在main.js中按需加载show.js,代码如下:
import('./show.js').then((show) => {
show('Webpack');
});
依据webpack的配置,对于按需加载或加载外部资源来说,output.publicPath的设置是重要的,否则会找不到指定的文件,所以需要在配置文件中配置publicPath。
分析当前生成的输出文件,按照位置来分存在3处不同:
webpackBootstrap函数内的不同
webpackBootstrap是webpack的启动函数,当没有按需加载时,该函数定义了require函数以及相关方法。存在按需加载,新增了:
- webpackJsonp函数定义
- installedChunks属性
- _webpack_require_.e即requireEnsure
- _webpack_require_.oe
webpackJsonp
webpackJsonp函数挂载在window对象下,其他都地方都可调用。
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
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;
}
// moreModules对应就是按需加载的模块
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
while(resolves.length) {
resolves.shift()();
}
};
installedChunks属性
该属性类似于installedModules功能,installedChunks这里记录所有chunk加载的情况,是加载中还是已加载等。
_webpack_require_.e
import()的具体实现,即按需加载的核心逻辑。
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
// chunk已加载
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
// chunk正在加载中
if(installedChunkData) {
return installedChunkData[2];
}
// 未加载chunk的promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 通过<script async>来实现chunk的按需加载
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);
}
// 这里的逻辑可看出publicPath的作用,按需加载依赖于此
script.src = __webpack_require__.p + "" + 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;
};
即_webpack_require_.e函数就用用来实现按需加载的,其背后的逻辑还是使用script标签 + async属性。
_webpack_require_.oe
该方法是用来输出错误错误信息和抛出异常的。
模块数组传参的不同
按需加载逻辑存在传递给webpackBootstrap函数的参数也存在不同,具体是:
需要按需加载的模块不会存在模块数组中,而是在独立的chunk里
在当前实例下,模块数组中只存在入口文件main.js模块,具体如下:
function(module, exports, __webpack_require__) {
// __webpack_require__.e(0) 即show.js模块对应的chunk
__webpack_require__.e(0)
.then(__webpack_require__.bind(null, 1))
.then((show) => {
show('Webpack');
})
})
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1))
// 等价于
__webpack_require__.e(0).then(function() {
const bindRequire = __webpack_require__.bind(null);
return bindRequire(1);
})
针对于show.js对应chunk,加载分为3个阶段:
- 触发加载:_webapck_require_.e开启指定chunk文件加载
- 加载中:对应的promise被执行,但是promise的状态始终是pending
- 加载后
- 加载成功:会执行webpackJsonp中的逻辑,将当前chunk状态置为0,此时执行_webapck_require_.e中模块已加载的promise,,即触发第一个then函数,即使用内置require函数加载show.js模块
- 加载失败:将当前chunk状态置为undefined,并且抛出错误信息
按需加载模块chunk文件
对应该实例中show.js模块,该chunkId是0,对应的chunk内容如下:
webpackJsonp(
[0],
[
(function(module, exports) {
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
module.exports = show;
})
]
);
可以看到按需加载分割出去的代码会调用webpackJsonp函数:
- 该函数第一个参数是chunkIds,即chunkId数组
- 包含当前模块的逻辑的函数集合
总结
按需加载的逻辑步骤如下:
- 将按需加载的模块分组到指定chunk文件,文件中调用webpackJsonp
- 输出文件中定义webpackJsonp和相关import()支持的实现
- 按需加载逻辑执行,webpack会调用import()的实现(_webpack_require_.e)来开启模块文件的加载(script标签 + async属性)
- 当模块异步加载成功后,执行chunk文件逻辑,即webpackJsonp立即执行设置对应的installedChunks中当前chunk状态为已加载
- 触发按需加载模块的then回调处理
其中需要注意的是installedChunks中值的状态,相关值存在3种:
- undefined:加载失败状态
- [resolve, reject, promise对象]:数组形式的,模块加载中
- 0:模块加载成功