它是如何工作的 概述基本概念
在了解Webpack原理之前,需要先掌握以下几个核心概念,以方便理解:
流程概览
Webpack的运行过程是一个串行过程,从开始到结束会依次执行以下过程:
初始化参数:从配置文件和Shell语句中读取并组合参数,得到最终参数; 开始编译:用上一步获得的参数初始化Compiler对象,加载所有配置的插件,并执行该对象的运行模式开始编译; 确定入口:根据配置中的入口找到所有入口文件; 编译模块:从入口文件开始,调用所有配置的Loader对模块进行翻译,然后找出该模块所依赖的模块,然后递归这一步,直到所有入口依赖的所有文件都在这一步处理完毕; 模块编译完成:在步骤4中使用Loader翻译完所有模块后,得到每个模块最终翻译的内容以及它们之间的依赖关系; 输出资源:根据条目和模块之间的依赖关系组装成包含多个模块的chunk,然后将每个chunk转换为单独的文件并添加到输出列表中。 这一步是改变输出内容的最后机会; 输出完成:确定输出内容后,根据配置确定输出路径和文件名,并将文件内容写入文件系统。
上述过程中webpack是,Webpack会在特定的时间点广播特定的风暴,插件监听到感兴趣的风暴后会执行特定的逻辑,插件可以调用Webpack提供的API来改变运行的方式Webpack 的结果。
工艺细节
Webpack的建立过程可以分为以下三个阶段:
初始化:启动并构建、读取并合并配置参数、加载Plugin、实例化Compiler。 编译:从Entry发出,为每个Module串行调用对应的Loader翻译文件内容,然后找到该Module所依赖的Module,并递归编译。 输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统。
如果只执行一次构建,则上述阶段将按顺序执行。 但当开启窃听模式时webpack是,流程就会变成如下:
每个主要阶段都会产生很多扰动,Webpack会将这些扰动广播出来供Plugin使用。
初始化阶段风暴名称解释
初始化参数
从配置文件和shell语句中读取并组合参数,得到最终的参数。 在这个过程中,会执行配置文件中的插件实例化语句newPlugin()。
实例化编译器
使用上一步获得的参数初始化Compiler实例,Compiler负责文件窃听并启动编译。 Compiler实例包含完整的Webpack配置,并且全局只有一个Compiler实例。
加载插件
依次调用插件的apply方法,使得插件能够监听后续所有的storm节点。 同时将编译器实例的引用传递给插件,方便插件通过编译器调用Webpack提供的API。
环境
开始将Node.js风格的文件系统应用到编译对象上,方便后续的文件查找和读取。
进入选项
读取配置好的Entry,为每个Entry实例化一个对应的EntryPlugin,并规划前一个Entry的递归解析。
后插件
调用所有外部和配置的插件的apply方法后。
后分解器
解析器根据配置初始化后,负责查找文件系统中指定路径下的文件。
编译阶段风暴名称解释
跑步
开始新的编译。
观察运行
与run类似,不同的是它是以窃听模式启动的编译。 在这场风暴中,您可以获取哪些文件发生了更改并导致重新启动新的编译。
编译
这次storm的目的是告诉插件新的编译即将开始,同时将编译器对象带到插件中。
汇编
当 Webpack 在开发模式下运行时,每次检测到文件更改时,都会创建一个新的 Compilation。 一个Compilation对象包含了当前的模块资源、编译后的资源、变化的文件等。Compilation对象还提供了很多风暴反弹供插件扩展。
制作
创建新的Compilation后,正式从Entry中读取文件,根据文件类型和配置的Loader对文件进行编译。 编译完成后,找到该文件所依赖的文件,递归编译并解析。
编译后
编译执行完成。
无效的
当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致Webpack退出。
在编译阶段,最重要的就是编译干扰。 由于在编译阶段调用Loader来完成各个模块的转换操作,因此编译阶段存在很多小扰动。 他们是:
风暴名称解释
构建模块
使用相应的Loader来转换模块。
普通模块加载器
用Loader转换一个模块后,使用acorn解析转换后的内容,输出对应的具体句子树(AST),方便Webpack后的代码分析。
程序
从配置好的入口模块开始,分析它的AST。 当遇到require等导出的模块语句时,就会被添加到依赖模块列表中。 同时,它会递归地分析新发现的依赖模块,最终识别出所有模块的依赖关系。 。
海豹
所有模块及其依赖模块经过Loader转换后,根据依赖关系生成chunk。
输出阶段风暴名称解释
应该发出
需要输出的文件都已经生成了,所以询问插件哪些文件需要输出,哪些不需要输出。
发射
确定输出什么文件后,执行文件输出,在这里可以获取和更改输出内容。
后发射
文件输出完成。
完毕
成功完成了一个完整的编译和输出过程。
失败的
如果编译输出过程中遇到异常导致Webpack退出,会直接跳转到这一步,插件可以获取本次风暴中错误的具体原因。
在输出阶段,已经得到了各个模块的转换结果及其依赖关系,只是将相关模块一一组合起来生成Chunk。 在输出阶段,根据Chunk的类型,将使用相应的模板生成最终的输出文件内容。
输出文件的剖析
虽然我们知道如何使用 Webpack 并且大致知道它是如何工作的,但是你有没有想过 Webpack 输出的bundle.js 是什么样子的? 为什么原来的模块文件合并成一个文件? 为什么bundle.js可以直接在浏览器中运行?
我们看一下最简单的项目创建的bundle.js文件的内容。 代码如下:
(
// webpackBootstrap 启动函数
// modules 即为存放所有模块的数组,数组中的每一个元素都是一个函数
function (modules) {
// 安装过的模块都存放在这里面
// 作用是把已经加载过的模块缓存在内存中,提升性能
var installedModules = {};
// 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index
// 作用和 Node.js 中 require 语句相似
function __webpack_require__(moduleId) {
// 如果需要加载的模块已经被加载过,就直接从内存缓存中返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 如果缓存中不存在需要加载的模块,就新建一个模块,并把它存在缓存中
var module = installedModules[moduleId] = {
// 模块在数组中的 index
i: moduleId,
// 该模块是否已经加载完毕
l: false,
// 该模块的导出值
exports: {}
};
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 再调用这个函数,同时把函数需要的参数传入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 把这个模块标记为已加载
module.l = true;
// 返回这个模块的导出值
return module.exports;
}
// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
__webpack_require__.p = "";
// 使用 __webpack_require__ 去加载 index 为 0 的模块,并且返回该模块导出的内容
// index 为 0 的模块就是 main.js 对应的文件,也就是执行入口模块
// __webpack_require__.s 的含义是启动模块对应的 index
return __webpack_require__(__webpack_require__.s = 0);
})(
// 所有的模块都存放在了一个数组里,根据每个模块在数组的 index 来区分和定位模块
[
/* 0 */
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__ 规范导入 show 函数,show.js 对应的模块 index 为 1
const show = __webpack_require__(1);
// 执行 show 函数
show('Webpack');
}),
/* 1 */
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出 show 函数
module.exports = show;
})
]
);
上面看似复杂的代码毕竟是一个立即执行的函数,可以简写如下:
(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的内容如下:
(function (modules) {
/***
* webpackJsonp 用于从异步加载的文件中安装模块。
* 把 webpackJsonp 挂载到全局是为了方便在其它文件中调用。
*
* @param chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
* @param moreModules 异步加载的文件中存放的需要安装的模块列表
* @param executeModules 在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的 index
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 把 moreModules 添加到 modules 对象中
// 把所有 chunkIds 对应的模块都标记成已经加载成功
var moduleId, chunkId, i = 0, resolves = [], result;
for (; i {
// 执行 show 函数
show('Webpack');
});
})
]
);
这里的bundle.js和上面提到的bundle.js很相似,不同的是:
使用CommonsChunkPlugin提取公共代码时,输出文件与使用异步加载时相同,都会有__webpack_require__.e和webpackJsonp。 原因是提取公共代码和异步加载本质上是代码拆分。
发表评论