-
Notifications
You must be signed in to change notification settings - Fork 3
Open
Labels
Description
Webpack不同于Rollup,其总是会在打包产物添加额外的代码,即Webpack模块加载器。
本文旨在分析Webpack模块加载器的功能逻辑。
打包源码
//index.js
import chunk, {chunkFn} from './chunk'
export const test = chunk
export const testFn = chunkFn
//chunk.js
export default 'chunk'
export function chunkFn() {
return 'chunk'
}
打包产物
以下是libraryTarget = 'window'
的构建产物,其他模块化产物的模块加载器代码相同。
window["$library"] =
(function(modules){
var installedModules = {}
function __webpack_require__(moduleId){
//判断模块是否已加载
if(installedModules[moduleId])
//已加载则直接返回模块结果
return installedModules[moduleId].exports
//创建新模块
var module = installedModules[moduleId] = {
i: moduleId,//id
l: false,//loaded
exports: {}//存储模块的运行结果
}
//执行模块逻辑
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
)
//记录模块已执行
module.l = true
//返回模块的运行结果
return module.exports
}
//定义只读属性
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
//定义为ES模块
__webpack_require__.r = function(exports) {
//设置[Symbol.toStringTags]属性
//以被Object.prototype.toString.call读取为'[object Module]'
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
}
//工具方法 hasOwnProperty
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
//加载入口模块
return __webpack_require__(__webpack_require__.s = 0)//modules数组第一项
})([
(function(module, exports, require){
'use strict'
require.r(exports)//定义导出模块为ES模块
// CONCATENATED MODULE: ./chunk.js
var chunk = ('chunk');
const test = 'test-chunk'
function chunkFn() {
return 'chunk'
}
// CONCATENATED MODULE: ./index.js
require.d(exports, 'test', ()=>test)
require.d(exports, 'testFn', ()=>testFn)
const es6_test = chunk
const testFn = chunkFn
})
])
Webpack模块加载逻辑为:
- 创建已安装的模块存储对象
- 判断模块是否已安装,是则直接返回模块结果,避免重复执行模块逻辑
- 创建新模块对象,用于记录模块加载状态及执行结果
- 执行模块逻辑
打包源码 - 动态导入模块
对于动态导入模块,打包产物的额外代码会复杂些
//index.js
export function testFn() {
import('./chunk').then(console.log)
}
//chunk.js
export default 'chunk'
export function chunkFn() {
return 'chunk'
}
打包产物 - 动态导入模块
构建产物入口文件
window["$library"] =
(function(modules) { // webpackBootstrap
//为正在加载的chunk安装JSONP回调
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId)
&& installedChunks[chunkId]) {
//未执行的chunk,加入待执行的chunk数组
resolves.push(installedChunks[chunkId][0]);
}
//将chunk记录为已加载
installedChunks[chunkId] = 0;
}
//将moreModules添加到modules
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
//执行chunk
while(resolves.length) {
resolves.shift()();
}
};
var installedModules = {};
// 存储已加载和加载中的chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
0: 0
};
// 获取jsonp脚本的加载路径
function jsonpScriptSrc(chunkId) {
//由webpack.config中的output.chunkFilename配置
return __webpack_require__.p + "chunk.js"
}
// require函数,与上文相同
function __webpack_require__(moduleId) {/*...*/}
// 当前文件只包含入口chunk
// 使用加载chunk的函数来加载额外的chunk
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// 使用JSONP加载chunk
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 意思是已安装
// 有值表示正在加载
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 往chunk存储中添加Promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 开始加载chunk
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
// nonce是script标签上与安全相关的一个属性
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// 在内存回收前创建错误对象,以便之后回溯
var error = new Error();
onScriptComplete = function (event) {
// 代码执行后清除标签上绑定的事件,以在IE中避免内存泄漏
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);//120秒后超时
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
__webpack_require__.d = function(exports, name, getter) {/*...*/}
__webpack_require__.r = function(exports) {/*...*/}
__webpack_require__.o = function(object, property) {/*...*/}
// 由webpack.config中的output.publicPath设置内容
__webpack_require__.p = "";
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };
var jsonpArray = window["webpackJsonp$library"] = window["webpackJsonp$library"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
//加载入口模块
return __webpack_require__(__webpack_require__.s = 0);
})([
(function(module, exports, require) {
"use strict";
require.r(exports);
require.d(exports, "testFn", ()=>testFn);
function testFn() {
require.e(1).then(require.bind(null, 1)).then(console.log)
}
})
]);
被异步加载的chunk.js产物:
(window["webpackJsonp$library"] = window["webpackJsonp$library"] || []).push([[1],[
/* 0 */,
/* 1 */
(function(module, exports, require) {
"use strict";
require.r(exports);
require.d(exports, "test", ()=>test);
require.d(exports, "chunkFn", ()=>chunkFn);
exports["default"] = ('chunk');
const test = 'test-chunk'
function chunkFn() {
return 'chunk'
}
})
]]);
支持动态导入的Webpack加载器逻辑应当分为两部分来理解:
一、当前模块文件的加载逻辑,在模块加载时立即执行
二、模块运行时的动态导入逻辑
其中第一部分的逻辑为:
- 创建已安装的模块存储对象、已加载和加载中的chunks存储对象
- 查询构建时已指定的存储异步chunk加载结果的全局变量,设置全局变量上的
push()
方法为webpackJsonpCallback()
。若全局变量已有值(即chunk已安装),则立即执行回调函数webpackJsonpCallback()
- 同上节加载当前模块
第二部分的逻辑为:
使用Promise、script标签实现JSONP,以此加载异步chunk。
小结
支持动态导入的Webpack加载器在原有的加载模块逻辑基础上,增加了以JSONP方式加载异步chunk的方法,并借助全局变量来避免chunk被重复加载和执行,支持在chunk加载出错时返回错误信息。