【webpack 进阶】Webpack 打包后的代码是怎么的?
webpack
是咱们现阶段要把握的重要的打包工具之一,咱们晓得 webpack
会递归的构建依赖关系图,其中蕴含应用程序的每个模块,而后将这些模块打包成一个或者多个 bundle
。
那么 webpack
打包后的代码是怎么的呢?是怎么将各个 bundle
连贯在一起的?模块与模块之间的关系是怎么解决的?动静 import()
的时候又是怎么的呢?
本文让咱们一步步来揭开 webpack
打包后代码的神秘面纱
筹备工作
创立一个文件,并初始化
mkdir learn-webpack-output
cd learn-webpack-output
npm init -y
yarn add webpack webpack-cli -D
根目录中新建一个文件 webpack.config.js
,这个是 webpack
默认的配置文件
const path = require('path');
module.exports = {
mode: 'development', // 能够设置为 production
// 执行的入口文件
entry: './src/index.js',
output: {
// 输入的文件名
filename: 'bundle.js',
// 输入文件都放在 dist
path: path.resolve(__dirname, './dist')
},
// 为了更加不便查看输入
devtool: 'cheap-source-map'
}
而后咱们回到 package.json
文件中,在 npm script
中增加启动 webpack
配置的命令
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"build": "webpack"
}
新建一个 src
文件夹,新增 index.js
文件和 sayHello
文件
// src/index.js
import sayHello from './sayHello';
console.log(sayHello, sayHello('Gopal'));
// src/sayHello.js
function sayHello(name) {return `Hello ${name}`;
}
export default sayHello;
所有筹备结束,执行 yarn build
剖析主流程
看输入文件,这里不放具体的代码,有点占篇幅,能够点击这里查看
其实就是一个 IIFE
莫慌,咱们一点点拆离开看,其实总体的文件就是一个 IIFE
——立刻执行函数。
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
function __webpack_require__(moduleId) {// ... 省略细节}
// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
"./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
函数的入参 modules
是一个对象,对象的 key
就是每个 js
模块的相对路径,value
就是一个函数(咱们上面称之为 模块函数)。IIFE
会先 require
入口模块。即下面就是 ./src/index.js
:
// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");
而后入口模块会在执行时 require
其余模块例如 ./src/sayHello.js"
以下为简化后的代码,从而一直的加载所依赖的模块,造成依赖树,比方如下的 模块函数 中就援用了其余的文件 sayHello.js
{"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
})
}
重要的实现机制——__webpack_require__
这里去 require
其余模块的函数次要是 __webpack_require__
。接下来次要介绍一下 __webpack_require__
这个函数
// 缓存模块应用
var installedModules = {};
// The require function
// 模仿模块的加载,webpack 实现的 require
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)
// 没有则创立并放入缓存中,其中 key 值就是模块 Id, 也就是下面所说的文件门路
var module = installedModules[moduleId] = {
i: moduleId, // Module ID
l: false, // 是否曾经执行
exports: {}};
// Execute the module function
// 执行模块函数,挂载到 module.exports 上。this 指向 module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
// 标记这个 module 曾经被加载
module.l = true;
// Return the exports of the module
// module.exports 通过在执行 module 的时候,作为参数存进去,而后会保留 module 中裸露给外界的接口,如函数、变量等
return module.exports;
}
第一步,webpack
这里做了一层优化,通过对象 installedModules
进行缓存,查看模块是否在缓存中,有则间接从缓存中获取,没有则创立并放入缓存中,其中 key
值就是模块 Id
,也就是下面所说的文件门路
第二步,而后执行 模块函数,将 module
, module.exports
, __webpack_require__
作为参数传递,并把模块的函数调用对象指向 module.exports
,保障模块中的 this
指向永远指向以后的模块。
第三步,最初返回加载的模块,调用方间接调用即可。
所以 这个 __webpack_require__
就是来加载一个模块,并在最初返回模块 module.exports
变量
webpack 是如何反对 ESM 的
可能大家曾经发现,我下面的写法是 ESM
的写法,对于模块化的一些计划的理解,能够看看我的另外一篇文章【面试说】Javascript 中的 CJS, AMD, UMD 和 ESM 是什么?
咱们从新看回 模块函数
{"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
})
}
咱们看看 __webpack_require__.r
函数
__webpack_require__.r = function(exports) {object.defineProperty(exports, '__esModule', { value: true});
};
就是为 __webpack_exports__
增加一个属性 __esModule
,值为 true
再看一个 __webpack_require__.n
的实现
// 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;
};
__webpack_require__.n
会判断 module 是否为 es 模块,当 __esModule
为 true 的时候,标识 module 为 es 模块,默认返回module.default
,否则返回module
。
最初看 __webpack_require__.d
,次要的工作就是将下面的 getter
函数绑定到 exports 中的属性 a 的 getter
上
// 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
});
}
};
咱们最初再看会 sayHello.js
打包后的 模块函数,能够看到这里的导出是 __webpack_exports__["default"]
,实际上就是 __webpack_require__.n
做了一层包装来实现的,其实也能够看出,实际上 webpack
是能够反对 CommonJS
和 ES Module
一起混用的
"./src/sayHello.js":
/*! exports provided: default */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
function sayHello(name) {return `Hello ${name}`;
}
/* harmony default export */ __webpack_exports__["default"] = (sayHello);
})
目前为止,咱们大抵晓得了 webpack
打包进去的文件是怎么作用的了,接下来咱们剖析下代码拆散的一种非凡场景——动静导入
动静导入
代码拆散是 webpack
中最引人注目的个性之一。此个性可能把代码拆散到不同的 bundle
中,而后能够按需加载或并行加载这些文件。代码拆散能够用于获取更小的 bundle
,以及管制资源加载优先级,如果应用正当,会极大影响加载工夫。
常见的代码宰割有以下几种办法:
- 入口终点:应用
entry
配置手动地拆散代码。 - 避免反复:应用 Entry dependencies 或者
SplitChunksPlugin
去重和拆散 chunk。 - 动静导入:通过模块的内联函数调用来拆散代码。
本文咱们次要看看动静导入,咱们在 src
上面新建一个文件 another.js
function Another() {return 'Hi, I am Another Module';}
export {Another};
批改 index.js
import sayHello from './sayHello';
console.log(sayHello, sayHello('Gopal'));
// 单纯为了演示,就是有条件的时候才去动静加载
if (true) {import('./Another.js').then(res => console.log(res))
}
咱们来看下打包进去的内容,疏忽 .map 文件,能够看到多出一个 0.bundle.js
文件,这个咱们称它为动静加载的 chunk
,bundle.js
咱们称为主 chunk
输入的代码的话,主 chunk
看这里,动静加载的 chunk
看这里,上面是针对这两份代码的剖析
主 chunk 剖析
咱们先来看看主 chunk
内容多了很多,咱们来细看一下:
首先咱们留神到,咱们动静导入的中央编译后变成了以下,这是看起来就像是一个异步加载的函数
if (true) {__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./Another.js */ "./src/Another.js")).then(res => console.log(res))
}
所以咱们来看 __webpack_require__.e
这个函数的实现
__webpack_require__.e
——应用 JSONP 动静加载
// 已加载的 chunk 缓存
var installedChunks = {"main": 0};
// ...
__webpack_require__.e = function requireEnsure(chunkId) {
// promises 队列,期待多个异步 chunk 都加载实现才执行回调
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
// 0 代表曾经 installed
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
// 指标 chunk 正在加载,则将 promise push 到 promises 数组
if(installedChunkData) {promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
// 利用 Promise 去异步加载指标 chunk
var promise = new Promise(function(resolve, reject) {// 设置 installedChunks[chunkId]
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// i 设置 chunk 加载的三种状态并缓存在 installedChunks 中,避免 chunk 反复加载
// nstalledChunks[chunkId] = [resolve, reject, promise]
promises.push(installedChunkData[2] = promise);
// start chunk loading
// 应用 JSONP
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {script.setAttribute("nonce", __webpack_require__.nc);
}
// 获取指标 chunk 的地址,__webpack_require__.p 示意设置的 publicPath,默认为空串
script.src = __webpack_require__.p + ""+ chunkId +".bundle.js";
// 申请超时的时候间接调用办法完结,工夫为 120 s
var timeout = setTimeout(function(){onScriptComplete({ type: 'timeout', target: script});
}, 120000);
script.onerror = script.onload = onScriptComplete;
// 设置加载实现或者谬误的回调
function onScriptComplete(event) {
// avoid mem leaks in IE.
// 避免 IE 内存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// 如果为 0 则示意已加载,次要逻辑看 webpackJsonpCallback 函数
if(chunk !== 0) {if(chunk) {var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk' + chunkId + 'failed.\n(' + errorType + ':' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
}
}
return Promise.all(promises);
};
- 能够看出将
import()
转换成模仿JSONP
去加载动静加载的chunk
文件 -
设置
chunk
加载的三种状态并缓存在installedChunks
中,避免 chunk 反复加载。这些状态的扭转会在webpackJsonpCallback
中提到// 设置 installedChunks[chunkId] installedChunkData = installedChunks[chunkId] = [resolve, reject];
installedChunks[chunkId]
为0
,代表该chunk
曾经加载结束installedChunks[chunkId]
为undefined
,代表该chunk
加载失败、加载超时、从未加载过installedChunks[chunkId]
为Promise
对象,代表该chunk
正在加载
看完__webpack_require__.e
,咱们晓得的是,咱们通过 JSONP 去动静引入 chunk
文件,并依据引入的后果状态进行解决,那么咱们怎么晓得引入之后的状态呢?咱们来看异步加载的 chunk
是怎么的
异步 Chunk
// window["webpackJsonp"] 实际上是一个数组,向中增加一个元素。这个元素也是一个数组,其中数组的第一个元素是 chunkId,第二个对象,跟传入到 IIFE 中的参数一样
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./src/Another.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Another", function() {return Another;});
function Another() {return 'Hi, I am Another Module';}
/***/ })
}]);
//# sourceMappingURL=0.bundle.js.map
次要做的事件就是往一个数组 window['webpackJsonp']
中塞入一个元素,这个元素也是一个数组,其中数组的第一个元素是 chunkId
,第二个对象,跟主 chunk
中 IIFE 传入的参数相似。要害是这个 window['webpackJsonp']
在哪里会用到呢?咱们回到主 chunk
中。在 return __webpack_require__(__webpack_require__.s = "./src/index.js");
进入入口之前还有一段
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 保留原始的 Array.prototype.push 办法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 将 push 办法的实现批改为 webpackJsonpCallback
// 这样咱们在异步 chunk 中执行的 window['webpackJsonp'].push 其实是 webpackJsonpCallback 函数。jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
// 对已在数组中的元素顺次执行 webpackJsonpCallback 办法
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
jsonpArray
就是 window["webpackJsonp"]
,重点看上面这一句代码,当执行 push
办法的时候,就会执行 webpackJsonpCallback
,相当于做了一层劫持,也就是执行完 push 操作的时候就会调用这个函数
jsonpArray.push = webpackJsonpCallback;
webpackJsonpCallback ——加载完动静 chunk 之后的回调
咱们再来看看 webpackJsonpCallback
函数,这里的入参就是动静加载的 chunk
的 window['webpackJsonp']
push 进去的参数。
var installedChunks = {"main": 0};
function webpackJsonpCallback(data) {// window["webpackJsonp"] 中的第一个参数——即[0]
var chunkIds = data[0];
// 对应的模块详细信息,详见打包进去的 chunk 模块中的 push 进 window["webpackJsonp"] 中的第二个参数
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];
// 所以此处是找到那些未加载完的 chunk,他们的 value 还是[resolve, reject, promise]
// 这个能够看 __webpack_require__.e 中设置的状态
// 示意正在执行的 chunk,退出到 resolves 数组中
if(installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);
}
// 标记成曾经执行完
installedChunks[chunkId] = 0;
}
// 挨个将异步 chunk 中的 module 退出主 chunk 的 modules 数组中
for(moduleId in moreModules) {if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {modules[moduleId] = moreModules[moduleId];
}
}
// parentJsonpFunction: 原始的数组 push 办法,将 data 退出 window["webpackJsonp"] 数组。if(parentJsonpFunction) parentJsonpFunction(data);
// 等到 while 循环完结后,__webpack_require__.e 的返回值 Promise 失去 resolve
// 执行 resolove
while(resolves.length) {resolves.shift()();}
};
当咱们 JSONP
去加载异步 chunk
实现之后,就会去执行 window["webpackJsonp"] || []).push
,也就是 webpackJsonpCallback
。次要有以下几步
- 遍历要加载的 chunkIds,找到未执行完的 chunk,并退出到 resolves 中
for(;i < chunkIds.length; i++) {chunkId = chunkIds[i];
// 所以此处是找到那些未加载完的 chunk,他们的 value 还是[resolve, reject, promise]
// 这个能够看 __webpack_require__.e 中设置的状态
// 示意正在执行的 chunk,退出到 resolves 数组中
if(installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);
}
// 标记成曾经执行完
installedChunks[chunkId] = 0;
}
- 这里未执行的是非 0 状态,执行完就设置为 0
-
installedChunks[chunkId][0]
实际上就是 Promise 构造函数中的 resolve// __webpack_require__.e var promise = new Promise(function(resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject]; });
- 挨个将异步
chunk
中的module
退出主chunk
的modules
数组中 - 原始的数组
push
办法,将data
退出window["webpackJsonp"]
数组 - 执行各个
resolves
办法,通知__webpack_require__.e
中回调函数的状态
只有当这个办法执行实现的时候,咱们才晓得 JSONP
胜利与否,也就是script.onload/onerror
会在 webpackJsonpCallback
之后执行。所以 onload/onerror
其实是用来查看 webpackJsonpCallback
的完成度:有没有将 installedChunks
中对应的 chunk
值设为 0
动静导入小结
大抵的流程如下图所示
总结
本篇文章剖析了 webpack
打包主流程以及和动静加载状况下输入代码,总结如下
- 总体的文件就是一个
IIFE
——立刻执行函数 webpack
会对加载过的文件进行缓存,从而优化性能- 次要是通过
__webpack_require__
来模仿import
一个模块,并在最初返回模块export
的变量 webpack
是如何反对ES Module
的- 动静加载
import()
的实现次要是应用JSONP
动静加载模块,并通过webpackJsonpCallback
判断加载的后果
参考
- 剖析 webpack 打包后的文件
- webpack 打包产物代码剖析
- 『Webpack 系列』—— 路由懒加载的原理