webpack4打包后的代码分析
此文分析时使用的依赖包版本号如下:
"devDependencies": {
"html-webpack-plugin": "4.5.0",
"webpack": "4.44.2",
"webpack-cli": "3.3.12"
}
webpack配置文件如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
devtool: 'none',
mode: 'development',
entry: './src/index.js',
output: {
filename: 'built.js',
path: path.resolve('dist')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
模块代码如下:
// index.js
const title = require('./title.js')
import name, { age } from './name'
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
import(/*webpackChunkName: "btn"*/'./btn.js').then(btn => {
console.log(btn)
})
})
console.log('index.js内容执行了')
console.log(`${name}: ${age}`)
console.log(title)
// title.js
module.exports = 'webpack analyze'
// btn.js
module.exports = '按钮被点击了'
// name.js
export const age = 18
export default 'yhzzy'
html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack</title>
</head>
<body>
<button id="btn">点击加载</button>
</body>
</html>
首先我们先来看看打包出来后的代码样子,我人为的去除了里面一些注释符号:
built.js
(function(modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && 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(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".built.js"
}
// The require function
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;
}
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in 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);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__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, { enumerable: true, get: getter });
}
};
// 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 });
};
// 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
__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 = "";
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
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;
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _name__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./name */ "./src/name.js");
const title = __webpack_require__(/*! ./title.js */ "./src/title.js")
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | btn */ "btn").then(__webpack_require__.t.bind(null, /*! ./btn.js */ "./src/btn.js", 7)).then(btn => {
console.log(btn)
})
})
console.log('index.js内容执行了')
console.log(`${_name__WEBPACK_IMPORTED_MODULE_0__["default"]}: ${_name__WEBPACK_IMPORTED_MODULE_0__["age"]}`)
console.log(title)
}),
"./src/name.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
const age = 18
/* harmony default export */ __webpack_exports__["default"] = ('yhzzy');
}),
"./src/title.js":
(function(module, exports) {
module.exports = 'webpack analyze'
})
});
btn.built.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["btn"],{
"./src/btn.js":
(function(module, exports) {
module.exports = '按钮被点击了'
})
}]);
下面我们就来分析一下这个源码。
代码结构
将打包后的代码折叠,不难发现,其实就是一个IIFE自执行函数,函数的参数则是被打包的具体模块代码的一个健值对形式
(function(modules) {
// 处理逻辑代码
})
({
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _name__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./name */ "./src/name.js");
const title = __webpack_require__(/*! ./title.js */ "./src/title.js")
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | btn */ "btn").then(__webpack_require__.t.bind(null, /*! ./btn.js */ "./src/btn.js", 7)).then(btn => {
console.log(btn)
})
})
console.log('index.js内容执行了')
console.log(`${_name__WEBPACK_IMPORTED_MODULE_0__["default"]}: ${_name__WEBPACK_IMPORTED_MODULE_0__["age"]}`)
console.log(title)
}),
"./src/name.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
const age = 18
/* harmony default export */ __webpack_exports__["default"] = ('yhzzy');
}),
"./src/title.js":
(function(module, exports) {
module.exports = 'webpack analyze'
})
})
其中每一个模块的值也是一个IIFE自执行函数,这个函数会接收三个参数
- 第一个参数 module,为缓存的模块,用来操作CommonJS规范代码的module.exports导出
- 第二个参数 exports或__webpack_exports__,为缓存的module的exports属性,为exports时用于CommonJS规范代码exports.xxx = yyy导出,而为__webpack_exports__时则是用于esModule规范的代码导出
- 第三个参数 webpack_require,是用来导入模块依赖的一个函数
处理逻辑代码分析
1.首先会定义一个对象用于缓存被加载的模块,将来在二次加载模块时,如果缓存中有则直接在缓存中取出,还会定一个用来标记模块是否加载的installedChunks
var installedModules = {}
// undefined没有加载,null预加载,Promise正在加载中,0已经加载
var installedChunks = {
"main": 0
}
2.逻辑代码的核心,用来加载所有的依赖模块,接收一个模块Id参数,用来确定加载的模块
function __webpack_require__(moduleId) {
// 检查该模块在缓存中是否存在,如果存在直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 缓存中不存在则新建一个,并保存在缓存中
var module = installedModules[moduleId] = {
i: moduleId, // 模块Id
l: false, // 模块是否已被加载
exports: {} // 模块的导出内容
}
// 然后将新建的module的exports属性带入到全局参数modules中对应模块Id中执行代码,传入的三个参数就是上文中介绍的三个参数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
// 设置该模块为已加载
module.l = true
// 返回模块的导出
return module.exports
}
3.我们先看源码执行时最后的返回,根据webpack配置的入口文件,对其进行加载
return __webpack_require__(__webpack_require__.s = "./src/index.js")
通过上文的分析,会新建一个i为"./src/index.js"的module保存到缓存模块中,并执行全局参数中"./src/index.js"内的代码
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _name__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./name */ "./src/name.js");
const title = __webpack_require__(/*! ./title.js */ "./src/title.js")
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | btn */ "btn").then(__webpack_require__.t.bind(null, /*! ./btn.js */ "./src/btn.js", 7)).then(btn => {
console.log(btn)
})
})
console.log('index.js内容执行了')
console.log(`${_name__WEBPACK_IMPORTED_MODULE_0__["default"]}: ${_name__WEBPACK_IMPORTED_MODULE_0__["age"]}`)
console.log(title)
})
3.1 代码中首先执行了__webpack_require__.r(webpack_exports),webpack_require.r的作用是将一个exports标记为esModule模式的导出,因为源代码里使用了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 });
};
3.2 导入依赖模块"./src/name.js"
var _name__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./name */ "./src/name.js");
再次执行__webpack_require__,将"./src/name.js"存入缓存中并执行其中代码,因为此模块也是esModule规范,所以首先也会通过__webpack_require__.r来标记为esModule
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
const age = 18
/* harmony default export */ __webpack_exports__["default"] = ('yhzzy');
})
接下来执行__webpack_require__.d方法来向__webpack_exports__对象的身上添加指定的属性,同时给该属性提供一个getter用来获取此属性
__webpack_require__.d(__webpack_exports__, "age", function() { return age; });
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
其中用到的__webpack_require__.o方法用来判断对象的身上是否存在指定的属性
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
最后将age定义,以便将来获取,然后将默认导出挂载到__webpack_exports__
const age = 18
__webpack_exports__["default"] = ('yhzzy')
执行完上面的步骤后,"./src/name.js"中的__webpack_exports__被返回并将"./src/index.js"中的_name__WEBPACK_IMPORTED_MODULE_0__指向它
3.3 接下来回到"./src/index.js"的代码中,继续执行对依赖模块"./src/title.js"的导入,并执行"./src/title.js"中的代码
const title = __webpack_require__(/*! ./title.js */ "./src/title.js")
(function(module, exports) {
module.exports = 'webpack analyze'
})
__webpack_require__的具体执行不再赘述,最后会将"./src/title.js"模块的exports设置为’webpack analyze’,并返回,所以title也等于’webpack analyze’
这一步执行完了后,便会执行几个打印事件输出数据
console.log('index.js内容执行了') // index.js内容执行了
// 上文已经确定了_name__WEBPACK_IMPORTED_MODULE_0__的值,输出yhzzy: 18
console.log(`${_name__WEBPACK_IMPORTED_MODULE_0__["default"]}: ${_name__WEBPACK_IMPORTED_MODULE_0__["age"]}`)
console.log(title) // webpack analyze
3.4 接下来注册按钮及按钮的点击事件
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | btn */ "btn").then(__webpack_require__.t.bind(null, /*! ./btn.js */ "./src/btn.js", 7)).then(btn => {
console.log(btn)
})
})
当点击按钮时,会执行__webpack_require__.e动态导入"./src/btn.js"模块,最后返回一个Promise
先看几个其中用到的函数
// 配置中的publicPath,默认为空
__webpack_require__.p = "";
// 组装script标签的src值
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".built.js"
}
再来看__webpack_require__.e
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// 从最开始定义的installedChunks中拿chunkId(此时为btn)的的值,为0则直接返回,第一次加载结果为undefined
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
// 判断installedChunkData的值是否为Promise正在加载中
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 创建一个Promise,并在其内部将installedChunks[chunkId](installedChunks[‘btn’])和installedChunkData的值设置为一个包含resolve, reject的数组
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 把installedChunkData[2]的值设置为promise本身,并保存到promises数组*将来调用
promises.push(installedChunkData[2] = promise);
// 组装script标签,并添加到index.html中调用
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in 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);
script.onerror = script.onload = onScriptComplete;
// 将script添加在html并执行,此例中执行了btn.built.js中的代码
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
btn.built.js执行
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["btn"],{
"./src/btn.js":
(function(module, exports) {
module.exports = '按钮被点击了'
})
}]);
其中window[“webpackJsonp”]方法在打包的入口文件中注册了
function webpackJsonpCallback(data) {
// webpackJsonpCallback逻辑
};
// 将webpackJsonp挂载到window全局对象上,并将值设置为[]
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 将数组原生push方法挂载到oldJsonpFunction
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 修改window["webpackJsonp"]的push方法为webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
// 拷贝一份jsonpArray
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// 将数组原生push方法赋给parentJsonpFunction
var parentJsonpFunction = oldJsonpFunction;
所以btn.built.js内的代码就相当于是
webpackJsonpCallback([["btn"], {
"./src/btn.js":
(function(module, exports) {
module.exports = '按钮被点击了'
})
}])
function webpackJsonpCallback(data) {
var chunkIds = data[0]; // ["btn"]
// {
// "./src/btn.js":
// (function(module, exports) {
// module.exports = '按钮被点击了'
// })
// }
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i]; // btn
// 判断installedChunks对象中是否存在btn,上文__webpack_require__.e执行时我们已经将btn添加了进去
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// 将btn的resolve方法添加到resolves中
resolves.push(installedChunks[chunkId][0]);
}
// 设置installedChunks中btn的状态为0已加载
installedChunks[chunkId] = 0;
}
// 获取"./src/btn.js"内的代码
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
//将"./src/btn.js"模块添加到全局参数modules中
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
// 执行resolves中的resolve方法,将上文__webpack_require__.e内的Promise变为成功状态
while(resolves.length) {
resolves.shift()();
}
};
回到__webpack_require__.e的最后一步执行return Promise.all(promises),此时由于已经在webpackJsonpCallback中resolve了,会返回一个Promise继续执行按钮点击中的then里面的函数,接下来我们回到按钮点击方法中,在then中会执行__webpack_require__.t
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | btn */ "btn").then(__webpack_require__.t.bind(null, /*! ./btn.js */ "./src/btn.js", 7)).then(btn => {
console.log(btn)
})
})
// 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) {
// 用传进来的mode数字与预设数字进行&预算,此例中7转为2进制为0111
// 1的2进制为0001,满足条件,__webpack_require__("./src/btn.js")执行,返回值为“按钮被点击了”,此时value=“按钮被点击了”
if(mode & 1) value = __webpack_require__(value);
// 8的2进制为1000,不满足条件
if(mode & 8) return value;
// 4的2进制为0100,满足条件,但是value的类型为string,继续向下执行
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
// 创建一个空对象null,并标记为esModule规范
var ns = Object.create(null);
__webpack_require__.r(ns);
// 将ns的default属性设置为value
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
// 2的2进制为0010满足,类型条件不满足
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
// {default: "按钮被点击了", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
return ns;
};
之后如果点击按钮便会输出
{default: "按钮被点击了", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
其余挂载到__webpack_require__上的方法介绍
// 保存modules数组
__webpack_require__.m = modules;
// 保存已安装的模块
__webpack_require__.c = installedModules;
// 用于设置具体的 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;
};