在了解webpack原理之前,咱们先看以下几个核心概念
Entry 入口文件,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。Module 模块,webpack中一个模块对应一个文件,Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。Chunk 代码块,一个Chunk由多个module组成,主要是用于代码的分割。Loader 模块转换器,用于把模块原内容按照需求转换成新内容。Plugin 扩展插件,在 Webpack 构建流程中的特定时机,会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口:根据配置中的 entry 找出所有的入口文件;编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。Webpack 的构建流程可以分为以下三大阶段:
初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
初始化阶段

编译阶段

在编译阶段中,最重要的是compilation 事件,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件,它们分别是:
输出阶段

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。
输出文件分析
虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过
Webpack 输出的 bundle.js 是什么样子的吗?
为什么原来一个个的模块文件被合并成了一个单独的文件?
为什么 bundle.js 能直接运行在浏览器中? 本节将解释清楚以上问题。
先来看看由 安装与使用 中最简单的项目构建出的 bundle.js 文件内容,代码如下:
<p
data-height="565"
data-theme-id="0"
data-slug-hash="NMQzxz"
data-default-tab="js"
data-user="whjin"
data-embed-version="2"
data-pen-title="bundle.js"
class="codepen">
See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async src="static.codepen.io/ass...;></script>
以上看上去复杂的代码其实是一个立即执行函数,可以简写为如下:
function(modules) {
// 模拟 require 语句
function __webpack_require__() {
}
// 执行存放所有模块数组中的第0个模块
__webpack_require__(0);
})([/*存放所有模块的数组*/])
bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 __webpack_require__ 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。
原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。
如果仔细分析 __webpack_require__ 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。
分割代码时的输出
例如把源码中的 main.js 修改为如下:
// 异步加载 show.js
import('./show').then((show) => {
// 执行 show 函数
show('Webpack');
});
重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js。
其中 0.bundle.js 内容如下:
// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
// 在其它文件中存放着的模块的 ID
[0],
// 本文件所包含的模块
[
// show.js 所对应的模块
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;
})
]
);
bundle.js 内容如下:
<p
data-height="565"
data-theme-id="0"
data-slug-hash="yjmRyG"
data-default-tab="js"
data-user="whjin"
data-embed-version="2"
data-pen-title="bundle.js"
class="codepen"
>
> See the Pen bundle.js by whjin (@whjin) on CodePen.</p><script async src="static.codepen.io/ass...;></script>
这里的 bundle.js 和上面所讲的 bundle.js 非常相似,区别在于:
多了一个 __webpack_require__.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。
在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 __webpack_require__.e 和 webpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。
loader 和plugin 下一篇文章再续