Webpack打包流程细节源码解析(P1)

说在前面:

这些文章均是本人花费大量精力研究整理,如有转载请联系作者并注明引用,谢谢

本文的受众人群不是webpack的小白,仅适合有一定基础的前端工程师以及需要对webpack进行研究但是在阅读源码的过程中有些小的细节不明白时进行查阅理解使用,学艺不精,文章中很多地方可能有理解上的问题希望在评论区指正

首发于个人博客 https://github.com/879479119/879479119.github.io/issues/1,欢迎star

基本概念

webpack整个系统中的核心组件基本都是继承自Tapable这个class的,内部维护一个插件对象,key是我们指定了流程中会运行的钩子事件名,value则是一个列表_plugins数组,里面存放着在key的对应时间能够以特定的方式执行的插件们。这里所说的特定方式可以是同步,异步,高阶函数,bail等等,各个地方的用法各有不同。

我们的插件主要通过自身的apply方法插入进去,同时因为核心组件都是Tapable的,所以可能会在apply中继续看到apply方法,就不用惊讶了

Webpack启动

webpack首先利用optimist对我们的命令行参数进行格式化处理,得到的对象可以拿到很多参数,然后与我们的webpack.config.js进行合并,当然webpack其实还默认支持别名的配置文件「webpackfile.js」,这两个配置文件都是直接可以通过node自身的require进行引入的,不要把她们和webpack的打包中的require搞混淆了

启动过程

通过创建webpack实例过后,我们一般将得到的对象命名为compiler,可以对她进行run操作,进行此操作过后的对象便会开始下面那张的图中的构造过程,下面我将进行尽可能详细的描述,便于读者在开发过程中对于整个插件系统有完整的了解

webpack

进行run操作后,开始了一系列的准备工作,这个时候

before-run

首当其冲的执行,进行了对于文件系统的设置,inputFileSystem是我们手动设置的,如果对于平时的构建操作,她会选用普通的文件系统,而对于使用dev-server的时候来说,其中使用到的dev-middleware就是一套实现了对应文件系统接口的库,其内部使用了memory-fs这个库进行对内存中的虚拟文件进行管理的操作。

webpack在很多地方参考了rollup,但是rollup却没有选择文件系统这样的操作,只能读入固定的文件系统,而写出的时候我们能够拿到代码,当然是可以写到内存里面也是没毛病

run

然后进入run流程中,这个时候第一个插件就开始工作了,不过为什么我没配置插件还有插件进行工作?之前也说到compiler对象是一个Tapable对象,自身不会带有太多执行逻辑,更多的是表明了业务流程,所以其实这里的插件是我们的内置插件—CachePlugin,她和我们整个系统资源缓存相关的,还有一个RecordId也是和缓存相关的

进入此阶段检查缓存过后(实际上是通过stat查看某个位置的文件是否存在),会调用readRecords的方法检查之前的记录recordsInputPath是否存在,由于是第一次编译所以当然是没有的

检查完成两项缓存正式进入compile函数进行编译阶段的处理

compile函数

这个阶段中最重要的对象就是compilition了,但是构建她需要一个参数,我们会在进入compile阶段时通过newCompilationParams方法给她提供相应的normalMoudle和contextMoudle等的上下文工厂函数,还有一个重要的compilcationDep存贮相应的依赖便于以后收集依赖进行打包以及代码处理(整个打包过程会有大量的Dep关键词出现,每一次都是不同的依赖分析,但是最终还是会被放到一个名叫dependencies的数组中,另外其实入口模块的依赖就是她自己,相当于作为了打包起点)

WX20170910-213840@2x
WX20170910-213840@2x

工厂函数的构造过程中,将rule的匹配设置以及loader处理等全部拿给了这些工厂函数进行存储,设置为对应的ruleSet对象(之前我找了好久没找到,原来藏在这里),这个ruleSet和我们之后的loader处理密切相关

normal-module-factory

进入这个事件,传进去的修饰参数竟然是nmf(normal-module-factory),现在还没有构造compilition,同时因为她也是继承自Tapable,所以有一些插件也会被插到nmf上面,另一个context是没有编译实体的,仅提供一个上下文环境。

上面可能说的有点糊,理一理,我们都知道webpack进行打包的时候会找到指定名字的文件然后添加依赖打包进去,但是这是知道明确文件名的时候做的操作(normal)。如果不知道具体的文件名,比如require('./module'+number)的时候,webpack系统实际上不能确定你用了哪一个,只能够把可能用上的包全部打包进去,这个时候会用到的就是context进行处理

说了上面一圈有什么用?实际上如果我们需要打包某个文件下的所有以jpg结尾的图片,就可以使用这种方法或者是require.context进行处理,全部打包进去,详情就看文档吧

before-compile,compile

平淡无奇没有操作

创建complication

创建了新的对象,同样是一个插件系统对象,所有的插件键值都存储在_plugins属性中

this-complication

由于终于出现了complication对象,就算没有什么要做的但是需要尽早注册自己事件的插件就在这个阶段登场注册了,比如我们的CommonsChunkPlugin直接给优化阶段注册了一个就走了

然后是JSONP相关的插件,她主要处理的是异步加载脚本的时候动态添加script标签的事情,她也只是把插件插到了template上面,而不是compilation,这里绑定的处理大都会在最后打包的时候用到,所以hot-chunk其实和chunk区别不大,主要体现在加载方式不同,包裹不同而已

WX20170910-221030@2x
WX20170910-221030@2x

complication

根据我的配置,这个阶段足足有38个插件,其中大部分还是来自wepack自身,由于使用Tapable带来的高度解耦,让人感觉webpack的核心是空的,只是一个走流程的架子,完全没有实际的业务逻辑

  1. 模块HMR插件,对compilation对象上的factory做了一个Map,把每种模块对应的工厂函数存了进去,也对之后的mainTemplate等封装的处理和代码parse的处理过程进行了注册
  2. DefinePlugin的处理也是集中在parse的阶段,进行替换
  3. HashModuleId,使用set记录已经使用的id,避免重复
  4. LoaderOption,为normal-module-loader步骤注册,帮助loader解决设置问题
  5. FunctionMoudle,最重要的就是添加上我们平时见到最多的commonjs的模块封装方式,分为render和package两个部分工作,第二部分主要是统计有些方法有没有被用到,应该是方便过后优化的注释
WX20170910-234440@2x
WX20170910-234440@2x
  1. NodeSource,主要是对node环境的一些表达式做特殊处理,比如global,process这样的
  2. LoaderTarger,修改target,没了
  3. EvalSourceMapDevToolPlugin,在moduleTemplate上面添加新的插件处理sourceMap
  4. MultiEntry和SingleEntry,这两个都是很关键的入口插件,此阶段仅仅是在之前提到的map上面设置好对应的nmf,方便过后使用
  5. CompatibilityPlugin,主要是做兼容处理,比如json-loader在2版本之后就不需要手动引入了,其实就是组件本身可以在这里开启引入进来的,在她的parse阶段也会对require进行处理,我们之后再说
  6. HarmonyModulePlugin, 在map中添加了大量处理import和export等的模板,另外还会在parse中注入几个插件,是处理import这些语句用的
  7. AMDPlugin,CommonJs和上面的类似,她处理的是AMD形式引入的模块
  8. LoaderPlugin,非常奇葩的两段式,没找到原因
  9. NodeStuff, 设置__dirname这种node环境下的变量,进行替换
QQ20170915-115623@2x
QQ20170915-115623@2x
  1. RequireJsStuff, requirejs相关变量配置,如require.config

  2. API,配置相关缩写,如果有需要可以直接在代码中写这些接口进行使用,同时处理后的代码可以根据这个进行比对

    WX20170911-005110@2x
    WX20170911-005110@2x
  3. ConstPlugin,处理if语句和?:语句以及被设置为__resourceQuery的地方(这个东西她是在我们的require请求后面带的那一堆query参数require('./a.js?sock:8000'),比如我们在使用devServer的时候会加上上请求端口等,打包的时候就可以通过她获取到)

  4. UseStrict, 由于use strict只能放在脚本最前面才起效,并且ast中解析出来的comment是放在别的位置的,所以把旧的use strict杀掉,过后统一一起加上

  5. RequireInclude,简直鸡肋,没有什么实际用途

  6. RequireEnsure,人如其名,会在parse阶段加入插件进行处理

  7. RequireContext,可以用来加载一整个文件目录下的资源呢,不过还是主要用在了contextModule上面

  8. ImportPlugin,用做处理System.import,还有import-call?什么鬼

  9. SystemPlugin,向外提供System这个屌对象

  10. EnsureChunkCondition,利用set进行筛选,清理输入模块中重复的部分,这里的ensure是确认,不是require.ensure里面那个

  11. RemoveParentModule,em。提取公共module,过后会讲到

  12. RemoveEmptyChunks,正如名字一样

  13. MergeDuplicateChunksPlugin,合并重复的chunk

  14. Flagincluded

  15. Occationaly

  16. FlagDep

  17. TemplatePath,过程中可以拿到结果的资源path和hash值,这对于html插件来说是很有意义的

  18. RecordId,

。。。。。。。

make

首先是html-webpack-plugin

html的插件会在这里执行,我们就可以跟着她进行分析了,需要注意的是她把catch放在了then的前面,只是处理前面的操作抛出的错误,更有针对性

runAsChild过后没有相应的事件处理了,其实也就是在子compiler中执行了compile方法,开出来的新的异步执行空间,具体请去看插件之中的源码

这里会createChildCompiler产生一个新的compiler,作用是能够得到父级的loader设置等等(issue说的)

同时也在原来的基础上增加了一些插件,比如比较重要的new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var'),还有改变了平台成为node,之后在代码进行处理的时候会根据这个名字再把她消除掉

WX20170911-020009@2x
WX20170911-020009@2x

html插件通过向外面提供一些「接口」,也可以被定制,我们早就说过,这些东西都是插件,现在的子compiler也不例外,我们这里下面写的这个事件就是一个钩子,使用她的插件

WX20170911-081629@2x
WX20170911-081629@2x

make阶段只是添加了这个新的compiler和一些异步任务,真正的使用还在emit的时候

这里有一点关于WEBPACK的特点就是js 的在处理的时候得到的source是一堆js代码,并不是我们最终输出的html,这里把她拿到vm中去执行一下才能拿到我们要的东西(HTML字符串),毕竟子compiler也只是打包而已,得到的代码还是要执行才能有相应的结果

最终执行返回的结果还是null,只不过会在asset中注册上要新创建的html文件

nml使用的是和compiler里面不同的parser,规则也会有不同的地方

然后是single-entry-plugin

这个阶段会用到我们之前在config文件或者是手动配置时的entry参数,看看会不会是单个入口文件

细心一些才能发现,之前在html插件中,插入了一个single-entry-plugin,并且把入口设置成了我们之前的html或者ejs等模板文件,导致我们在第一次进入此插件的时候会发现是那个模板文件

通过之前的Map获取到相对应的moduleFactory来创建模块,这里得到的是一个nmf普通模块

现在出现了一个特别变量semaphore,她是一个信标,本来是在多线程中使用的资源控制使用的,(其实这里的资源说的是并行处理的数目)默认情况下是100的可用资源量,如果资源不足时就让过后的请求挂起等待,这里的acquire方法就去申请一个资源,然后进入了首个Module的create方法

factory(nmf)

nmf自身有一个处理函数被插入进去,并且ExternalModuleFactory在nmf上面插入了一个处理函数,

由于是两个连续的过程调用,通过函数式的处理返回了一个回调方法factory,直接使用factory对我们的ejs文件进行封装

external的模块都是nodejs的内部模块,这些模块不能拿来打包

QQ20170915-115623@2x
QQ20170915-115623@2x

然后是multiple-entry-plugin

这里出现了我们设置的第一个index,多入口文件,进入multiple开始进行处理,其实内部还是把multiple的每一项,转化成为了单独的SingleEntry,并且所有的id都是从100000开始计数的

然后通过Map获取到对应的moduleFactory是Multiple的,并开始执行create方法,create后会相同的执行addMoudle的操作,但是这次和之前的html有不同,因为她有明确的依赖关系,能够正确进入处理函数得到结果

如何获取module的id呢?module中的identifier方法会返回出表现当前module特性的字符串包含「multi」「path」等等,把她的索引缓存到_modules里面,如果下次又出现了了相同id的模块则直接退出

不过缓存又是放到另一个地方的,即cache里面,这里面缓存module的key就是在id前面加上一个m,或者是其她定义的缓存分组(这个机制暂时不知道有什么用),在添加进缓存完成后返回true表示成功添加新模块

buildModule

非常重要的一项操作,loader的应用和AST的解析都是在这一步进行的,可以说除去打包优化的所有重要功能都是在这里进行的

build-module的时候只是sourceMap的插件把useSourceMap开启了,然后没了

正式调用module的build方法,比较重要的是传递了设置和相关的文件系统

不过mutiple的处理很水,直接什么也没做就构造完成了,把built置成了true

处理完成一个模块后释放之前的标识semaphore,又回到了100个的默认值

现在才是真操作,processModuleDependencies将做递归操作,为每一个模块的依赖进行获取和处理

然后又进入了nmf的create函数里面,现在又是before-resolve,这次和ejs那次不一样,能够拿到的是nmf的plugin,不过这次能够执行操作了

factory是通过waterfall将插件组合成为一个函数对结果进行处理,resolver也是一样的道理将所有操作合成为一个函数进行处理

在resolver里面把request进行了拆分,得到最终的资源文件所在位置和需要使用到的loaders,并行的执行两步操作,一步是resolveRequestArray用于一个一个的检查这个模块上应用上了哪些loaders的扩展,一个接一个的进行执行

QQ20170915-171239@2x
QQ20170915-171239@2x

在进行实实在在的doResolve操作的时候使用到了一个操作栈的概念,每一次处理得文件会按照一定规律将名字缓存起来,放到函数上面作为属性保存;

QQ20170915-142845@2x
QQ20170915-142845@2x

最后进行runMormal做最普通的处理,然后添加了很多生命周期栈,应该说这里面的操作都是在为编译的实际过程做准备,并且做了一堆准备工作过后最后还主要是返回了package.json里面的东西回去继续异步处理

NormalModuleFactory.js:100

然后从第一个参数中提取到刚才我们分析出来需要使用的loader,再到ruleSet对象中跑一遍,先是exec然后_run方法,进行一个个的规则匹配工作,看看当前文件到底要使用哪些loader

这里需要分辨一下,这个地方的是按照webpack.config.js里面的规则,正则表达式来进行匹配的,但是我们说的上面的那一次resolver处理实际上是对类似于require('index.js?babel-loader')这种形式进行的操作

QQ20170915-175107@2x
QQ20170915-175107@2x

经过一系列的匹配我们知道了现在需要的只有babel-loader进行处理,所以返回一个result,如果我们有enforce的需要,会把loader挂载到对应的时机上面处理,但是实际执行的时候并行执行了三种loader

然后她又来到了我们之前执行过的resolveRequestArray方法,就是从request中取出需要使用的loader那一步用到的东西。(NormalModuleFactory.js:247)

当处理完的时候进入回调的时候神TM的results中得到的loader又变成了空数组!合着我辛辛苦苦拿到的loader最后又喂了狗(未解之谜,正常的流程操作在下面,请接着看)

从伤痛之中走出来发现,底下执行回调函数的时候并没有直接执行,而是放到了nextTick里面,过后试一下拿出来会怎么样

由于回调回去会用到解析器进行词法分析,这里调用了getParser方法尝试拿到一个解析器,如果没有现成的的话就创建一个新的,顺便把她缓存下来之后使用

进入创建解析器的函数,我们会发现她创建好新的对象过后会把自身上安装上插件「二元计算」「typeof计算」等等,另外还发现在处理字符串的split,substr等操作函数时,会有特别的处理,由此可见其实在这一步里面parser其实会做少量的字符串优化

创建完成之后,得到了parser她也是继承自Tapable的,那么我们其她组件也可以慢慢发挥自己的作用了,通过把自己在parse阶段要做的处理apply上去,比如之前说的import和require.ensure等等就是在这里找到指定的表达式进行替换处理的

解析时工作的插件

按照旧例梳理一下有哪些插件应用到了这上面,详细的作用不做梳理,不然能讲一年:

  1. HotModuleReplacement,主要是做模块热替换的处理(这玩意儿都能讲一个专题)

    1. webpack_hash变成webpack_require.h()对照前面的看看这个函数具体会变成什么作用

    2. 把module.hot的计算值根据开发环境还是什么的进行替换,这里的模块热替换也可以去看看相关的知识点,她的依赖图谱也是一个树形的结构,牵一发而动全身,如果本层没有处理这个更新那么会向上一层传递

    3. 调用module.hot.accept的时候,会根据里面的参数进行转换,这个地方再次引入了插件。。。简直了。。如果带参数就是callback,没有就是withoutCallback,居然是在HarmonyImportDependencyParser里面引入的两个处理模块

      if (module.hot) {
        module.hot.accept('./print.js', function() {
          console.log('Accepting the updated printMe module!');
          printMe();
        })
      }
      

      这里做的处理就是吧前面的request收集起来,做成包的依赖

    4. module.hot.decline….

      QQ20170915-195157@2x
      QQ20170915-195157@2x
  2. DefinePlugin,把我们在plugin中设置好的表达式进行替换就是了

    1. 主要的处理是在can-rename中进行的,不过一般不好保证这个插件能比较完美的执行

    2. 如果出现循环替换的怎么办,a->b,b->a,这样的情况下,直接返回原本代码的计算值

      QQ20170915-203536@2x
      QQ20170915-203536@2x
  3. NodeSource,就是处理global,process,console,Buffer,setImmediate这几个

    1. 那么global是怎么替换的呢,就像下面这样,把她用来替换,不过这里面这个(1,eval)我是真没搞懂,还有webpack里面的(0,module)都很trick的感觉

      QQ20170915-204951@2x
      QQ20170915-204951@2x
      QQ20170915-205646@2x
      QQ20170915-205646@2x
    2. 那么其她呢?其实都是依赖的node-libs-browser这个package里面的内容,不过其实有些模块还是没有实现的,当然都是无可避免的,比如dns,cluster这一类,简单看看console的实现,其她自行查阅

      QQ20170915-205819@2x
      QQ20170915-205819@2x
  4. Compatibility,主要是针对browserify做的一些处理,不过我没有用过这个东西,她好像是require后面可以带上两个参数同时添加上一个新的ConstDependecy

  5. HarmonyModules,里面包含了好几个插件

  1. HarmonyDetectionParser主要是在program阶段,处理是import和export等语句,如果有这两个关键字出现的话,就把这个模块当成HarmonyCompatibility依赖处理,添加进入依赖列表,注意这个module是从parser.state.module中取出来的

  2. HarmonyImportDependencyParser,根据import寻找依赖,把找到的依赖通过HarmonyImportDependency添加进依赖列表中;如果有import specifier好像会换成imported var过后会有另一个插件来处理,把她变成HarmonyImportSpecifierDependency的依赖,这个specifier应该说的是引入部分模块那种操作

  3. HarmonyExportDependencyParser,对export做了类似的处理,不过有点看不懂

  4. 上面这些注册的调用是在哪里执行的呢?当然就是在parser处理得过程中啦,由于名字都是一一对应的所以我们只需要简单的搜索一下就能知道「import specifier」是在Parser.js:656开始进行处理的,可以往回观察她的逻辑

    WX20170911-001528@2x
    WX20170911-001528@2x
  5. AMD,安装了两个插件,第一个处理了所有和require相关的依赖加载(复杂异常)有很多的parser类似于「call require:amd:array」这种应该是parser阶段做的特殊处理;第二个是处理define加载相关解析操作的,和前一个差的不多;剩下的就是对typeof等等的一些处理了

  6. COMMONJs,就是处理module.exports这种啦,当然还有require,同样为了保证给require赋值时不导致undefine的尴尬,插件会加上一个var require;

  7. NodeStuff,有什么用呢?当然就是把文档中所写的那些nodeAPI给替换掉啊,浏览器环境可是没有什么__dirname这种东西的,当然还有module相关的什么id,loaded之类的东西

  8. RequirejsStuff,有些小用处

    1. 让require.config和requirejs.config返回undefined

    2. 把require.version换成0.0.0,这样我们可以看看当前的系统是不是使用的webpack打包咯~,毕竟只有requirejs参与的话这里就会是她的版本了

    3. require.onError是加载错误的回调函数,会转变成webpack.oe,请对照之前说的列表看看怎么操作的,不过有了import可以用catch处理这个也没那么重要了

      WX20170911-005110@2x
      WX20170911-005110@2x
  9. API,还是官方文档里写的那些表达式的处理,__wepack_require__我们上面说的oe也在这里面哦

  10. Const,主要是处理表达式的true或者false,进行表达式的计算直接替换做优化,另外还有一个__resourceQuery是和热加载相关的

    QQ20170915-191630@2x
    QQ20170915-191630@2x
  11. UseStrict,处理的时候添加了一个空的ConstDependency,和一个issue有关,不这样处理位置可能不对issue:1970

  12. Require.include,没鸟用,下一个

  13. Require.ensure,除了基本的typeof处理等,加载了一个插件RequireEnsureDependenciesBlockParser处理异步加载,她最多的处理参数可以达到4个。。而处理的逻辑里面会发现,这次操作并没有把当前得到的模块添加到parser的依赖上面,而是直接赋值了一个给parser.state.current

  14. RequireContext,好东西啊,不过不常用,之后再解释

  15. Import,这个import和之前的Harmony插件有什么区别呢?区别就是这个import其实是webpack里面那个System.import 和 import函数,进来处理的时候呢,首先会把她的第一个参数进行计算处理,然后判断这个东西计算出来是不是一个字符串,如果是直接可以计算出是某个确定的字符串的话,那我们就可以直接引入相对应的模块

    ​ 1. 从源码才看出来,其实这个加载支持几种方式,有lazy模式只是其中一种,她还可以是eager和weak的方式

    ​ 2. 当是其她两种方式的时候会加入对应type的Dependency,但是如果是lazy模式下面则会作为一个新的block添加,这个block继承自AsyncDependenciesBlock,就是平时的异步模块

    ​ 3. 但如果不是字符串的时候怎么办呢,这个时候创建的就是我们的ContextDependency了,这个东西会根据我们已经知道的模块信息进行模块查找,匹配的都打包到一起i(未验证)

  16. System,处理System这个变量,不知道有什么鸟用,现在她上面其实只有import一个方法,对她的set,get,register都做了报错处理

parser.state.dependendies好像是一个特别重要的东西

另外要注意的是我们现在调用的是nmf的createParser,所以只会有类似于params.mormalModuleFactory.plugin("parser")这种才会在这个步骤进行注册操作,其她如hot或者chunk这一类的会在自己的周期中进行注册

至此,parser创建完毕,返回回去

create-module

然后创建一个真正的NomalModule对象,进行一些没有实际插件工作的操作,过后再次进入到我们的doBuild过程中,调用runLoader的方法使用loadLoader加载我们将要使用的loader

进入loadLoader函数中,首先会检查是否有System这个对象,以及System.import这个东西,但是讲道理这个东西应该是在我们将要处理的文件中出现的东西,为什么会在我们的工具代码中出现呢?这点暂时不得而知,不过有可能是因为这一段代码也有可能被打包进入我们的工程文件,然后通过webpack进行处理;在发现没有这种方法将资源引入过后,webpack会选择使用require的方式把loader加载进来(注意这里的require是node的require,即同步加载的require :loaderRunner/lib/loadLoader.js:2

在这过后调用iteratePitchingLoaders方法,不断递归的检查还有没有其她的loader加入(pitch指的就是我们的loader是否是从这个位置开始执行,所以如果当pitch为undefined的时候会导致她不断的递归处理,直到到达最前面一个loader或者是刚好是pitch的loader)

这里我们只有一个ejs的loader,那么就会进入processResource中开始对资源进行处理

  1. 进来过后又会添加依赖,不过这次的依赖不是添加到我们的module或者是compilation上面,而是添加到了loaderContext维护的一个数组上
  2. 利用我们设置好的资源加载函数获取到资源(毕竟现在可能把文件系统设置为了内存中或者是webDV等)
  3. 获取到资源过后又开始进行一个递归,不明白为什么全部的逻辑都用那一个函数处理,让人头大,总之好不容易是拿到了我们要的资源,现在是buffer格式的,利用createSource方法把她的字符串类型和buffer类型都放到_source里面存好便于以后处理(注意这里的buffer解码成string的时候没有设置选项直接是UTF-8)
  4. 这个时候总算是回到了我们doBuild函数的回调函数之中对资源进行操作了
  5. 这里提到我们可以对资源进行noParse的设置,反正检查了一下,设置项中好像没看到这个东西
  6. 常使用我们准备好的parser来处理文件了,话说loader呢????这里webpack使用的是acorn,像平时的babel使用的是babylon来解析的一样(不过对于她们两个来说有一个解析出来的是ESTree,另一个是BabelASTTree,规范不同导致不兼容,所以最后选择的是acorn,至于babylon网上有人说就是acorn的魔改版本,不再扩展,link:https://blog.zsxsoft.com/post/28

聊一聊AST相关的解析

  1. 利用acorn新创建一个parser,这个parser和我们之前提到的设置好的parser不一样,这个是实际上内部真正处理内容的parser,之前那个相当于是外部的一层封装便于我们使用各种插件对她进行处理
  2. 获取设置,根据我们的配置进行处理,可以看到开启了dynamicImport插件的使用;然后按照设置的ESMA解析版本对关键词的过滤进行设置如「let」「default」「import」等等
  3. 根据8开始到当前版本往回获取保留的关键字,像我们设置的是6,所以这里保留关键字还剩下「await」「enum」,再往后就把她们和完全没有版本实现的一些关键字数组拼接起来,比如「static」「private」等
  4. 加载我们设置好的插件进来进行处理,进行parse操作
    1. 利用skipSpace筛选跳过所有空格和注释
    2. 读入第一个token,然后开始进行无尽的循环,直到eof
    3. 读出来的数据全部放到node的body里面
    4. 完了过后有会调用next方法,里面又会调用nextToken方法相当于做了一次检查
    5. 然后这一步处理完成加上type的标签,最外面一层的type就是Program,里面的是各种各样的表达式或者块部分,相当于这个树的每一个节点都有自己的type,这也是我们之前注册的parser插件们得以正常工作的前提
    6. 最终得到了整个的抽象语法树,但是要知道注释是单独抽出来的没有放到语法树里面
  5. 拿到了AST开始执行我们之前绑定的事件,比如首当其冲的program就开始处理啦

还记得之前说的use strict加入了一个constDependency吗,其实这玩意儿没什么用,她不会添加新的依赖,给人感觉只是一个干净的占位符,不过另外还有其她类型的依赖,她们就是有各种各样的特殊作用了

其实啊,这些依赖呢,本身就是存储了相应的位置信息,还有需要添加的模板,她们都有一个非常重要的属性Template,这个东西能够在最后加入进文档的时候把她们的内容进行添加操作

继续进行prewalk的处理,这里我们简单的举一些例子,比如第一次进来的var Sockjs首先会进行一个var-XXX的绑定操作,然后才是var Sockjs的操作,所以要是真正开发的时候这里面的解析顺序还是非常值得注意的问题,在处理完成过后就会在defination数组之中加入我们新创建的变量名字,以便后续的处理过程使用。

QQ20170916-131359@2x
QQ20170916-131359@2x

进行完成prewalk过后就是我们的walk操作,在这一步中

  1. 进行statement的绑定操作,没有绑定的操作
  2. 之前进行过一次的prewalk操作,操作并记录了一些变量的名字,现在在walk的时候我们可以对变量进行改名操作等等,但是rename操作有个特点,那就是在rename之前必须有个can-rename XXX得返回true,判断这个变量是可以进行rename操作的
  3. 对于call expression这种表达式,会实时的调用evaluate相关的绑定进行替换,就不再赘述,总之我们之前插件中绑定的解析器处理函数都在这里起作用就对了

success-module

在回调函数中,由于完成了一个文件的解析处理,这里我们把semaphore还回去一个,即释放回去一个资源,同时由于这里的递归参数被设置为了true,我们会继续寻找已处理模块的依赖(注意这里的模块概念,我们的input设置的一个键名即对应了一个chunk,而不是只数组的每个值对应一个模块)

突然发现这个block的单位值得拿出来说一说,在这个地方添加依赖的时候首先就是会执行addDependenciesBlock,这样算是把整个模块当成是一个block来处理了,然后再处理里面的子block和dependencies等等,全部添加进去

QQ20170915-135718@2x
QQ20170915-135718@2x

既然有入口,那么肯定就免不了循环的寻找依赖了,现在又会调用我们之前使用过的addModuleDependencies方法,进行依赖的寻找,以及所依赖模块的依赖递归处理

单独说一下NullFactory,她会处理我们之前提到的ConstDependency,整个create函数毛也没干直接就执行了回调函数,暂时没发现有什么用,所以你也知道ConstDependency只是占位了

不过如果是碰到正常的模块的话比如说Coomonjs的依赖,她在map中其实对应的就是一个普通的nmf,这个时候就会把这个模块普普通通的进行像之前解析入口文件一样的操作

loader执行操作

由于之前的模块在node_module里面,成功的避开了设置好的经历loader处理的过程,所以这里先单独拿出来说过后补上去

可以看到这次好不容易啊,我们的loaders数目终于变成了1,总算是可以进行babel的处理了,还有需要注意的问题就是这个东西她不知道是又开了一个进程还是怎么的,如果别的地方打了断点是进不来的,花了好多时间尝试,inspect的机制也有待了解

终于可以正式进行处理了好兴奋,这里也会把我们的输入参数进行格式化(其实就是拿到的文件资源buffer,webpack也是很聪明的,如果不使用buffer这样的原始内存空间,那么项目的大小和资源大小就会收到限制了)

然后我们一直说的处理BOM头,这个BOM头究竟是个啥,其实她就是0xFEFF,我们在第一个字符找到了她要记得清除哦,不然鬼知道会解析出来的什么鬼东西

然后进入了神秘的runSyncOrAsync函数,可能执行同步或者异步操作,看来就是拿给我们的loader来做决定了

babel-loader

  1. 进来插件里面当然会礼貌性的检查一下我们有没有.babelrc的配置文件,不过并没有主动的找,而是看我们有没有设置

  2. 由于我们的loader要执行异步操作,这里便先执行一下webpack要求的this.async方法,主要就是跑回去把isSync变成了false,下次检查的时候就知道这个不是同步处理了

  3. 我们发现loader的默认缓存路径是在node_module/.cache/babel-loader里面,而且由于没有使用外部的fs系统,她的内容是确实的存在在硬盘中的,检查目录的时候也是用的mkdirp确定目录存在

  4. 在进行debug操作的时候要记得把之前的缓存删掉不然会直接拿到旧的数据

  5. 反正终于通过read方法拿到了我们需要的文件,这个时候就尝试调用transform方法进行转化工作,这里的编译函数就是index.js:38中的transpile函数,可以看到很多用void 0而不是undefined,除了代码压缩的时候我还少有看到这么干的

  6. babel-core的代码看起来贼难受,本身也是从es6的代码转化过来的,这算是自举了?23333333

  7. 总之嘛,算是把她解析好了看看处理完成什么样子,算是包含了注释单独抽出来,ast树结构,这个babel解析的树肯定还是和之前说的那个有些不一样的,解析出来的代码code,我们的sourceMap,以及在处理过程中得到的所有token居然也保留了下来,这个时候返回我们处理的结果,但是只返回了code,map和metadata这就让人很难受了啊,如果这个语法树要是能够直接拿去给检查依赖用多好省了不少时间

  8. 返回出来的结果把她缓存到我们之前说过的目录里面,以便下次使用加快编译

  9. 我们留意一下编译出来的代码,会发现每处外部加载包被调用之前都会有(0, XXX)的写法,到现在还没发现到底拿来干嘛

  10. 看了一下存下来的metadata发现她还是存下来了import进去的依赖,看来过后还是可以使用的嘛

  11. 然后会让metaData的订阅者们首先处理一下这些依赖,讲道理我们也可以在这里做些手脚,不过现在发现是没有东西进行操作

  12. 最终执行回调退出过程,这次还是没有吧metadata带走!所以metadata还是没能翻身!

  13. 回到我们熟悉的runSyncOrAsync的回调之中LoaderRunner.js:233再次执行iterateNormalLoaders,为什么会这样呢?当然是因为还是要去这个韩束里面判断我们是否还有loader要对她进行处理咯,事实证明是没有的,有空我们看看less文件怎么办

  14. 随着调用栈的不断退出!我们终于又回到了doBuild中,开始了新一轮解析AST的征程!是不是有毛病!

说了一下babel是这样处理的,那么其她资源文件是怎么做的呢?

less文件的处理会把文件进行转码最终变成字符串传给下游,css和style等组件并不会直接把资源做多大的处理,她们更多的是添加依赖进去module里面,这些添加的依赖是一些工具函数,最终会帮助资源进行封装工作

这里可以做一下思考为什么webpack的设计者会让实际上越后处理资源的loader放到列表的前面呢?

另外还有一点就是css从某个版本开始没有直接使用字符串存放我们的css资源了,取而代之的是使用了base64的字符串,如果支持的情况下会使用atob的方式对资源进行解码,这样处理好像是对于sourceMap更加方便

在github原文中有详细的核心插件作用介绍,欢迎查看 :)

【1】淘宝FED-细说 webpack 之流程篇 http://taobaofed.org/blog/2016/09/09/webpack-flow/

【2】zsx的博客 https://blog.zsxsoft.com/post/28

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容