webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。
无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。
准备工作:
1. 首先初始化一个package.json文件出来,然后我们分析是用的 webpack4和webpack-cli3版本,如下:
{
"name": "webpack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"webpack": "4",
"webpack-cli": "3"
},
"dependencies": {
"html-webpack-plugin": "4"
}
}
2.然后准备一下webpack配置文件新建 webpack.config.js文件,内容如下:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
devtool: "none",
mode: "development",
output: {
filename: "build.js",
path: path.resolve("dist"),
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
3. 最后准备一下webpack.config.js中用到html和js文件,如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">加载</button>
</body>
</html>
index.js
const { time } = require("./utils");
import page, { p1 } from "./page1";
const test = "加载了";
time("home");
console.log(page,p1,test);
const btn = document.getElementById('btn')
btn.addEventListener('click',function(){
import(/* webpackChunkName: "async" */'./async').then((snyc)=>{
console.log(snyc)
})
},false)
page1.js
import utils from './utils'
const page = "这是page";
export const p1 = "1";
utils.time('cha')
export default page;
utils.js
function time(type) {
console.log(type, new Date());
}
module.exports = {
time,
};
snyc.js
const value = '异步文件'
function add(){
consople.log('ssss')
}
module.exports = {
value,
add
};
// export {
// value
// }
文件夹结构:
以上代码逻辑如下:
index.js
- 通过
require关键字加载utils模块commonJS规范导出的内容 - 通过
import关键字加载page1模块esModule规范导出的内容 - 通过btn按钮点击使用
import()懒加载,async模块commonJS规范导出的内容
page1.js
- 通过
import关键字加载utils模块commonJS规范导出的内容 - esModule规范导出内容
剩下的utils.js和async.js都是commonJS规范做的导出.
知道这几个文件都做了哪些事之后,我们运行npx webpack 来把他们打包看看产出之后的一个结果,然后我们针对它产出的结果build.js和async.build.js 来进行分析。
产出的index.html
产出的index.html没有什么可看的,就多了一个把build.js加载到html里
产出的build.js
(function(modules){
// webpackBootstrap
// 14 webpackJsonpCallback 的实现 实现:合并懒加载模块 并把模块promise状态改为成功态
function webpackJsonpCallback(data){
// 01 获取需要被加载的模块ids webpackChunkName
var chunkIds = data[0];
// 02 懒加载模块的依赖关系对象
var moreModules = data[1];
// 03 循环判断chunkIds里对应的模块内容是否已经完成了加载
var moduleId,
chunkId,
i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i]; //当前的模块的webpackChunkName
if (
//先从已下载的chunks中installedChunks 看下有没有有这个属性,看看它是不是要被加载的
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
installedChunks[chunkId] //再看看它 是不是0 0代表已经加载过了
) {
resolves.push(installedChunks[chunkId][0]); // 把installedChunks[chunkId] 存储的数组 [resolve,reject,promise] 第一个决议resolve函数 存起来
}
installedChunks[chunkId] = 0; // 到这里 当前模块状态 加载完成 改为成功态
}
// 懒加载过来的模块合并到modules 上
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// 如果chunkId 这个模块也被其他模块动态加载了 它会被用到
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
// 把resolve里面存的 进行决议 这会 __webpack_require__.e 的promise.all 对应的promise决议完成 paomise.all全部完成之后
// t函数执行 取出对应模块 并进行包装 t函数被当作then的函数传入
// 也就是说它执行完成之后的返回值 会继续被包装成promise then下去 这样我们import().then 就可以接到它返回的模块内容了
resolves.shift()();
}
}
// 15 定义installedChunks用于标识某个chunkid对应的chunk是否完成了加载
var installedChunks = {
// 0已加载 promise 正在加载 undefined未加载 null块已预加载/预取
main: 0, //主入口 因为没有用webpackChunkName 名称定义 所以默认为main
};
// 01 定义缓存对象
var installedModules = {};
// 02 定义内部自己的 __webpack_require__ import 和 require都会被转成它 用来导入模块内容
function __webpack_require__(moduleId){
// 判断缓存中是否有加载的模块 有的话直接返回它的exports
if(installedModules[moduleId]){
return installedModules[moduleId].exports
}
// 如果缓存不存在 定义对象并写入缓存
var module = installedModules[moduleId] = {
i:moduleId, // 就是拼接地址
l:false, //l是否加载了
exports:{} // exports 是模块最后导出内容
}
//调用moduleId对应的函数执行 加载内容到module.exports 中
modules[moduleId].call(module.exports,module,module.exports,__webpack_require__)
//修改当前模块为已加载
module.l = true
//把拿回来的内容返回出去
return module.exports
}
// 03 定义m属性用于保存modules
__webpack_require__.m = modules;
// 04 定义c属性用于保存cache
__webpack_require__.c = installedModules; // 缓存模块
// 05 定义o方法 用于判断对象身上是否存在指定的属性
__webpack_require__.o = function(object,property){
return Object.prototype.hasOwnProperty(object,property)
}
// 06 定义d方法用于在对象的身上添加指定的属性
__webpack_require__.d = function(exports,name,getter){
if(!__webpack_require__.o(name)){ // 如果exports没有name属性 防止重复添加相同得值
// 给他添加上这个属性 并且把它改为可枚举得 并且只传入get访问器函数时 只能获取该属性 其他模块导入时不可以修改 因为没有set
Object.defineProperty(exports,name,{enumerable:true,get:getter}) // 访问器由模块传入
}
}
// 07 标记为是一个__esModule 并且再支持es6时添加 获取类型时时一个module commonjs规范走不到这里
__webpack_require__.r = function(exports){
if(typeof Symbol !== 'undefined' && Symbol.toStringTag){ // 支持es6时
// 通过object.prototype.toString.call(exports) 可以得到一个Module 标记一下exports是一个模块
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 直接添加__esModule属性为true 标记
Object.defineProperty(exports, '__esModule', { value: true });
}
// 08 定义n方法用于设置具体的getter 目前看在esmodule模块中 使用import语法 来导入commonjs模块时 会用到它
__webpack_require__.n = function(module){
// 最后通过xxx.a 来获取对应的默认导出的内容
var getter = module && module.__esModule //如果是esmodule返回default commonjs规范下直接返回 抹平两种导入得差别
? function getDefault() {
return module["default"];
}
: function getModuleExports() {
return module;
};
__webpack_require__.d(getter, "a", getter);
return getter;
}
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".build.js";
}
// 16 实现e方法 使用 jsonp 来懒加载模块 并设置超时时间 返回promise 一旦promsie决议 代表模块加载完成
__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) {
// 有值下面赋值的数组 可能是promise 加载中 后续会给installedChunkData[2] 赋值
promises.push(installedChunkData[2]); // 把这个promise装起来 等加载完成之后执行then 可能这个模块还没加载完毕 点击函数又点击一次
} else {
// setup Promise in chunk cache
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise)); // 把这个promise装起来 installedChunks[chunkId][2] 位置把这个promsie存起来
//创建jsonp加载文件
// 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指定地址 chunkId就是webpackChunkName
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); // reject函数 抛出错误
}
installedChunks[chunkId] = undefined; // 模块标记为未加载
}
};
var timeout = setTimeout(function () {
onScriptComplete({ type: "timeout", target: script });
}, 120000); // 创建一个懒加载模块的超时时间 到时间未加载成功报错
script.onerror = script.onload = onScriptComplete; // 都赋值 onload成功之后 模块状态为0 不会报错
document.head.appendChild(script); //开始加载
}
}
return Promise.all(promises);
};
// mode & 1: value 是一个模块 id,需要它
// mode & 2: 将 value 的所有属性合并到 ns 中
// mode & 4: 已经是 ns 对象时返回值
// mode & 8|1:表现得 require
// 17 定义t方法 用于加载指定value的模块内容,之后对内容进行处理返回
__webpack_require__.t = function(value,mode){
// 01加载value对应的模块内容(value 一般就是模块id)
// 加载之后的内容重新赋值给value变量
if(mode & 1){
value = __webpack_require__(value)
}
if(mode & 8){
return value
}
if((mode & 4) && typeof value === 'object' && value && value.__esModule){
return value
}
// 如果8和4都没有成立 则需要定义ns 来通过default属性返回内容
var ns = Object.create(null)
__webpack_require__.r(ns) // 标记为esm
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
// 下面判断表示如果返回的value是一个对象 那需要给他依次添加getter 到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));
return ns;
}
// 09 定义p属性 用于保存资源访问路径
__webpack_require__.p = ''
// 11 定义变量存放数组
var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []); // 首次执行 赋值空数组
// 12 保存原来的push方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 保存一份原生的push方法 后续可能会用
// 13 重写push方法
jsonpArray.push = webpackJsonpCallback; // 重写jsonpArray.push 相当于也重写了 window["webpackJsonp"].push
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++)
webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
// 10 调用__webpack_require__ 方法执行模块导入与加载操作
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 _page1__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./page1 */ "./src/page1.js");
const { time } = __webpack_require__(/*! ./utils */ "./src/utils.js");
const test = "加载了";
time("home");
console.log(
_page1__WEBPACK_IMPORTED_MODULE_0__["default"],
_page1__WEBPACK_IMPORTED_MODULE_0__["p1"],
test
);
const btn = document.getElementById("btn");
btn.addEventListener(
"click",
function () {
__webpack_require__
.e(/*! import() | async */ "async")
.then(
__webpack_require__.t.bind(null, /*! ./async */ "./src/async.js", 7)
)
.then((snyc) => {
console.log(snyc);
});
},
false
);
},
"./src/page1.js": function (
module,
__webpack_exports__,
__webpack_require__
) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"p1",
function () {
return p1;
}
);
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./utils */ "./src/utils.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0___default =
/*#__PURE__*/ __webpack_require__.n(_utils__WEBPACK_IMPORTED_MODULE_0__);
const page = "这是page";
const p1 = "1";
_utils__WEBPACK_IMPORTED_MODULE_0___default.a.time("cha");
/* harmony default export */ __webpack_exports__["default"] = page;
},
"./src/utils.js": function (module, exports) {
function time(type) {
console.log(type, new Date());
}
module.exports = {
time,
};
},
});
我们能看到这一块代码 是重点,我们把它拆开来看:
1.匿名函数自调,传入模块定义
(function(modules){
})({
"./src/index.js": function (
module,
__webpack_exports__,
__webpack_require__
) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _page1__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./page1 */ "./src/page1.js");
const { time } = __webpack_require__(/*! ./utils */ "./src/utils.js");
const test = "加载了";
time("home");
console.log(
_page1__WEBPACK_IMPORTED_MODULE_0__["default"],
_page1__WEBPACK_IMPORTED_MODULE_0__["p1"],
test
);
const btn = document.getElementById("btn");
btn.addEventListener(
"click",
function () {
__webpack_require__
.e(/*! import() | async */ "async")
.then(
__webpack_require__.t.bind(null, /*! ./async */ "./src/async.js", 7)
)
.then((snyc) => {
console.log(snyc);
});
},
false
);
},
"./src/page1.js": function (
module,
__webpack_exports__,
__webpack_require__
) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"p1",
function () {
return p1;
}
);
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./utils */ "./src/utils.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0___default =
/*#__PURE__*/ __webpack_require__.n(_utils__WEBPACK_IMPORTED_MODULE_0__);
const page = "这是page";
const p1 = "1";
_utils__WEBPACK_IMPORTED_MODULE_0___default.a.time("cha");
/* harmony default export */ __webpack_exports__["default"] = page;
},
"./src/utils.js": function (module, exports) {
function time(type) {
console.log(type, new Date());
}
module.exports = {
time,
};
},
});
这样看,我们抛开匿名函数里webpack自己定义的函数及webpackBootstrap来看匿名函数调用时传递的参数modules,modules就是我们的模块定义,数据结构就是键值对的对象:
- 键就是我们的模块标识 公共资源访问路径+模块路径
- 值是一个函数,函数里的内容就是我们原始模块里的内容,在这里是用的函数作用域做的隔离。
2.然后我们来看匿名函数里定义的内容,1-10
build.js 的匿名函数中 有我标识的01 - 17的注释步骤顺序,我们按这个来看,首先我们先来看01 - 10,11 - 17的是懒加载的内容,我们最后结合另一个chunk文件async.build.js再讲,步骤如下
- 定义模块缓存对象,加载过的模块会被缓存起来
- 定义内部自己的
__webpack_require__原模块里的import 和 require都会被转成它 用来导入模块内容 - 定义
__webpack_require__.m属性用于保存modules - 定义
__webpack_require__.c属性用于保存cache - 定义
__webpack_require__.o方法 用于判断对象身上是否存在指定的属性 - 定义
__webpack_require__.d方法用于在对象的身上添加指定的属性 - 定义
__webpack_require__.r方法 标记当前模块是一个__esModule,并且在支持es6时添加,获取类型时是一个module。注:主要是为了处理混合使用 ES6 module 和 CommonJS 的情况 - 定义
__webpack_require__.n方法用于设置获取cjs还是esm的getter,使用 CommonJSmodule.export = test2导出函数,导入使用 ES6 moduleimport test2 from './test2会结合注释7、8使用 - 定义
__webpack_require__.p属性 用于保存资源访问公共前缀路径 - 调用
__webpack_require__方法执行模块导入与加载操作
到这里我们能看到__webpack_require__(__webpack_require__.s = './src/index.js') 通过它我们开始了从index.js 开始的入口载入.
我们来看 __webpack_require__的实现:
- 判断缓存中是否有加载的模块 有的话直接返回它的
exports - 如果缓存不存在 定义
module对象并写入缓存及module - 调用
moduleId对应的函数执行 加载内容到module.exports 中 - 修改当前模块为已加载
- 把拿回来的内容返回出去
return module.exports
我们看到了上述的第三步,会调用对应函数执行,按入口来讲也就是说会调用./src/index.js对应的函数
./src/index.js 对应的函数:
- 通过
__webpack_require__.r标记为esModule,因为它是esm规范导出的 - 通过
__webpack_require__加载./src/page1.js./src/utils.js
我们看__webpack_require__实现知道,导入会执行对应模块标识的函数,最后对应模块导出的内容都会被挂载到__webpack_require__内部的 module.exports对象上,那也就是说上述操作又会依次触发./src/page1.js ./src/utils.js 对应的函数.
./src/page1.js 对应的函数:
- 通过
__webpack_require__.r来标记当前模块是一个esModule - 通过
__webpack_require__.d来给module.exports添加p1属性,及getter访问器 - 通过
__webpack_require__来加载./src/utils.js模块 (在esModule规范中导入了utils的commonJS规范导出的内容) - 通过
__webpack_require__.n获取默认的导出(触发到上面所说的结合注释7、8,n的主要作用就是抹平两种导入的差异) - 调用导入的utils.time函数和导出默认default内容
./src/utils.js 对应的函数
该函数用的默认的符合webpack的commonJS规范导出,没有做任何处理。
截至到这里不同规范的加载流程我们就都看到了,接下来我们来讲懒加载模块,也就是11-17,如下:
- 定义
jsonpArray = window["webpackJsonp"]数组 用来存储加载过的jsonp模块 - 保存
window["webpackJsonp"]原来的push方法 - 重写jsonpArray.push 相当于也重写了 window["webpackJsonp"].push,重写为
webpackJsonpCallback webpackJsonpCallback实现:合并懒加载模块到modules,并把懒加载模块promise状态改为成功态- 定义installedChunks对象用于标识某个chunkid对应的chunk是否完成了加载(0已加载、数组[resolve,reject,promise]正在加载、undefined未加载、null块已预加载/预取)
- 实现
__webpack_require__.e方法 使用 jsonp 来懒加载模块,并设置超时时间,返回promise,一旦promsie决议,代表模块加载完成。 - 实现
__webpack_require__.t方法,t方法主要是根据模块标识取出对应的模块内容,根据mode对模块进行加工返回。(使用import()导入commonJS模块时,会用它加工)
然后我们看下async.build.js的内容:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["async"],
{
"./src/async.js": function (module, exports) {
const value = "异步文件";
function add() {
consople.log("ssss");
}
module.exports = {
value,
add,
};
},
},
]);
// [
// [ ['async'],{... 模块对象} ]
// ]
看到这里我们也可以想下如果我们自己来实现的话,要怎么实现,如下:
- 首先使用
__webpack_require__.e()下载动态资源,返回promise - 然后下载完之后会添加到
head里,这回会执行下载的async.build.js内容,它里面执行了window["webpackJsonp"].push()这个函数,而这个函数正好被我们重写过webpackJsonpCallback。 - 那我们只需要在
webpackJsonpCallback里面把 它push的这个懒加载模块合并到modules上,并且把这个installedChunks对应的懒加载模块改为下载完成,并且把__webpack_require__.e()返回的promise改为完成态,让他继续往下走就可以了。
然后我们再来看它的官方实现:
__webpack_require__.e
- 先查看该模块 ID 对应缓存的值是否为 0,0 代表已经加载成功了,第一次取值为
undefined。 - 如果不为 0 并且不是
undefined代表已经是加载中的状态。然后将这个加载中的 Promise 推入promises数组。 - 如果不为 0 并且是
undefined就新建一个installedChunkData = installedChunks[chunkId] = [resolve,reject,promise]用于加载需要动态导入的模块,然后把 Promise推入promises数组。 - 生成一个
script标签,URL 使用jsonpScriptSrc(chunkId)生成,即需要动态导入模块的 URL。 - 为这个
script标签设置一个 2 分钟的超时时间,并设置一个onScriptComplete()函数,用于处理超时错误 - 然后添加到页面中
document.head.appendChild(script),开始加载模块。 - 返回
promises数组
继续下一步当 JS 文件下载完成后,会自动执行文件内容。也就是说下载完 async.bundle.js 后,会执行 window["webpackJsonp"].push()。
webpackJsonpCallback
对这个模块 ID 对应的 Promise 执行 resolve(),合并懒加载模块,同时将缓存对象中的值置为 0,表示已经加载完成了。这个函数还是挺好理解的。
然后按模块中btn点击加载完成之后的操作,来看:
btn.addEventListener(
"click",
function () {
__webpack_require__.e(/*! import() | async */ "async").then(
__webpack_require__.t.bind(null, /*! ./async */ "./src/async.js", 7)
).then((snyc) => {
console.log(snyc);
});
},
false
);
执行完__webpack_require__.e之后意味着懒加载模块已经下载合并到modules中了,然后因为async.build.js时commonJS规范导出,所以需要__webpack_require__.t二次加工之后返回,最后这个then也就是我们原始模块中写的then处理了。
到这里基础的模块加载原理就分析完了,如果哪里还有不懂的建议把这个代码下载下来,打下断点调试一下,还是比较简单的。