webpack原理,webpack源码解析,超详细

文章最后有webpack流程图,建议先看代码,再看流程图,不然容易厌学

webpack-cli通过process.argv内容以及webpack.config.js文件内容构建compliler对象,其中process.args是node提供的全局变量,其中你在执行webpack-cli时输入的命令行相关配置,webpack在打包过程中会通过这个全局变量内部的命令行,进一步修改相关配置,再通过这些修改后的配置创建compiler对象,webpack.config.js就是我们进行的一些配置

讲完一些我们看不到的,下面来说webpack源码

第一阶段:创建compiler对象

webpack的成功执行需要webpack-cli调用webpack函数,而webpack函数内定义了create函数

create函数首先对配置进行校验,然后通过判断options是否为数组来判断是调用createcomplier还是调用createmulticonpiler函数。具体如下图

webpack函数会根据webpack-cli是否传入回调函数判断如何创建compiler,以及判断watch判断是否需要热更新,但是不管如何,最终都是调用create()

首先options是什么?

他就是我们经过一些列处理得到的配置,但是配置不应该是对象吗,怎么会是数组呢?

这是因为在多编译模式下(后面详细讲多编译模式),会出现多个配置以及多个complier对象,这也解释了为什么当判断options为数组时要调用createmulticompiler了,因为我们要创建多个compiler对象并将其组合成一个compilers数组,对于多个compiler对象,我们想使其并行编译,还需要一个管理工具multicompiler,createmulticompiler函数通过遍历options数组并且调用createcompiler(创建单个compiler对象的方法,下面说,很简单,这里就是对其遍历调用,创建多个compiler对象组成数组)创建compiler数组,并且创建了nulticompiler管理工具

如下图

如果没有开启多编译模式那么我们只会有一个option对象,并且创建一个compiler对象,所以只需要调用createcompiler即可

其中createcompiler函数内部包含了处理配置,以及创建compiler对象的过程,如下图

从图中我们可以看到,在创建compiler过程中,createcompiler函数还会将plugin进行注册,如果plugin为函数,他还会将compiler对象作为参数传入,这很重要,有助于我们自定义插件,并且代码的最终打包靠的就是compiler上的run方法

我们的相关配置还会通过webpackOptionApply对象的process方法,赋值到compiler对象上,并且该实例化对象上还存在一些if判断,该实例化对象通过判断compiler对象上赋值的我们的配置,将我们的配置通过插件的形式调用,如下图

途中我只截取了output,除此之外还有一堆if(真的很多,不是一般的多)。

到这,我们创建compiler对象的过程就完成了,那么还有一个问题,什么是多编译模式,什么时候会开启多编译模式?

多编译模式(Multi Compilation Mode)是指在 webpack 中同时处理多个编译任务,每个编译任务对应一个入口配置。简单点说,当我们配置不止一个入口文件时,那么每个入口文件都是独立的编译任务,此时便会给每一个入口分配一个compiler对象,这就是多编译模式。

第二阶段:创建compilation对象

成功创建了compiler对象后,我们初始化阶段就完成了,接下来进入主题,webpack-cli会进一步调用compiler对象上的run方法来进行打包

run函数内部定义了很多函数,但是我们先不看,首先记住这个finalCallback

这段代码是run函数的末尾,很明显代码走到这步之后要继续执行了,但是走哪一路呢?于是我开始找idle,发现在finalCallback发现了这个变量,并且我发现这个函数被大量用于返回错误,所以我猜测,他一定是走else这条路了,因为只有在finalCallback中才存在这个变量,而只有发生错误时,才会进入该函数。

而这个里面执行的run函数可不是compiler对象的属性,而是compiler上的run函数内部定义的run函数,如下图

看到这里,我要讲一下callAsync以及tapAsync这两个函数了,不然大家会很疑惑,首先tapAsync是用来注册异步事件监听器的,它接受两个参数:事件名称和回调函数。而callAsync就是触发这个监听器的,传参分别为给tapAsync的参数以及自己的回调函数,那事件名呢?事件名是被放在前面的,也就是图片中的beforerun,run等。

然后我们在说回代码,这段源码中beforeRun的事件监听器做了一些编译前准备工作,如清楚输出目录,准备环境等,而run事件监听器做了一些初始化工作,如输出进度信息,记录初始事件,初始化数据等,不管怎么样,最后都会运行到compile函数,这才是重点,而其中的参数是一个函数,他的作用我们后面再说。

进入到这个函数我们发现,我们终于来到了创建compilation对象的环节,在compile函数内通过newCompilationParams函数创建,此函数在上文定义,图中已标注。

为什么又来了一个compilation对象,他是干什么用的,跟compiler有什么区别?

首先compilation对象是用来编译代码的,那我们要compiler干嘛呢?编译代码需要一个合适的环境啊,除了编译代码的环节,其他的如解析更改配置文件,以及将我们写的配置换成对应插件,以及注册钩子(就是我们之前说的tapAsync)以及输出文件等等,基本除了转换代码的活,都是他在做,而compilation对象的作用仅仅是转换代码,所以创建了这个对象,也意味着我们进入了转换代码的阶段了。

这就是compile的完整函数,很多,很陌生,但是并不难,这个函数的意思其实就是上个异步钩子函数执行完,回调时调用下一个异步函数勾子,在函数最开始还进行了创建compilation的操作,之前已经说了

首先是beforeCompile钩子函数,这个钩子函数就是做一些解析前的准备工作如清楚目录,修改编译配置等,这里就不细说了,我们直接进入下一个

第三阶段 :优化和构建依赖关系

接下来是make钩子函数,在beforeCompiler没有报错的情况下其回调函数会成功调用make钩子函数,那么让我们看看make钩子函数做了什么。

这个钩子函数没办法ctrl点击进入,我们只能自己寻找,这是在EntryPlugin.js文件下EntryPlugin插件内的apply函数,函数内部通过EmtryPlugin.createDependency函数创建map对象保存所有依赖关系(后面构建模块树用),compilation对象编译代码需要从入口文件开始,所以编译的钩子函数也被定义在这里,它内部执行了另外一个compilation对象上的addEntry函数,让我们来看看里面是什么

可以看到addEntry调用了compilation身上的私有方法(可以看到上面的上面的图中是通过compilation对象的属性调用的addEntry,所以addEntey是compilation上的方法,所以this也只想compilation,事实上compilation上也确实有这两个方法,这也证明了compilation对象就是用来编译代码的),那么让我们继续进入到这个方法内(此时entry就是我们上一层获取的依赖关系,webpack多次传参,不断改名,看时一定要注意,不然很容易看了后面忘了前面,webpack源码真恶心)

注意:每次获取的依赖关系都是本次构建的文件,和该文件的依赖,并不会将所有文件的依赖关系都找到,webpack会一层一层的循环,直到所有文件被打包完成

里面代码很多,我进行了删减,留下最重要的一个,这是compilation对象上的addModuleTree方法,目的是要构建依赖树,内部代码如下(往后的传参内容大概都是,内容,依赖以及配置这三个)

在这里我们拿到了之前保存的入口依赖并将传进handleMoudleCreation将依赖树进行优化,并且转换为图结构(就问webpack官网上的图一样,互相依赖),内部代码如下

可以看到handleModuleCreation内部调用了factorizeModule函数,我们展开来看看

可以看到factorize函数调用时传入了一些配置(模块和相关依赖的信息)以及一个回调函数,回调函数内的这段代码是对依赖关系进行优化,提取可以共享的代码以及一个不知名函数的调用,那我们看看何时factorize执行这个回调函数,这是我们需要看到factorize函数的具体内容如下

很简短的代码,仅仅将我们的信息和回调函数传入了factorizeQueue.add函数进行调用

那么我们现在要去看看,factorizeQueue.add函数什么时候执行我们的回调呢

下面是add方法

从图中可以看到item形参接收了模块信息,经过一系列处理,又传给了回调函数。我们前面又说了,我们这个回调函数是通过moduleGraph和currentProfile两个实例优化依赖关系的,那么我想这些处理一定是将依赖关系处理成moduleGraph和currentProfile两个实例能解析的结构。

回调函数被推入队列,代码正常执行,factorizeModule函数内部优化完依赖关系后,看似是调用addModule函数我们展开看看

addModule的调用依旧是第一个参数是模块信息,第二个参数是回调函数,并且回调函数内有有一个函数的调用,不同的是这此回调函数执行的功能是将依赖关系转化为依赖图结构。我们进入addMdoule代码内部,看看何时调用回调

回调函数执行时有会通过addModule方法再次插入新队列,如下图

我们发现和factorizeModule的代码非常像,我们再点进addModuleQueue.add中看看

和factorizeModuleQueue.add完全一样,看来这是通过原型链方法调用的同一个add函数,目的都是将函数推入队列,以及处理模块信息交给moduleGraph和currentProfile两个实例处理,并且addMdule函数是在factorizeModule的回调函数中,这样保证了在factorizeMdoule在出队列执行时,addModule函数被插入队列,这样可以让依赖信息先进行优化,再转化为图结构,而factorizeModule和addModule函数内部都只有将回调推理额推入队列的功能,其真正的优化功能是在回调函数中,但是为什么要推入队列呢,明明通过回调函数已经可以实现按顺序调用函数的功能了啊。

我猜测是因为推入队列时,我们用到的实例,我们发现推入队列的方法add都是同一个函数,但是为什么还需要不同的实例(两次调用add方法分别是使用factorizeQueue和addModuleQueue实例)来调用呢,因为实例内部有一个processor的属性,此属性的值为一个私有方法,这个私有方法会在实例被使用时执行,而实力的方法,并没有被推入队列,所以我想webpack应该是想先执行这个私有方法,再去执行回调函数的内容,这个私有方法,在我们推入队列是用到的实例对象中都有,比如addModuleQueue,factorizeQueue以及buildQueue,其中buildQueue的processor十分重要,打包过程都在其中,后面会说到。

这些就是我们调用add方法是用到的实例对象,找出来让大家更直观的看到。然后我们继续说

将依赖关系处理为图结构之后,addModule的回调函数调用了_handleModuleBuildAndDependencies函数,我们将其展开来看看

展开发现,是一个普通的函数调用,模块的相关信息,被传进去。

从名字可以看出来,这个是跟我们代码打包相关的函数,所以也预示着我们进入第三阶段

第三

四阶段:代码打包

那让我们看看这个函数做了什么功能

可以看到,这个代码需要判断一些东西来判断是都执行代码(俩面的代码是进行一些初始化配置),而判断外有一个函数被调用

可以看到,这个函数的结构跟fostorizeModule和addModule两个函数很像,这个函数的参数仍然传进一个module信息以及一个回调函数,回调函数内依然调用一个名叫processDependencies函数

进入函数后发现依旧是推入队列,但其回调函数并没有实现什么功能,而功能则是在buildQueue这个实例上,让我们看看这个实例的内容

其他就不介绍了,其中processor的意思是处理器,再推入队列时,这个函数被调用(addMOdule以及factorizeModule也有这个实例,并且也有对应的processor,但我看不懂他到底做了什么,也没有文献解释)

注意:到此为止,后面的类似于factorizeModule和addModule这样前面传递参数,后面回调函数的函数,都没有被推入队列,这也再次验证了我之前的猜测,因为往后的代码都是在执行buildQueue实例上的processor内的方法,并没有在使用其他实例,并且目前buildModule的回调函数,仍在队列中未执行。

进入后发现,此函数调用了module.needBuild,值的注意的是,此方法时module对象上的,而module记录了所有模块相关信息,再结合函数名字可以推测,模块要开始构建了,我们在进入needBUild方法中

此函数直接执行回调函数,让我看看needBuild调用时传入了什么回调函数

此函数做了一些准备工作,根据needbuild调用此回调函数时传入的needBuild参数,判断是否需要构建,一旦发现需要构建,则需要调用module.build函数开始构建

我们发现依旧是这种形式,两个参数,第二个是回调函数,我们依旧进入这个函数,看看他在做什么,不出意外的话依旧是函数的调用

build依旧在做准备工作,并调用doBuild

doBuild函数依旧是传入一堆信息以及一个回调函数

doBuild不仅回调函数内容多,本身内容也巨多,其中内容主要为通过new LazySet()建立懒加载合计,保存依1赖,内容以及加载方面相关依赖(如记载其插件等),其中cacheable表示能否进行缓存,然后开始执行runloader执行所有loader

runloader函数不仅是信息参数,加回调函数参数的形式,而且信息参数内还有一个回调函数在runloader的回调函数内被回调,让我们先看看runloader如何处理代码

点进去发现是模块引入,进入模块内部继续寻找

找到后代码内容非常多,总之他是通过loadercontext上面的属性对代码进行处理,处理完之后再交给iteratePitchingLoaders在进行处理,处理完后再通过iteratePitchingLoaders调用runloader的回调函数,那么我们们重新回来,看看runloader的回调函数做了什么、

我们发现这个回调函数,把我们处理之后的代码赋值给懒加载合集,并且调用processResult,我们可以在_doBuild函数内部找到这个函数

我们可以发现,processResult对结果进行了一些处理。source应该就是处理之后的代码了,最后调用了一个callback,而这个回调函数是_doBuild传入的

来到_doBuild调用的地方,我们发现其传入的回调函数进行了一些错误判断,然后,调用了handleParseResult函数,对结果进行再次处理

可以看到handleParesResult函数处理完之后,handleBuildDone继续处理,处理完之后,再交给SnapshotOptions,最终调用callback,这个callback并不是我们肉眼能看见的,而是属于buildQueue实例通过new AsyncQueue实例化是传递给processor的,我们不知道它具体做了什么,但可以知道的是,到此为止,入口模块被处理完毕了,接下来会跳转到factorizeMoudule,也就是优化依赖那一步,重新开始处理入口模块依赖的其他模块。我们在建立模块之前会先通过needbuild方法判断是否需要重新构建,其实就是为了方式相同依赖重新构建。直到所有模块都被构建完成后,才会跳出循环。这次跳出循环标志着整个代码的构建已经全部完成,但还记得我们是如何开始构建的吗?

没错,我们是通过callAsync函数触发tapAsync异步监听函数,如今这个异步监听函数,已经完成了任务,那么callAsync中的回调函数要开始执行了。

make.callAsync回调函数调用了finalmake.callAsync,我们这这里不过过多讲解,直接看最重要的,finalmake.callAsync回调函数中的seal函数,其中内容众多,我没办法截图下来,我直接介绍他在做什么

seal意为密封,和他的名字一样,seal会创建chunk也就是块,为我们生成的最终打包文件分成各个模块,生成最终代码,seal执行完毕后,finalmake.callback的回调函数中,还执行了afterCompile.callAsync

从名字就可以看出,我们对打包代码的构架已经彻底结束,最终还调用了一个callback那么这个callback又有什么作用了,也只剩一个作用了

可以发现compile在run函数中被调用,oncompiled就是这个回调函数

物品们关注其中最重要的emitAssets,这个的功能是创建文件夹输出文件打包正式完成。

下面是我自己整理的流程图

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Vue 和 Webpack 是开发现代前端应用程序的两个重要工具。Vue 是一个用于构建用户界面的渐进式 JavaScript 框架,而 Webpack 是一个模块打包工具。 Vue 的核心思想是将界面的不同部分抽象为组件,每个组件有自己的模板、样式和逻辑。Vue 在运行时会解析组件,并且通过虚拟 DOM 的方式进行高效的渲染和更新。 Webpack 则是一个用于打包模块的工具。它可以将各种类型的资文件(JavaScript、CSS、图片等)作为模块进行处理,并将它们打包成静态文件,以用于浏览器端的加载和运行。 在 Vue 项目中使用 Webpack,通常会通过 webpack.config.js 文件进行配置。在配置中,我们可以定义入口文件、输出目录、加载器(Loaders)、插件(Plugins)等。 入口文件指定了 Vue 应用程序的入口点,通常是一个 JavaScript 文件。Webpack 会从这个入口文件开始,分析依赖关系,并且构建整个应用程序的依赖图。 加载器是用于处理各种类型文件的模块转换器。例如,Babel-loader 可以将 ES6+ 的 JavaScript 代码转换为兼容各种浏览器的 JavaScript 代码;CSS-loader 可以处理 CSS 文件,并将其转换为 JavaScript 对象;File-loader 可以处理图片文件,并将其转换为可在浏览器中使用的 URL。 插件是用于扩展 Webpack 功能的工具。例如,Vue-loader 插件可以将 Vue 的单文件组件(.vue)转换为 JavaScript 代码;HtmlWebpackPlugin 插件可以自动生成 HTML 文件,并将打包后的静态文件自动引入到 HTML 中。 总结起来,Vue 使用 Webpack 实现了模块化的开发方式,通过加载器和插件的配合,可以实现对各种类型文件的处理和打包。这样就能够以组件化的方式构建 Vue 应用程序,并且通过 Webpack 进行打包和优化,使应用程序更加高效和可维护。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不止会JS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值