Webpack: 核心流程之Init、Make、Seal

概述

在前文中,我们了解了 Webpack 的基本应用、性能优化、Loader 与 Plugin 组件开发方方面面的知识,相信学习过这些内容之后,你已经对 Webpack 有相当深入的理解了,可以开始从更底层的视角,自底向上重新审视 Webpack 实现原理。

Webpack 的功能集非常庞大:模块打包、代码分割、按需加载、Hot Module Replacement、文件监听、Tree-shaking、Sourcemap、Module Federation、Dev Server、DLL、多进程打包、Persistent Cache 等等,但抛开这些花里胡哨的能力,最最核心的功能依然是:At its core, webpack is a static module bundler for modern JavaScript applications,也就是所谓的静态模块打包能力

在这里插入图片描述

Webpack 能够将各种类型的资源 —— 包括图片、音视频、CSS、JavaScript 代码等,通通转译、组合、拼接、生成标准的、能够在不同版本浏览器兼容执行的 JavaScript 代码文件,这一特性能够轻易抹平开发 Web 应用时处理不同资源的逻辑差异,使得开发者以一致的心智模型开发、消费这些不同的资源文件。

打包功能的底层实现逻辑很复杂,抛去大多数分支逻辑后,大致包含如下步骤:
请添加图片描述
为了方便理解,我把上述过程划分为三个阶段:

在这里插入图片描述

  1. 初始化阶段:修整配置参数,创建 Compiler、Compilation 等基础对象,并初始化插件及若干内置工厂、工具类,并最终根据 entry 配置,找到所有入口模块;
  2. 构建阶段:从 entry 文件开始,调用 loader 将模块转译为 JavaScript 代码,调用 Acorn 将代码转换为 AST 结构,遍历 AST 从中找出该模块依赖的模块;之后 递归 遍历所有依赖模块,找出依赖的依赖,直至遍历所有项目资源后,构建出完整的 模块依赖关系图
  3. 生成阶段:根据 entry 配置,将模块组装为一个个 Chunk 对象,之后调用一系列 Template 工厂类翻译 Chunk 代码并封装为 Asset,最后写出到文件系统。
  • 提示:单次构建过程自上而下按顺序执行,如果启动了 watch ,则构建完成后不会退出 Webpack 进程,而是持续监听文件内容,发生变化时回到「构建」阶段重新执行构建。

三个阶段环环相扣,「初始化」的重点是根据用户配置设置好构建环境;「构建阶段」则重在解读文件输入与文件依赖关系;最后在「生成阶段」按规则组织、包装模块,并翻译为适合能够直接运行的产物包。三者结合,实现 Webpack 最核心的打包能力,其它功能特性也几乎都是在此基础上,通过 Hook 介入、修改不同阶段的对象状态、流程逻辑等方式实现。

可以说,深度理解这三个阶段,才算是真正掌握了 Webpack 核心原理,所以接下来,让我们一起深入底层源码,剖析各阶段的具体实现。

初始化阶段

初始化阶段主要完成三个功能:修整 & 校验配置对象、运行插件、调用 compiler.compile 方法开始执行构建操作,代码比较简单,如下图:
请添加图片描述
首先,校验用户参数,并合并默认配置对象:

  1. 启动时,首先将 process.args 参数与 webpack.config.js 文件合并成用户配置;
  2. 调用 validateSchema 校验配置对象(validateSchema 底层依赖于 schema-utils 库);
  3. 调用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终配置。

之后,创建 Compiler 对象并开始启动插件:

  1. 调用 createCompiler 函数创建 compiler 对象。

  2. 遍历 配置中的 plugins 集合,执行插件的 apply 方法。

  3. 调用 new WebpackOptionsApply().process 方法,根据配置内容动态注入相应插件,包括:

    • 调用 EntryOptionPlugin 插件,该插件根据 entry 值注入 DynamicEntryPluginEntryPlugin 插件;
    • 根据 devtool 值注入 Sourcemap 插件,包括:SourceMapDevToolPluginEvalSourceMapDevToolPluginEvalDevToolModulePlugin
    • 注入 RuntimePlugin ,用于根据代码内容动态注入 webpack 运行时。

最后,调用 compiler.compile 方法开始执行构建,这一步非常重要,源码:

// webpack/lib/compiler.js 
compile(callback) {const params = this.newCompilationParams();this.hooks.beforeCompile.callAsync(params, err => {// ...const compilation = this.newCompilation(params);this.hooks.make.callAsync(compilation, err => {// ...this.hooks.finishMake.callAsync(compilation, err => {// ...process.nextTick(() => {compilation.finish(err => {// ...compilation.seal(err => {// ...this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);return callback(null, compilation);});});});});});});});}

虽然 compile 方法并没有任何实质的功能逻辑,但它搭建起了后续构建流程框架:

  1. 调用 newCompilation 方法创建 compilation 对象;
  2. 触发 make 钩子,紧接着 EntryPlugin 在这个钩子中调用 compilation 对象的 addEntry 方法创建入口模块,主流程开始进入「构建阶段」;
  3. make 执行完毕后,触发 finishMake 钩子;
  4. 执行 compilation.seal 函数,进入「生成阶段」,开始封装 Chunk,生成产物;
  5. seal 函数结束后,触发 afterCompile 钩子,开始执行收尾逻辑。
  • 提示:compile 函数是后续所有功能逻辑的起点,非常重要,请务必前往阅读 源码。

调用 compile 函数触发 make 钩子后,初始化阶段就算是结束了,流程逻辑开始进入「构建阶段」。

构建阶段

构建阶段」从 entry 模块开始递归解析模块内容、找出模块依赖,按图索骥逐步构建出项目整体 module 集合以及 module 之间的 依赖关系图,这个阶段的主要作用就是读入并理解所有原始代码。

实现上,在上述「初始化阶段」的最后,compiler.compile 函数会触发 compiler.hook.make 钩子,EntryPlugin 监听该钩子并开始调用 compilation.addEntry 添加入口:

class EntryPlugin {apply(compiler) {const { entry, options, context } = this;// 创建入口 Dependency 对象const dep = EntryPlugin.createDependency(entry, options);compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {compilation.addEntry(context, dep, options, err => {callback(err);});});}
}

addEntry 之后的执行逻辑:

请添加图片描述

  1. 调用 handleModuleCreation,根据文件类型构建 module 子类 —— 一般是 NormalModule;

  2. 调用 loader-runner 转译 module 内容,将各类资源类型转译为 Webpack 能够理解的标准 JavaScript 文本;

  3. 调用 acorn 将 JavaScript 代码解析为 AST 结构;

  4. 在 JavaScriptParser 类中遍历 AST,触发各种钩子,其中最关键的:

    1. 遇到 import 语句时,触发 exportImportSpecifier 钩子;
    2. HarmonyExportDependencyParserPlugin 监听该钩子,将依赖资源添加为 Dependency 对象;
    3. 调用 module 对象的 addDependency, 将 Dependency 对象转换为 Module 对象并添加到依赖数组中。

在这里插入图片描述

  1. AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖数组;
  2. 对于 module 新增的依赖,调用 handleModuleCreate,控制流回到第一步;
  3. 所有依赖都解析完毕后,构建阶段结束。

过程中模块源码经历了 module => ast => dependences => module 的流转,先将源码解析为 AST 结构,再在 AST 中遍历 import 等模块导入语句,收集模块依赖数组 —— dependences,最后遍历 dependences 数组将 Dependency 转换为 Module 对象,之后递归处理这些新的 Module,直到所有项目文件处理完毕。

  • 提示:这个过程会调用 acorn 将模块内容 —— 包括 JS、CSS,甚至多媒体文件,解析为 AST 结构,所以需要使用 loaders 将不同类型的资源转译为标准 JavaScript 代码。

这个递归处理流程是「构建阶段」的精髓,我们来看个例子,假设对于下图这种简单模块依赖关系:

在这里插入图片描述

其中 index.js 为 entry 文件,依赖于 a/b 文件;a 依赖于 c/d 文件。初始化编译环境之后,EntryPlugin 根据 entry 配置找到 index.js 文件,并调用 compilation.addEntry 函数将之添加为 Module 对象,触发构建流程,构建完毕后内部会生成这样的数据结构:

在这里插入图片描述

之后,调用 Acorn 将 index.js 代码解析为 AST,并遍历 AST 找到 index.js 文件的依赖:

在这里插入图片描述

得到两个新的依赖对象:dependence[a.js]dependence[b.js] ,这是下一步操作的关键线索,紧接着调用 module[index.js]handleParseResult 函数处理这两个依赖对象,得到 a、b 两个新的 Module 对象:
在这里插入图片描述
接着,又触发 module[a/b]handleModuleCreation 方法,从 a.js 模块中又解析到 c.js/d.js 两个新依赖,于是再继续调用 module[a]handleParseResult,递归上述流程:
在这里插入图片描述
最终得到 a/b/c/d 四个 Module 与对应的 Dependency 对象:
请添加图片描述

  • 提示:Dependency、Module、Entry 等都是 Webpack 内部非常重要的基本类型,在后续章节中我们会单独展开这几个类型的基本涵义与相互之间的关系。

到这里解析完所有模块,没有新的依赖后就可以继续推进,进入「生成阶段」。

生成阶段

「构建阶段」负责读入与分析源代码文件,将之一一转化为 Module、Dependency 对象,解决的是资源“输入”问题;而「生成阶段」则负责根据一系列内置规则,将上一步构建出的所有 Module 对象拆分编排进若干 Chunk 对象中,之后以 Chunk 粒度将源码转译为适合在目标环境运行的产物形态,并写出为产物文件,解决的是资源“输出”问题。

「生成阶段」发生在 make 阶段执行完毕,compiler.compile 调用 compilation.seal 函数时:

// webpack/lib/compiler.js 
compile(callback) {// ...const compilation = this.newCompilation(params);this.hooks.make.callAsync(compilation, err => {// ...compilation.seal(err => {/* */});});}

也就是说,compilation.seal 函数是「生成阶段」的入口函数,seal 原意密封、上锁,我个人理解在 Webpack 语境下接近于“将模块装进 Chunk”,核心流程:
请添加图片描述

  1. 创建本次构建的 ChunkGraph 对象。

  2. 遍历 入口集合 compilation.entries

    • 调用 addChunk 方法为每一个入口 创建 对应的 Chunk 对象(EntryPoint Chunk);
    • 遍历 该入口对应的 Dependency 集合,找到 相应 Module 对象并 关联 到该 Chunk。
  3. 到这里可以得到若干 Chunk,之后调用 buildChunkGraph 方法将这些 Chunk 处理成 Graph 结构,方便后续处理。

  4. 之后,触发 optimizeModules/optimizeChunks 等钩子,由插件(如 SplitChunksPlugin)进一步修剪、优化 Chunk 结构。

  5. 一直到最后一个 Optimize 钩子 optimizeChunkModules 执行完毕后,开始调用 compilation.codeGeneration 方法生成 Chunk 代码,在 codeGeneration 方法内部:

    1. 遍历每一个 Chunk 的 Module 对象,调用 _codeGenerationModule;
    2. _codeGenerationModule 又会继续往下调用 module.codeGeneration 生成单个 Module 的代码,这里注意不同 Module 子类有不同 codeGeneration 实现,对应不同产物代码效果。
      在这里插入图片描述
  6. 所有 Module 都执行完 codeGeneration,生成模块资产代码后,开始调用 createChunkAssets 函数,为每一个 Chunk 生成资产文件。

  7. 调用 compilation.emitAssets 函数“提交”资产文件,注意这里还只是记录资产文件信息,还未写出磁盘文件。

  8. 上述所有操作正常完成后,触发 callback 回调,控制流回到 compiler 函数。

  9. 最后,调用 compiler 对象的 emitAssets 方法,输出资产文件。

seal 很复杂,重点在于将 Module 按入口组织成多个 Chunk 对象,之后暴露 optimizeXXX 钩子,交由插件根据不同需求对 Chunk 做进一步修剪、整形、优化,最后按 Chunk 为单位做好代码合并与转换,输出为资产文件。

提示:上述 optimizeXXX 钩子常被用于优化最终产物代码,例如 SplitChunksPlugin 就可以在这里分析 Chunk、Module 关系,将使用率较高的 Module 封装进新的 Chunk,实现 Common Chunk 效果。

简单理解,Entry 与 Chunk 一一对应,而 Chunk 与最终输出的资源一一对应,我们来看个示例,假如有这样的配置:

// webpack.config.js
module.exports = {entry: {a: "./src/a.js",b: "./src/b.js",},// ...
};

实例配置中有两个入口,对应的文件结构:

在这里插入图片描述
a 依赖于 c/e;b 依赖于 c/d;a/b 同时依赖于 c。最终生成的 Chunk 结构为:
在这里插入图片描述
也就是根据依赖关系,chunk[a] 包含了 a/c/e 三个模块,chunk[b] 包含了 b/c/d 三个模块。

seal 过程中会不断调用 compilation.emitAssets 提交资产记录,而直到 seal 结束后则调用 compiler.emitAssets 函数,函数内部调用 compiler.outputFileSystem.writeFile 方法将 assets 集合写入文件系统,Webpack 完成从源码到资产文件的转换,构建工作至此结束。

资源形态流转

OK,上面我们已经把逻辑层面的构造主流程梳理完了,最后我们再结合资源形态流转的角度重新考察整个过程,加深理解:
在这里插入图片描述

  • compiler.make 阶段:

    • entry 文件以 dependence 对象形式加入 compilation 的依赖列表,dependence 对象记录了 entry 的类型、路径等信息;
    • 根据 dependence 调用对应的工厂函数创建 module 对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为 module
  • compilation.seal 阶段:

    • 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 Chunk;
    • Chunk 之间最终形成 ChunkGraph 结构;
    • 遍历 ChunkGraph,调用 compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合。
  • compiler.emitAssets 阶段:

    • assets 写入文件系统。

这个过程用到很多 Webpack 基础对象,包括:

  • Entry:编译入口;
  • Compiler:编译管理器,Webpack 启动后会创建 compiler 对象,该对象一直存活直到构建结束进程退出;
  • Compilation:单次构建过程的管理器,比如 watch = true 时,运行过程中只有一个 compiler,但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象;
  • Dependence:依赖对象,记录模块间依赖关系;
  • Module:Webpack 内部所有资源都会以 Module 对象形式存在,所有关于资源的操作、转译、合并都是以 Module 为单位进行的;
  • Chunk:编译完成准备输出时,将 Module 按特定的规则组织成一个一个的 Chunk。

这里简单了解即可,后面章节中我们还会继续挖掘不同对象的作用与细节。

总结


综上,Webpack 底层源码非常复杂,但撇除所有分支逻辑后,构建主流程可以简单划分为三个阶段:

  • 初始化阶段:负责设置构建环境,初始化若干工厂类、注入内置插件等;
  • 构建阶段:读入并分析 Entry 模块,找到模块依赖,之后递归处理这些依赖、依赖的依赖,直到所有模块都处理完毕,这个过程解决资源“输入”问题;
  • 生成阶段:根据 Entry 配置将模块封装进不同 Chunk 对象,经过一系列优化后,再将模块代码翻译成产物形态,按 Chunk 合并成最终产物文件,这个过程解决资源“输出”问题。

这个过程串起资源「输入」到「输出」的关键步骤,可以说是 Webpack 最重要的流程骨架,没有之一!所以建议你务必跟随上述各个阶段的介绍,翻阅源码中对应的具体代码,深度理解 Webpack 构建功能的实现细节。

在后面章节中,我还会在这个流程骨架基础上,继续展开一些有代表性的对象、分支、功能实现逻辑,帮助你更体系化理解 Webpack 实现原理。

我们可以思考下,在「构建阶段」,为什么需要先将依赖文件构建为 Dependency,之后再根据 Dependency 创建文件对应的 Module 对象?Dependency 对象到底有什么作用?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/39637.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

直流电机介绍

一、引入 电机,即电动机(motor)俗称马达,是电能转化为机械能的总称,按工作原理分类电机可以分为:直流电机(通过直流电源供电,依靠电刷和换向器改变电流方向,产生连续转动…

文献解读-长读长测序-第十四期|《作为了解棉花驯化的资源,印度棉(Gossypium herbaceum L. Wagad)基因组》

关键词:基因组;长读长测序;棉花基因组; 文献简介 标题(英文):The Gossypium herbaceum L. Wagad genome as a resource for understanding cotton domestication标题(中文&#xff…

DBdoctor v3.2.2 版本发布,支持对sqlserver、vastbase的纳管!

DBdoctor 3.2.2版本新增PgSQL的索引推荐及性能审核功能;拓展了oracle的纳管版本并支持纳管oracle rac;新增对sqlserver、vastbase数据库的纳管支持;修复了体验官活动中大家提出的一系列体验问题。 详细更新内容如下: 功能优化 Pg…

贪心算法题目总结

1. 整数替换 看到这道题目,我们首先能想到的方法就应该是递归解法,我们来画个图 此时我们出现了重复的子问题,就可以使用递归,只要我们遇到偶数,直接将n除以2递归下去,如果是奇数,选出加1和减1中…

我在手提电脑上将大模型训练成了语文老师

(图片由大模型生成,如有侵权,立删) 记得一年多以前,和不少商家交流大模型解决方案时,他们谈到内部有很多的资料,可以对大模型进行训练,让大模型变得更有智慧,从而为客户…

Day9:逆波兰表达式求值150 滑动窗口最大值239 前 K 个高频元素347

题目150. 逆波兰表达式求值 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int evalRPN(vector<string>& tokens) {//使用栈来消除stack<string> st;for(int i0;i<tokens.size();i){if(tokens[i]""||tokens[i]"-"|…

创新赋能,高效二开!CRMEB 标准版 v5.4公测版发布

历经十年磨砺&#xff0c;CRMEB 标准版如今已成为众多技术开发者与企业二次开发、构建定制化项目的热门系统&#xff0c;其全开源无加密、功能齐全、独立部署的特质&#xff0c;造就了标准版系统方便二开的优势&#xff0c;不仅深受开发者喜爱&#xff0c;更因其实用性和可靠性…

运行vue : 无法加载文件 C:\Program Files\nodejs\node_global\vue.ps1,因为在此系统上禁止运行脚本。

报错背景: 重装了win10系统,然后准备安装Vue,这个时候我已经安装好了node.js和npm,输入node -v和npm -v都有正确输出,但是每次输入npm install -g vue/cli 安装的时候,就会报错. 大家安装node.js的时候最好就是默认路径(C:\Program Files\nodejs),别去修改不然很多报错.(个人…

Linux Centos7部署Zookeeper

目录 一、下载zookeeper 二、单机部署 1、创建目录 2、解压 3、修改配置文件名 ​4、创建保存数据的文件夹 ​5、修改配置文件保存数据的地址 ​6、启动服务 7、api创建节点 一、下载zookeeper 地址&#xff1a;Index of /dist/zookeeper/zookeeper-3.5.7 (apache.org…

名企面试必问30题(十五)——你的学历有点低,你怎么看?

1.思路 首先承认学历低这一事实&#xff1a;我坦诚地承认自己的学历相对较低。 后续解决方案&#xff1a;不过&#xff0c;我相信能力的展现不仅仅取决于学历。在过往的工作经历中&#xff0c;我积累了丰富的实践经验&#xff0c;培养了较强的学习能力和解决问题的能力。我会持…

两台电脑怎么传文件?干货分享教程

当需要在两台电脑之间传输文件时&#xff0c;有多种方便的方法可供选择&#xff0c;以下是一些常见的方式及教程&#xff1a; 使用局域网共享&#xff1a; 确保两台电脑连接在同一个局域网内。 在其中一台电脑上&#xff0c;设置要共享的文件夹。右键点击文件夹&#xff0c;选…

论文学习——使用基于多项式拟合的预测算法求解动态多目标问题

论文题目&#xff1a;Solving dynamic multi-objective problems using polynomial fitting-based prediction algorithm 使用基于多项式拟合的预测算法求解动态多目标问题&#xff08;Qingyang Zhang , Xiangyu He,Shengxiang Yang , Yongquan Dong , Hui Song , Shouyong Ji…

归并排序-MergeSort (C语言详解)

目录 前言归并排序的思想归并排序的递归法归并排序的非递归法归并排序的时间复杂度与适用场景总结 前言 好久不见, 前面我们了解到了快速排序, 那么本篇旨在介绍另外一种排序, 它和快速排序的思想雷同, 但又有区别, 这就是归并排序, 如下图, 我们对比快速排序与归并排序. 本…

Nacos配置中心客户端源码分析(二): 客户端和服务端交互

本文收录于专栏 Nacos 推荐阅读&#xff1a;Nacos 架构 & 原理 ⚠️&#xff1a;使用的Nacos版本为2.3.X 文章目录 前言一、NacosConfigLoader二、NacosConfigService三、ClientWorker四、服务端处理逻辑总结 前言 上篇文章我们简单看了看Nacos客户端在启动的时候&#xf…

远程抄表管理系统建设方案(Word原件)

远程抄表管理系统建设方案应涵盖智能表计安装、数据采集与传输、数据处理与分析三大核心环节。首先&#xff0c;安装智能表计以实时采集水、电、气等数据&#xff1b;其次&#xff0c;利用先进的通信技术&#xff08;如GPRS、LoRa等&#xff09;实现数据的稳定传输&#xff1b;…

c语言回顾-数组(全网最详细,哈哈哈)

目录 前言&#xff0c;和小编一起感受数组的魅力&#xff01;&#xff01;&#xff01; 1.数组的概念 2.一维数组的创建和初始化 2.1数组创建 2.2数组的初始化 2.3数组的类型 3.一维数组的使用 3.1数组下标 3.2数组元素的输入输出 小结&#xff1a; 4.一维数组在内存…

pycharm的usages在哪设置?

参考文章&#xff1a;https://blog.51cto.com/save/8961821 在代码编辑器&#xff08;如PyCharm或IntelliJ IDEA&#xff09;中&#xff0c;"1 usage"通常表示当前光标所在的代码元素&#xff08;如变量、函数、类等&#xff09;在其他地方被使用了一次。这个功能可…

什么是自动气象站呢

自动气象站&#xff0c;作为现代气象观测的重要工具&#xff0c;已经深入到我们生活的各个领域&#xff0c;从气象预报到农业生产&#xff0c;再到环境保护&#xff0c;自动气象站都发挥着不可或缺的作用。 自动气象站&#xff0c;顾名思义&#xff0c;是一种能够自动收集、处理…

昇思25天学习打卡营第7天|网络构建

网络构建 神经网络模型由tensor操作和神经网络层构成。 MIndSporezhong&#xff0c;Cell是构建所有网络的基类&#xff0c;也是网络的基本单元。cell也由子cell构成。 定义模型类 # 继承nn.Cell类 class Network(nn.Cell):def __init__(self):super().__init__()self.flatte…

所以,这些AI产品都死了?? 创业就是一场大型狼人杀;独立开发者的营销流量密码;谷歌竟然搞砸过300个项目 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;ShowMeAI官网 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; 1. 你可能不知道&#xff1a;Google Graveyard「埋葬」着 300 个被谷歌搞砸的项目 官网链接 → https://killedbygoogle.com Google Graveyard (Killed by Google…