Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
打包后文件分析
webpack-code
├─ dist
│ ├─ bundle.js
│ └─ index.html
├─ src
│ ├─ index.html
│ └─ index.js
├─ package.json
├─ webpack.config.js
├─ README.md
└─ yarn.lock
// index.js
console.log('index.js 内容');
module.exports = "入口文件导出内容";
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist")
},
mode: "development",
devtool: "none",
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html"
})
]
}
// dist/bundle.js 删除注释
(function (modules) {
// modules传入的模块定义:{ moduleId: moduleFunc(module, exports) {} }
// 模块加载缓存
var installedModules = {};
// webpack 自定义的 require 函数,用来加载模块,最终导出加载的模块内容
function __webpack_require__(moduleId) {
// 检查缓存中是否存在需要加载的模块
// 存在就返回exports,exports 既是模块内容
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 新建一个模块,并存入缓存
var module = (installedModules[moduleId] = {
i: moduleId, // 模块ID
l: false, // 是否已加载过
exports: {}, // exports 对象,初始化为一个空对象
});
// 加载模块对应的函数 moduleFunc(module, exports) {}
modules[moduleId].call(
module.exports, // 初始化this为一个空对象,这就是模块具有自身作用域的实现
module, // 需要加载的模块
module.exports, // 模块的exports
__webpack_require__ // webpack的require方法,用来替换CommonJS的require
);
// 标注该模块已经加载
module.l = true;
// 返回模块的导出
return module.exports;
}
// 暴露模块对象 (__webpack_modules__)
__webpack_require__.m = modules;
// 暴露加载缓存
__webpack_require__.c = installedModules;
// 为 exports 定义 getter 函数,并添加一个属性
// 当模块中使用ESModule导出成员时,可以通过这个方法将成员绑定到exports上
// getter 就是返回这个成员的方法 () => name
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 给exports定义__exModule,用来标识是一个ESModule
// 主要解决ESModule和CommonJS混合使用的问题
__webpack_require__.r = function (exports) {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
// Symbol 如果存在即表示是ESModule,添加 Module 标识
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
// 添加__esModule标识
// 来确认这是一个标准的ESModule
Object.defineProperty(exports, "__esModule", { value: true });
};
// 创建一个虚假的命名空间对象
// 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 函数 CommonJS/AMD/CMD等
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule
? function getDefault() {
// 如果是ESModule的话就返回默认导出
return module["default"];
}
// 不是的话直接返回这个module
: 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打包的公共路径
__webpack_require__.p = "";
// 加载模块并返回模块内容
return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
"./src/index.js": function (module, exports) {
console.log("index.js 内容");
module.exports = "入口文件导出内容";
},
});
从打包结果可以看出,webpack将模块打包成一个立即执行(IIFE)函数,入参是一个键值对对象,可以认为是模块定义,键是模块的相对路径,值是一个函数,将来用于加载模块内容,函数体就是模块里面的代码内容。
CommonJS模块打包
以上是单文件打包,__webpack_require__
上面挂载的属性方法的作用没有体现出来,现在我们在index中引入一个模块:
// log.js
module.exports = (log) => console.log(log)
// index.js
let log = require('./log');
console.log('index.js 内容');
module.exports = "入口文件导出内容";
log('这是index引用的log模块。')
依赖的模块中是以CommonJS的方式导出一个方法,看一下打包结果:
// dist/bundle.js
// 上面函数体没有任何变化
...
// CommonJS模块的导入使用 __webpack_require__
({
"./src/index.js": function (module, exports, __webpack_require__) {
let log = __webpack_require__(/*! ./log */ "./src/log.js");
console.log("index.js 内容");
module.exports = "入口文件导出内容";
log("这是index引用的log模块。");
},
"./src/log.js": function (module, exports) {
// 导出也改成了module自己的exports
module.exports = (log) => console.log(log);
},
})
由上可以看出,webpack使用CommonJS构建代码,对于CommonJS引用CommonJS,编译的代码并没有什么特别大的改变,那么我们把引用的代码改成ESModule看看:
// log.js
export const name = 'jack'
const age = 14
export default {
say: () => {
console.log(`${name} is ${age} years old.`)
}
}
// index.js
const log = require('./log')
console.log(log.name)
log.default.say()
// dist/bundle.js
// 上面函数体没有任何变化
...
({
"./src/index.js": function (module, exports, __webpack_require__) {
// 对于CommonJS的require引用依然只是替换为 __webpack_require__
const log = __webpack_require__(/*! ./log */ "./src/log.js");
console.log(log.name);
log.default.say();
},
"./src/log.js":
/*! exports provided: name, default */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// 这是一个ESModule,往module的exports对象上添加一个__esModule属性进行标注
__webpack_require__.r(__webpack_exports__);
// 这里就是通过 r 方法将ESModule导出的成员绑定至module的exports上
// 将返回这个成员作为该属性的getter方法
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"name",
function () {
return name;
}
);
const name = "jack";
const age = 14;
// 为module的exports添加default属性
/* harmony default export */ __webpack_exports__["default"] = {
say: () => {
console.log(`${name} is ${age} years old.`);
},
};
},
});
可以看出如果我们的引用模块中使用的全部是ESModule,__webpacl_require__
上面挂载的 r 方法和 d 方法就派上用场了。
ESModule模块打包
下面我们在主入口文件中使用ESModule的方式引入其它模块,引入的模块分别使用CommonJS和ESModule来导出成员,来看看打包的结果:
1. 依赖模块使用CommonJS导出
// index.js
import log from './log'
console.log(log.name)
log.say()
// log.js
const name = 'jack'
const age = 15
module.exports = {
name,
say() {
console.log(`${name} is ${age} years old.`)
}
}
log.js中使用CommonJS导出name属性和say方法,index中通过ESModule的方式导入,我们看一下打包结果:
// dist/bundle.js
// 上面函数体无变化
...
({
"./src/index.js":
/*! no exports provided */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// 首先标注这个模块是一个标准的ESModule
// Module {__esModule: true, Symbol(Symbol.toStringTag): 'Module'}
// Symbol(Symbol.toStringTag):'Module'
// __esModule:true
// >__proto__:Object
__webpack_require__.r(__webpack_exports__);
// import 改为 __webpack_require__
// 声明一个变量用来承接加载的log内容,命名前缀使用模块名,数字表示加载的第几个模块
// {name: 'jack', say: ƒ}
// name:'jack'
// > say:ƒ say() {\n console.log(`${name} is ${age} years old.`);\n }
// > __proto__:Object
/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./log */ "./src/log.js");
// 调用挂载的 n 方法判断是否是ESModule,是的话就加载默认导出,不是的话就加载整个module
// 加载的内容通过 o 函数挂载到一个 a 属性上
// ƒ getModuleExports() {\n return module;\n }
// > a (get):ƒ getModuleExports() {\n return module;\n }
// > :Object
// name:'jack'
// say:ƒ say() {\n console.log(`${name} is ${age} years old.`);\n }
// __proto__:Object
// arguments:null
// caller:null
// length:0
// name:'getModuleExports'
// > prototype:{constructor: ƒ}
// [[FunctionLocation]]:@ /Users/xueyong/lagou-edu/webpack-code/dist/bundle.js:87
// > [[Scopes]]:Scopes[3]
// > __proto__:function () { [native code] }
/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0___default =
/*#__PURE__*/ __webpack_require__.n(_log__WEBPACK_IMPORTED_MODULE_0__);
// 调用a就会调用它的get方法,get方法就是 n 函数返回的那个getter,有一个 getModuleExports 函数
// 调用这个函数就是返回模块中的内容 module
console.log(_log__WEBPACK_IMPORTED_MODULE_0___default.a.name);
_log__WEBPACK_IMPORTED_MODULE_0___default.a.say();
},
"./src/log.js":
/*! no static exports found */
function (module, exports) {
const name = "jack";
const age = 15;
module.exports = {
name,
say() {
console.log(`${name} is ${age} years old.`);
},
};
},
});
由上可见,webpack对CommonJS的导出不做修改,对于ESModule的导入修改较大,首先会使用 r 函数标记这个模块是ESModule,其次导入会修改成 __webpack_require__
,导入其它模块时会使用 n 函数标示默认导出并添加 a 属性绑定导出内容。
2. 依赖模块使用ESModule导出
// log.js
export const name = 'jack'
const age = 14
export default {
say: () => {
console.log(`${name} is ${age} years old.`)
}
}
// index.js
import log, { name } from './log'
console.log(name)
log.say()
// dist/bundle
// 上面函数体无变化
...
({
"./src/index.js":
/*! no exports provided */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./log */ "./src/log.js");
console.log(_log__WEBPACK_IMPORTED_MODULE_0__["name"]);
_log__WEBPACK_IMPORTED_MODULE_0__["default"].say();
},
"./src/log.js":
/*! exports provided: name, default */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"name",
function () {
return name;
}
);
const name = "jack";
const age = 14;
/* harmony default export */ __webpack_exports__["default"] = {
say: () => {
console.log(`${name} is ${age} years old.`);
},
};
},
});
当引用模块是ESModule时,导入会变得简单一点,因为引用模块的导出会被webpack修改,所有属性都会绑定到exports上,引用时直接取值即可。
手写功能函数
我们可以自己实现一下函数体中的功能函数,它在每次打包时几乎都是不变得:
// myBubdle.js
/**
* webpack打包函数
* @param { 模块定义 } modules
*
* installedModules -> 模块加载缓存
* __webpack_require__ -> webpack加载模块的方法
* m -> 暴露modules
* c -> 暴露installedModules
* o -> 判断对象上是否存在自身属性
* d -> 给对象加上某个属性
* r -> 标注ESModule模块
* n -> 获取module导出
* p -> 公共路径
*/
function (modules) {
// 01 定义一个缓存对象,用来存放加载过得模块
var installedModules = {};
// 02 webpack自己的加载模块方法
function __webpack_require__ (moduleId) {
// 判断缓存中是否存在该模块,存在的话就返回其导出
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 声明一个模块
var module = {
i: moduleId,
l: false,
exports: {}
}
// 调用函数,加载模块内容
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标注加载完毕
module.l = true;
// 返回模块内容
return module.exports;
}
// 03 暴露modules
__webpack_require__.m = modules;
// 04 暴露加载缓存
__webpack_require__.c = installedModules;
// 05 提供函数判断一个对象上是否存在某个属性
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}
// 06 给对象添加某个属性,用于解析ESModule时绑定成员属性至exports上
// 并给这个属性添加get方法,用来获取导出对象exports
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
}
// 07 标注一个模块是ESModule,以便加载时将其转换为CommonJS
// 首先判断当前模块是否是ESModule,是的话就使用Symbol来绑定一个唯一属性,标注为 Module
// 不问当前是否是ESModule,最终打包ESModule时都会加上 { __esModule: true }
__webpack_require__.r = function(exports) {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
}
// 08 获取模块的导出
// 根据module上是否被标记__esModule来判断导出对象
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() {
return module["default"];
}
:
function getModuleExports() {
return module;
}
// 给 exports 添加a属性用来绑定返回的模块内容,a属性提供get方法用来访问模块中的成员属性
__webpack_require__.d(getter, "a", getter);
return getter;
}
// 09 公共路径
__webpack_require__.p = "";
// 10 返回加载的模块内容
return __webpack_require__((__webpack_require__.s = "./src/index.js"));
}
懒加载实现流程
1. 懒加载打包文件分析
我在通过页面上的一个按钮来加载引用的模块:
// index.html
<button id="btn">点击加载</button>
// index.js
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
import(/* webpackChunkName: "log" */'./log').then(function(module) {
console.log(module)
module.default('log 加载完毕。')
})
})
// log.js
module.exports = (log) => console.log(log)
点击按钮后可以看到加载的模块内容:
▼ Module {__esModule: true, Symbol(Symbol.toStringTag): "Module", default: ƒ}
▼ default: (log) => console.log(log)
arguments: (...)
caller: (...)
length: 1
name: ""
▶ [[FunctionLocation]]: log.bundle.js:10
▶ [[Prototype]]: ƒ ()
▶ [[Scopes]]: Scopes[1]
Symbol(Symbol.toStringTag): "Module"
__esModule: true
我们来看一下打包后的文件:
// dist/bundle.js
// 已经删除前面相同的功能函数代码,只保留了新生成的函数
(function (modules) {
// webpackBootstrap
// 块加载的JSONP回调函数
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()();
}
}
// 一个对象,用来存储加载过得或者正加载中的块
// undefined = 块未加载, null = 块预加载或预请求
// Promise = 块加载中, 0 = 块加载完毕
var installedChunks = {
main: 0,
};
// 拼接 script src
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".bundle.js";
}
// bundle.js只包含加载入口文件模块
// 下面是额外依赖模块的加载函数
// 使用promise的形式异步地创建script标签,将异步加载的模块嵌入应用中
// 通过JSONP加载模块内容
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// javascript 的 JSONP 块加载
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
// 0 表示加载完毕了.
// 如果是一个Promise,表示正在加载过程中,promises中推入加载块.
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 在块缓存中设置 Promise
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise));
// 开始加载块
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);
// 在堆栈展开之前创建错误以便稍后获得有用的堆栈跟踪
// 读取加载块时的错误回调
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);
};
// 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;
};
// 异步加载的错误回调
__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, exports, __webpack_require__) {
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
__webpack_require__
.e(/*! import() | log */ "log")
.then(__webpack_require__.t.bind(null, /*! ./log */ "./src/log.js", 7))
.then(function (module) {
console.log(module);
module.default("log 加载完毕。");
});
});
btn.click()
},
});
// dist/log.bundle.js
/**
* window上挂载一个全局对象 webpackJsonp
* 将模块名和模块加载函数push到这个全局对象中
* push方法已经在加载bundle.js被重写,也就是JSONP回调
*/
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["log"],
{
"./src/log.js": function (module, exports) {
module.exports = (log) => console.log(log);
},
},
]);
由上可见懒加载主要通过JSONP结合Promise来实现模块的动态加载。
2. t 方法分析及实现
// 创建一个命名空间对象,主要是为了返回模块内容
// mode & 1: value是一个模块id加载这个模块
// mode & 2: value是一个对象,合并对象的所有属性至ns中
// mode & 4: 此时ns已经被标注__esmodule,并且属性合并完毕,可以返回
// mode & 8|1: 标准的CommonJS模块,直接返回模块内容
__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;
// 当它不满足CommonJS和ESModule的时候,声明一个对象用来合并成员属性
var ns = Object.create(null);
// 标注其为ESModule
__webpack_require__.r(ns);
// 绑定默认导出,导出内容为value
Object.defineProperty(ns, "default", { enumerable: true, value: value });
// 当value是一个对象,且不是string类型时将其属性合并至ns
if (mode & 2 && typeof value != "string")
for (var key in value)
__webpack_require__.d(
ns,
key,
function (key) {
return value[key];
}.bind(null, key)
);
// 返回这个ns对象
return ns;
};
按位与运算说明:
操作数被转换为32位整数,并由一系列位(0和1)表示。 超过32位的数字将丢弃其最高有效位。 例如,以下大于32位的整数将被转换为32位整数:
Before: 11100110111110100000000000000110000000000001
After: 10100000000000000110000000000001
第一个操作数中的每个位都与第二个操作数中的相应位配对:第一位到第一位,第二位到第二位,依此类推。
将运算符应用于每对位,然后按位构造结果。
与运算的真值表:
a | b | a AND b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
只有当两位都为真时才为真,其余全部为假。
代码拆解于实现:
// myBundle.js
// 09 创建一个对象用来合并模块的成员属性,并返回
__webpack_require__.t = function(value, mode) {
if (mode & 1) {
// 当 mode & 1 为真时,value是moduleId,此时就加载模块内容
value = __webpack_require__(value);
}
if (mode & 8) {
// 当 mode & 8|1 为真时,说明表示为require,是CommonJS,直接返回加载的value
return value;
}
if (mode & 4 && typeof value === "object" && value && value.__esModule) {
// 当 mode & 4 为真时,且value是一个对象,value值存在,并且具备__esModule为true的条件
// 说明ns已经被标注为ESModule,且成员属性合并完毕,直接返回value
return value;
}
// 以上一切都不满足时说明其不是标准的CommonJS,且未被标注为ESModule,未完成对象合并
// 此时就声明一个空对象用来合并模块的成员
const ns = Object.create(null);
// 标注其为ESModule
__webpack_require__.r(ns);
// 设置默认导出,值为value
Object.defineProperty(ns, "default", { enumerable: true, value: value });
// 如果 mode & 2 为真时,说明value是一个对象,那么就进行对象合并
if (mode & 2 && typeof value !== "string") {
for (var key in value) {
__webpack_require__.d(
ns,
key,
// 提供一个get方法获取属性值,重置this为null
function(key) {
return value[key];
}.bind(null, key)
)
}
}
// 返回ns
return ns
}
3. 单文件懒加载源码解析
全局安装 http-server,启动一个本地服务器:
// http://127.0.0.1:8080/
Index of /
(drwxr-xr-x) .vscode/
(drwxr-xr-x) dist/
(drwxr-xr-x) node_modules/
(drwxr-xr-x) src/
(-rw-r--r--) 213B package.json
(-rw-r--r--) 229B README.md
(-rw-r--r--) 350B webpack.config.js
(-rw-r--r--) 129.6k yarn.lock
Node.js v15.3.0/ http-server server running @ 127.0.0.1:8080
添加调试配置:
// launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
bundle.js 打上断点,点击浏览器中的dist目录就可以进入调试,一步步查看代码执行结果。
// log.bundle.js
// 此时的push方法已经被改写了,是JSONP的加载回调
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["log"],
{
"./src/log.js": function (module, exports) {
module.exports = (log) => console.log(log);
},
},
]);
// bundle.js
// 省略了其它代码,只保留了懒加载的核心代码
(function (modules) {
// webpackBootstrap
// 加载模块的JSONP回调,在动态加载依赖模块时会调用window["webpackJsonp"]的push方法
// 该方法在加载主文件的过程中已经被改写成webpackJsonpCallback方法
function webpackJsonpCallback(data) {
// 第一项是依赖的模块ID结合
var chunkIds = data[0];
// 第二项是一个对象,键为模块ID,值是模块的加载函数
var moreModules = data[1];
// resolves用来存放依赖模块的promise resolve函数,就是通过 __webapck_require__e 函数加载模块
var moduleId,
chunkId,
i = 0,
resolves = [];
// 遍历模块ids
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
// 判断该模块正在加载中且是一个promise对象
if (
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
// 缓存它的resolve方法
resolves.push(installedChunks[chunkId][0]);
}
// 修改其加载状态为0,也就是加载完毕
installedChunks[chunkId] = 0;
}
// 遍历所有模块加载函数,和当前主入口的模块定义 modules 进行合并
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// parentJsonpFunction作用:使异步加载的模块在多个不同的bundle内同步
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
}
// 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 + ".bundle.js";
}
// 该bundle.js文件只加载主入口文件
// 其它动态加载的模块通过这个方法加载
__webpack_require__.e = function requireEnsure(chunkId) {
// 加载模块的promise集合
var promises = [];
// 从加载缓存中取出该模块
var installedChunkData = installedChunks[chunkId];
// 为0表示已经加载完毕,此处为undefined
if (installedChunkData !== 0) {
// 如果是一个promise表示正在加载中,将这个加载模块的promise推送至promises集合中
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 声明一个promise缓存resolve和reject函数
var promise = new Promise(function (resolve, reject) {
// 将resolve绑定至installedChunkData
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 将这个promise压入promises集合中,并将promise绑定至installedChunkData
promises.push((installedChunkData[2] = promise));
// 使用JSONP完成模块加载
var script = document.createElement("script");
var onScriptComplete;
script.charset = "utf-8";
script.timeout = 120;
// 判断是否启用了内联脚本的安全策略,如果启用的话就设置nonce为设置的值
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
// 设置脚本src
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);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
// 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;
};
// on error function for async loading
__webpack_require__.oe = function (err) {
console.error(err);
throw err;
};
// 定义一个 jsonp加载数组,初始加载时 window["webpackJsonp"] 空的,就默认设置为 []
var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
// 缓存jsonpArray的原生push方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写jsonpArray方法为jsonp的回调
jsonpArray.push = webpackJsonpCallback;
// 浅复制jsonArray
jsonpArray = jsonpArray.slice();
// 当jsonpArray不为空时就加载jsonpArray里面的模块
for (var i = 0; i < jsonpArray.length; i++)
webpackJsonpCallback(jsonpArray[i]);
// 将jsonpArray的原生push方法赋值给parentJsonpFunction
var parentJsonpFunction = oldJsonpFunction;
// Load entry module and return exports
return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
"./src/index.js": function (module, exports, __webpack_require__) {
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
__webpack_require__
.e(/*! import() | log */ "log")
.then(__webpack_require__.t.bind(null, /*! ./log */ "./src/log.js", 7))
.then(function (module) {
console.log(module);
module.default("log 加载完毕。");
});
});
},
});