坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

之前的文章中提到loader是在哪执行的,有兴趣的大佬可以去看下。

坏蛋Dan:webpack5流程分析4 - NormalFactory、loader、plugin相关

另外由于有些是在公司写的,有些是在家里写的,所以截图内容会有些不同,但不影响分析。

配置

正文

在进入loader的入口前,我们得知道loader是在什么时候在哪里被调用的。之前的文章里分析过了,在NormalModule_doBuild中执行,也就是module被转换成source的时候调用。并且是用了一个名叫loader-runner的包。

然后直接进入主题,先来看下代码,200多行,我们一点一点分析。

先来看下source是什么

调试工具里没法截图全部,所以这里只有一个``

  • thread-loader: 一个第三方包,支持多进程方式解析处理资源,奇怪的是我这边并没有

thread-loader | webpackwebpack.js.org/loaders/thread-loader/

  • plugin_1.default.NS: 这个plugin_1指的是VueLoaderPlugin,当你需要使用vue-laoder时,你需要在配置文件中加上这个plguin

来都来了,就顺便把这个VueLoaderPlugin给说了。

首先会判断你的版本,然后加载不同的文件,因为webpack5做了一些破坏性的更新,所以这里做了兼容处理。

一进来便是一堆rule相关的,把人都整懵了。但是你仔细看你会发现这些字段其实有些和我们在配置文件中写的module.rules里的rule一样,比如testincludeexcluderesolve等。

module.rules:大家应该已经很熟悉了,就不多说了。[1]

RuleSetCompiler:之前的文章中并没有分析这个,主要是流程上并不影响。但现在这里有的话就不放过了。官网并没有对这个类做解释,不过看名字就知道是一个rule集合的处理器。

先来生成的看下数据

先是创建一个名为rulehook,如果你不清楚tapable是什么可以去看一下我之前的写的文章,这里就不多说了。将然后将传入的pluginsapply。然后我们来看下被applyplugin做了什么,篇幅问题就不贴代码了。

先是将传入的plugins都执行,注册到hook中。然后我们来看下被applyplugin做了什么。

  • BasicMatcherRulePlugin: 先是监听ruleSetCompilerrule这个hook,被执行时先是判断自己的rule里面的字段(也就是property)是否还有没被调用的,有的话先将这个字段删除,然后调用ruleSetCompilercompileCondition方法,将这个rulePropertyrule对应的value转换为用于匹配的functioncompileCondition这个方法就不看代码了,简单的说下就是根据你rule的字段做判断,然后返回一个function用来验证你这个字段的rule,比如test字段在rule中的value对应的是一个RegExp正则,那么通过这个方法返回的会是一个(v) => condition.test(v),这里的condition就是test字段对应的value,一个正则。 接着将这个匹配用的function存入result.conditions中,这个result是一个集合,从ruleSetCompiler中来,具体是从哪来的,干啥的我们先不说,后面会提到。
  • ObjectMatcherRulePlugin: 和BasicMatcherRulePlugin做的事差不多,就不多说了。
  • UseEffectRulePlugin: 对useloader字段的判断,最终已function的方式存储到result.effects中,具体这么做的就不说了。等会看下数据即可。

暂时就分析到这里,我们先往下分析,之后会再次使用到这个ruleSetCompiler的。

首先,先是监听compilercompilation这个hook,在compilation被创建之后执行。然后监听compilationloader这个hook,在loader被加载被执行时,将loaderContext.vue-loader = true。这样做是为了确保这个插件一定在vue-loader之前执行,如果不是,vue-loader会报错。

然后便是遍历你配置的规则,如果规则是eslint的就不处理,查找这个规则是否符合$.vue或者$.vue.html的规则。如果遍历完毕没找到符合的规则就报错。

接着判断这个规则是否有vue-loader字眼,如果没有则报错。

然后clone除了vue的规则之外的所有规则,并加入了一些自定义字段,对比原来的rule,每个rule中多了resource、resourceQuery两个字段。

来看下cloneRule方法做了什么

一上来就是执行ruleSetCompiler.compileRule这个方法,正好我们顺着这个方法把ruleSetCompiler做了什么给说了。

进入到compileRule方法中。

做的事情非常简单,就是调用rule这个hook

而这个hook将触发所有监听这个hook的事件,也就是说这个hook将把前面注册的一堆plugin都走一遍。这个时候你应该已经发现了,这些plugin和这个ruleSetCompiler的关系了,并且应该也猜到这些个plugin具体是做什么用的了。是的,这些所谓的plugin就是把你写配置文件中module.rules中每个rule里的有的字段,比如testuse等。而ruleSetCompiler收集这些字段,等到rule被传进来后走一遍这些字段的判断,最终转换成result这个集合。

所以我们直接来看下这个方法执行后的数据。test等用于匹配判断的将被放到condition中,而useloader等用于处理的则会被放到effects中。

总结一下rulePluginruleSetCompiler

rulePlugin拓展module.rules中每个rule可用的字段,而ruleSetCompiler用于加载这些plugin并将每个rule都过一遍这些plugin转换成一个带有conditionseffects格式的集合。


接着我们回到cloneRule中,重写除了enforce处理之外的ruleuse字段,删除loader、options字段。接着给rule新增两个字段,前面提到过的resourceresourceQuery

这段代码非常重要,所以单独拉出来了。

利用闭包在resource执行时缓存resourcescurrentResource中。然后在resouceQuery执行时将这个数据传入ruleResouce方法中。而resourceQuery方法则是先将请求的路径参数从字符串转换成对象,然后判断是否带有vue字段,如果没有则return false,如果有的话先确认是否是符合vue文件的请求,不是则return false。如果是的话将请求的路径改写为resouce.query.lang的格式。比如请求的是?vue&type=style&index=0&id=4a49fab7&lang=css,那么在转换后的路径会是xxx.vue.css。接着是调用闭包缓存的ruleconditionfn来判断是否匹配到了。如果没匹配到则return false

这一段代码处理的非常巧妙,首先利用了闭包缓存这个ruleresources,然后通过判断传入的query是否符合vue请求路径,不符合return false, 只有最后符合这个rule的规则的vue资源请求才会被执行。

比如当前是/$.css/的规则,那么对应的loader则为style-loader、css-loader。如果此时请求的资源是普通的css文件则return false,而如果请求的是vue?lang=cssreturn true,因为我们要处理的只有vue相关的请求资源,并且对应rule对了才会return true。这样就完美的避开了因为改写其它rule带来的问题并且自身的代码也能被这些loader处理。

然后我们再回到cloneRule中,删除test字段后返回Object.assign之后的rule

总结一下cloneRule做了什么

  1. 调用ruleSetCompiler.compileRulerule过一遍所有注册了的rulePlugin,转换rule的字段,具体可以往回看。
  2. 改写原rawRuleuse, 将loader等字段的value合并到use中。
  3. Object.assign原来的rule,然后新增两个字段,简单的说就是为了能让vue相关的请求用正确的loader,具体分析请往回看。
  4. 移除test字段,这个不清除干啥的。。。
  5. 如果这个rule中带有rulesoneOf的字段则递归cloneRule处理rule.rules/oneOf

接着让我们回到VueLoaderPlguin

如果你也调试中,你会发现template的请求是不会带有lang的,也就是说并没有loader能处理vuetemplate资源。所以这里加入了对模块的处理的ruleloader

这个TemplateLoader就不看代码了,因为涉及到vue/compiler-sfc这个包,这个是准备后面开个文章分析学习的,所以这里简单的说下就是loadertemplate模板代码变成ast的形式,然后再转换为浏览器能识别的runtime

接着回到VueLoaderPlugin

这一段针对的是对js/ts文件有匹配rule, 有趣的是jsRuleCheck是判断type是否是template

结合注释for each rule that matches plain .js files, also create a clone and match it against the compiled template code inside *.vue files, so that compiled vue render functions receive the same treatment as user code (mostly babel)

我们可以知道这是用来处理render function的,让render function得到和开发者的普通js的同样待遇。因为render function并不是最开始就有的,而是template通过templateLoader转换成的,所以这里需要特殊处理。

又一个rule,结合注释1global pitcher (responsible for injecting template compiler loader & CSS post loader)注释2This pitching loader is responsible for intercepting all vue block requests and transform it into appropriate requests.猜测这里面做了两件事,一个是调整loader顺序,另一个是拦截vue相关资源的请求,改写为可以被loader识别的。

我们来分析下这个pitcher

先是获取loaderidentifier,如果loader自身不是字符串形式就拼接它的pathquery

接着判断这个query的类型是否是style的,如果是,先找到css-loader的位置,然后判断是否是inline的样式,是的话

  • style-post-loader:

结合代码和注释This is a post loader that handles scoped CSS transforms. Injected right before css-loader by the global pitcher (../pitch.js) for any <style> selection requests initiated from within vue files.。我们能推测出这段代码是用来给sfc中的scoped css添加scoped id的,确保样式作用的范围是正确的。

  • stlye-inline-loader: 转换成esm

接着我们回到pitcher.js中继续往下看

最后这个方法用来改写请求路径和确保都能export出去,尤其是`template``的。

那这个loader就讲完了,我们来总结一下pitcher

  1. 如果这次请求的是css的文件,那么就改写loaders,在执行css-loader前先执行style-post-loadersfc scoped css加上scoped id确保样式只作用于这个scoped中。如果是inline css,那么就不执行style-loader、css-loader等。在执行完style-post-loader后执行style-inline-loader,将资源export出去。
  2. template资源的引入方式,确保能export出去。改写request路径,将loader的路径和ruleSet中的位置以及request资源路径拼接到一起。

接着让我们回到VueLoaderPlugin中接着往下看。

最后是重写compiler.options.module.rules

来看下这个重写后的rules ,加了一些“加料”的rule

总结下VueLoaderPlugin做了什么

首先将vue-loader的状态变为true,这样在调用vue-loader的时候就不会报错,这样做是为了确保VueLoaderPlugin的执行先于vue-loader。接着判断是否有vue-loader这个loader,没有直接报错。 然后复制一份不包含use vue-loaderrules,给他们加resouceresourceQuery两个字段,其中resource是用于缓存request source,而resourceQuery则是一个方法,利用闭包缓存当前的rulesource,判断这里面请求的是vue相关的资源并且资源能被这个rule匹配到,这样做确保vue相关的请求资源被vue-loader处理后能被其他loader也处理了。 然后注入templateLoader用于处理vuetemplaterender function。 接着又copy了一份和处理js/ts资源的loader相关的rules,这样做是为了template通过templateLoader转换得到的render functionjs/ts代码能被这些loader也处理了。 然后又注入pitcher这个loader,用来处理两个问题。

  1. 通过在css-loader前注入style-post-loader来给vue sfc scoped css增加scoped id来处理vue sfc中的scoped css资源作用域问题。如果是inline css资源则不再调用css-loader以及css-loader之后执行的loader,相反,使用style-inline-poster将这些inline css变成esm资源,export出去。
  2. 改写request路径,将loader的路径和loaderruleSet中的位置以及request资源路径拼接到一起。 将template的资源export出去。 最后覆盖compiler.options.module.rules 具体分析请往回看。

总结完VueLoaderPlugin后我们回到vue-loader中继续往下看。

这一段都是在初始化一些数据,我们就不分析了,直接看下重要的几个字段

  • loaderContext:这个就不看了,就是_doBuild中将compiler、compilation等合并到一起的一个对象
  • filename

  • descriptor

这一段代码中将.vue文件源码字符串拆分为templatescript以及style 。至于里面怎么做的我们之后的文章会分析。现在先继续往下看。

这一段很简单,就不看descriptorCache的代码了,就是将这个解释后的descriptor[filename]: descriptor的格式缓存到全局Cache这个集合中。下次获取如果存在则直接get,不存在则再执行一次parse

这一段很简单也很重要,这里id就是scopedid,现在我们知道了这个是根据文件路径甚至加上文件源码生成的hash值。

先是判断是否是typescript,判断script标签中是否带有lang。然后拼接请求路径,scriptImport则是一段runtime

script一样,都是判断场景然后生成请求runtime

css这个自然也是一样的操作。

  • data-v-${id}:大家应该都很熟悉了。

这段结合注释可以知道是和vue-devTool相关,就不多说了。

如果你的文件中加了一段不是script、template、style的奇怪标签,那么就会被这里捕获到,一样会被导入。但是除了function之外都不会执行。

最后注入了一段runtime,让这些属性能被注入到components中。

/#__PURE__/: 通知webpack这个是"纯净的",是可以tree shake掉的。

这一段是和热更新有关的,来看下这个hotReload.js,先看下templateRequest

templateRequest

这是一段runtime,到时会被带上浏览器,现在我们看的不是很懂,后面会开个文章分析,所以这里简单的看下代码即可。 而这段runtime最核心的一段就是api.rerender 这里。

将组件导出去。

最后看一下完整的componentsruntime

当浏览器请求这个资源的时候才会执行。

最后return出去。

总结一下vue-loader做了什么。

简单地说就是先调用compiler-sfc包将.vue文件切割成template、script、style、custom四部分,然后分别拼装请求路径(path + query)。再加上一些其他runtime,最终组成一个componentsruntime被带上浏览器中,等待调用执行。

VueLoaderPlugin前面总结了,这里就不说了。

这里还有两个点没有分析

  1. compiler-sfc
  2. hotReload这段runtime

这两个准备后面开个文章分析。

最后如果对大佬你来说有点用的话麻烦点的赞,谢谢!


补充

上面说到的hot-reload相关的得等到分析vueruntime包才会涉及到。

参考

  1. ^module.rules https://webpack.js.org/configuration/module/#modulerules

编辑于 2023-01-24 21:25・IP 属地广东