webpack4源码分析

2023-12-18 17:08
文章标签 分析 源码 webpack4

本文主要是介绍webpack4源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

webpack设计模式

Webpack 源码是一个插件的架构,他的很多功能都是通过诸多的内置插件实现的。Webpack为此专门自己写一个插件系统,叫 Tapable 主要提供了注册和调用插件的功能。

Tapable

tabpable是一个事件发布订阅插件,它支持同步和异步两种;在需要使用的类上继承tabpable,并且该类的构造函数中使用this.hooks添加事件名称。

 this.hooks = {accelerate: new SyncHook(["newSpeed"]),break: new SyncHook(),calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])};
复制代码

订阅

要使用订阅功能,需要先拿到上面说到的类实例,通过实例对象.hooks.break.tap来订阅。

myCar.hooks.break.tap("WarningLampPlugin", () => warningLamp.on());
复制代码

发布

在需要触发的时机调用this.hooks.accelerate.call就可以触发订阅accelerate的所有监听函数,newSpeed是传入的参数。

setSpeed(newSpeed) {this.hooks.accelerate.call(newSpeed);}
复制代码

webpack的插件架构

webpack从配置初始化到build完成定义了一个生命周期,在这个生命周中的每一个阶段定义一些完成不同的功能的含义,webpack的流程就是定义了一个规范,无论是内部插件还是自定义插件只要遵循这个规范就能完成构建;上面提到了webpack是一个插件架构,webpack主要是使用CompilerCompilation类来控制webpack的整个生命周期,定义执行流程;他们都继承了tabpable并且通过tabpable来注册了生命周期中的每一个流程需要触发的事件。webpack内部实现了一堆plugin,这些内部plugin是webpack打包构建过程中的功能实现,订阅感兴趣的事件,在执行流程中调用不同的订阅函数就构成了webpack的完整生命周期。

webpack流程概述

Webpack首先会把配置参数和命令行的参数及默认参数合并,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compiler的run来真正启动webpack编译构建过程,webpack的构建流程包括compile、make、build、seal、emit阶段,执行完这些阶段就完成了构建过程。

  • 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段
  • 在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了
  • 进入 make 阶段,会从 entry 开始进行两步操作:
  • 第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码
  • 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树
  • 最后调用 compilation.seal 进入 render 阶段,根据之前收集的依赖,决定生成多少文件,每个文件的内容是什么

初始化

启动

首先从bin/webpack.js开始调用webpack-cli插件的./bin/cli.js`文件,在cli.js中使用yargs来解析命令行参数并合并配置文件中的参数(options),然后调用lib/webpack.js实例化compiler。

实例化compiler

实例化compiler是在lib/webpack.js中完成的,首先会检查配置参数是否合法;然后根据传入的参数判断是否为数组,若是数组则创建多个compiler,否则创建一个compiler;下面以创建一个compiler来讲述,首先会调用WebpackOptionsDefaulter把传入的参数和默认参数合并得到新的options,创建Compiler,创建读写文件对象和执行注册配置的plugin插件,最后通过WebpackOptionsApply初始化一堆构建需要的内部默认插件。

执行

实例compiler后根据options的watch判断是否启动了watch,如果启动watch了就调用compiler.watch来监控构建文件,否则启动compiler.run来构建文件。

编译构建

接下来正式进入webpack的构建流程,webpack构建流程入口是compiler的run或者watch方法,下面通过run来描述编译过程;在run方法中先执行beforeRun、run钩子函数后进入compile,可以写插件在构建之前来处理一些初始化数据。

在进入构建之前解释两个类

  • Compiler:该类是webpack的神经中枢,一方面所有的配置数据都存储在该实例上,另一方面它是在构建过程中控制整个大体的流程。
  • Compilation:该类是webpack的cto,所有的构建过程中产生的构建数据都存储在该对象上,它掌控着构建过程中每一个细节流程。

compile

在run中先实例化normalModuleFactory等参数,然后调用this.hooks.beforeCompile事件执行一些编译之前需要处理的插件,最后才执行this.hooks.compile事件(比如compile钩子中会执行DllReferencePlugin,在这里注册代理插件);this.hooks.compile执行完后实例化Compilation对象,并调用this.hooks.compilation通知感兴趣的插件,比如在compilation.dependencyFactories中添加依赖工厂类等操作。compile阶段主要是为了进入make阶段做准备,make阶段才是从入口开始递归查找构建模块。

make

make是compilation初始化完成触发的事件,该事件一般情况是通知在WebpackOptionsApply中注册的EntryOptionPlugin插件,在该插件中使用entries参数创建一个单入口(SingleEntryDependency)或者多入口(MultiEntryDependency)依赖,多个入口时在make事件上注册多个相同的监听,并行执行多个入口;然后调用compilation.addEntry(context, dep, name, callback)正式进入make阶段。

addEntry中并没有做任何事,就调用this._addModuleChain方法,在_addModuleChain中根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule对象,并且把MultModule对象存入compilation的modules中后执行MultModule.build,因为是入口module,所以在build中没处理任何事直接调用了afterBuild;在afterBuild中判断是否有依赖,若是叶子结点直接结束,否则调用processModuleDependencies方法来查找依赖;因为入口传入了一个SingleEntryDependency,所以下面正式讲述从SingleEntryDependency开始的构建。

上面提到入口会创建一个SingleEntryDependency传入,所以上面讲述的afterBuild肯定至少存在一个依赖,processModuleDependencies方法就会被调用;processModuleDependencies根据当前的module.dependencies对象查找该module依赖中所有需要加载的资源和对应的工厂类,并把module和需要加载资源的依赖作为参数传给addModuleDependencies方法;在addModuleDependencies中异步执行所有的资源依赖,在异步中调用依赖的工厂类的create去查找该资源的绝对路径和该资源所依赖所有loader的绝对路径,并且创建对应的module后返回;然后根据该moduel的资源路径作为key判断该资源是否被加载过,若加载过直接把该资源引用指向加载过的module返回;否则调用this.buildModule方法执行module.build加载资源;build完成就得到了loader处理过后的最终module了,然后递归调用afterBuild,直到所有的模块都加载完成后make阶段才结束。

<figure>[图片上传中...(image-775802-1545355486959-9)]

<figcaption></figcaption>

</figure>

DllReferencePlugin

在make阶段webpack会根据模块工厂(normalModuleFactory)的create去实例化module;实例化moduel后触发this.hooks.module事件,若构建配置中注册了DllReferencePlugin插件,DelegatedModuleFactoryPlugin会监听this.hooks.module事件,在该插件里判断该moduel的路径是否在this.options.content中,若存在则创建代理module(DelegatedModule)去覆盖默认module;DelegatedModule对象的delegateData中存放manifest中对应的数据(文件路径和id),所以DelegatedModule对象不会执行bulled,在生成源码时只需要在使用的地方引入对应的id即可。

build

上面在make阶段提到了build,但是没有深入讲解,因为build是在module对象中执行,这节单独说一下build是如何加载和执行loader最后查找该module的依赖后返回的。

在build中会调用doBuild去加载资源,doBuild中会传入资源路径和插件资源去调用loader-runner插件的runLoaders方法去加载和执行loader。执行完成后会返回如下图的result结果,根据返回数据把源码和sourceMap存储在module的_source属性上;doBuild的回调函数中调用Parser类生成AST语法树,并根据AST语法树生成依赖后回调buildModule方法返回compilation类。

<figure>[图片上传中...(image-d9240f-1545355486959-8)]

<figcaption></figcaption>

</figure>

loader-runner处理流程

runLoaders方法调用iteratePitchingLoaders去递归查找执行有pich属性的loader;若存在多个pitch属性的loader则依次执行所有带pitch属性的loader,执行完后逆向执行所有带pitch属性的normal的normal loader后返回result,没有pitch属性的loader就不会再执行;若loaders中没有pitch属性的loader则逆向执行loader;执行正常loader是在iterateNormalLoaders方法完成的,处理完所有loader后返回result;如下列是loader的执行规则。

Loader执行顺序:

|- a-loader `pitch`|- b-loader `pitch`|- c-loader `pitch`|- requested module is picked up as a dependency|- c-loader normal execution|- b-loader normal execution
|- a-loader normal execution
复制代码

Parser

在Parser类中调用acorn插件生产AST语法树,acorn不在本文的分析范围,有兴趣的可以去阅读一下;Parser中生产AST语法树后调用walkStatements方法分析语法树,根据AST的node的type来递归查找每一个node的类型和执行不同的逻辑,并创建依赖。

<figure>[图片上传中...(image-65a45d-1545355486959-7)]

<figcaption></figcaption>

</figure>

MiniCssExtractPlugin

如果在webpack中使用MiniCssExtractPlugin插件把css单独打包成文件,会在样式处理规则中配置MiniCssExtractPlugin.loader,当解析到css文件时,会首先执行MiniCssExtractPlugin的loader中实现的pitch方法,pitch方法会为每一个css模块调用this._compilation.createChildCompiler创建一个childCompiler和childCompilation;childCompiler控制完成该模块的加载和构建后返回。childCompilation中构建的module是CssModule,并且使用type='css/mini-extract'来区分。

<figure>[图片上传中...(image-67c88-1545355486959-6)]

<figcaption></figcaption>

</figure>

在seal中MiniCssExtractPlugin会根据module的type='css/mini-extract'的类型来区分是否css样式,进行单独处理,而其他js模版不认识type='css/mini-extract'类型的module也就被过滤掉了,这样就实现了样式分离。

小结

在所有的资源bulid完成后,webpack的make阶段就结束了,make阶段是最耗时的,因为会进行文件路径解析和读文件等IO流操作;make结束后会把所有的编译完成的module存放在compilation的modules数组中,modules中的所有的module会构成一个图。

<figure>[图片上传中...(image-5c684-1545355486959-5)]

<figcaption></figcaption>

</figure>

seal

在所有模块及其依赖模块 build 完成后,webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

在seal中首先会触发optimizeDependencies类型的一些事件去优化依赖(比如tree shaking就是在这个地方执行的),大家要注意一点是在优化类插件中是不能有异步的;优化完成后根据入口module创建chunk,如果是单入口就只有一个chunk,多入口就有多个chunk;该阶段结束后会根据chunk递归分析查找module中存在的异步导module,并以该module为节点创建一个chunk,和入口创建的chunk区别在于后面调用模版不一样。所有chunk执行完后会触发optimizeModulesoptimizeChunks等优化事件通知感兴趣的插件进行优化处理。所有优化完成后给chunk生成hash然后调用createChunkAssets来根据模版生成源码对象;使用summarizeDependencies把所有解析的文件缓存起来,最后调用插件生成soureMap和最终的数据,下图是seal阶段的流程图。

<figure>[图片上传中...(image-bb0170-1545355486959-4)]

<figcaption></figcaption>

</figure>

生成 assets

在封装过程中,webpack 会调用 Compilation 中的 createChunkAssets 方法进行打包后代码的生成。 createChunkAssets 流程如下

<figure>[图片上传中...(image-fbb725-1545355486959-3)]

<figcaption></figcaption>

</figure>

从上图可以看出不同的chunk处理模版不一样,根据chunk的entry判断是选择mainTemplate(入口文件打包模版)还是chunkTemplate(异步加载js打包模版);选择模版后根据模版的template.getRenderManifest生成manifest对象,该对象中的render方法就是chunk打包封装的入口;mainTemplate和chunkTemplate的唯一区别就是mainTemplate多了wepback执行的bootsrap代码。当调用render时会调用template.renderChunkModules方法,该方法会创建一个ConcatSource容器用来存放chunk的源码,该方法接下来会对当前chunk的module遍历并执行moduleTemplate.render获得每一个module的源码;在moduleTemplate.render中获取源码后会触发插件去封装成wepack需要的代码格式;当所有的module都生成完后放入ConcatSource中返回;并以该chunk的输出文件名称为key存放在Compilation的assets中。

<figure>[图片上传中...(image-ca3523-1545355486959-2)]

<figcaption></figcaption>

</figure>

seal产物

通过seal阶段各种优化和生成最终代码会存放在Compilation的assets属性上,assets是一个对象,以最终输出名称为key存放的输出对象,每一个输出文件对应着一个输出对象,如下图所示。

<figure>[图片上传中...(image-737465-1545355486959-1)]

<figcaption></figcaption>

</figure>

emit

最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项异步将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。

watch

当配置了watch时webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem(memory-fs 插件) 实例。

监控

当执行watch时会实例化一个Watching对象,监控和构建打包都是Watching实例来控制;在Watching构造函数中设置变化延迟通知时间(默认200),然后调用go方法;webpack首次构建和后续的文件变化重新构建都是执行_go方法,在__go方法中调用this.compiler.compile启动编译。webpack构建完成后会触发 _done方法,在 _done方法中调用this.watch方法,传入compilation.fileDependencies和compilation.contextDependencies需要监控的文件夹和目录;在watch中调用this.compiler.watchFileSystem.watch方法正式开始创建监听。

Watchpack

在this.compiler.watchFileSystem.watch中每次会重新创建一个Watchpack实例,创建完成后监控aggregated事件和触发this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime)方法,并且关闭旧的Watchpack实例;在watch中会调用WatcherManager为每一个文件所在目录创建的文件夹创建一个DirectoryWatcher对象,在DirectoryWatcher对象的watch构造函数中调用chokidar插件进行文件夹监听,并且绑定一堆触发事件并返回watcher;Watchpack会给每一个watcher注册一个监听change事件,每当有文件变化时会触发change事件。

在Watchpack插件监听的文件变化后设置一个定时器去延迟触发change事件,解决多次快速修改时频繁触发问题。

触发

当文件变化时NodeWatchFileStstem中的aggregated监听事件根据watcher获取每一个监听文件的最后修改时间,并把该对象存放在this.compiler.fileTimestamps上然后触发 _go方法去构建。

<figure>[图片上传中...(image-c53ccd-1545355486959-0)]

<figcaption></figcaption>

</figure>

在compile中会把this.fileTimestamps赋值给compilation对象,在make阶段从入口开始,递归构建所有module,和首次构建不同的是在compilation.addModule方法会首先去缓存中根据资源路径取出module,然后拿module.buildTimestamp(module最后修改时间)和fileTimestamps中的该文件最后修改时间进行比较,若文件修改时间大于buildTimestamp则重新bulid该module,否则递归查找该module的的依赖。

在webpack构建过程中是文件解析和模块构建比较耗时,所以webpack在build过程中已经把文件绝对路径和module已经缓存起来,在rebuild时只会操作变化的module,这样可以大大提升webpack的rebuild过程。

总结

刚开始读webpack源码时心中的万马奔腾,MMMP数不清的事件名、看不完的内部插件,各种事件之间调过去调过来;~~~就这样吧,~_~

作者:外星人180
链接:https://juejin.im/post/5c1859745188254fef232ead
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这篇关于webpack4源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509168

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。