webpack4源码分析

webpack设计模式

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

Tapable

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

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

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

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

在须要触发的时机调用this.hooks.accelerate.call就能够触发订阅accelerate的全部监听函数,newSpeed是传入的参数。git

setSpeed(newSpeed) {
        this.hooks.accelerate.call(newSpeed);
    }
复制代码
webpack的插件架构

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

webpack流程概述

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

  • 根据咱们的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,能够写插件在构建以前来处理一些初始化数据。bash

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

  • 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阶段才结束。

make

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类。

result

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的类型和执行不一样的逻辑,并建立依赖。

ast

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'来区分。

css

在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会构成一个图。

moule

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阶段的流程图。

seal

生成 assets

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

create-assets

从上图能够看出不一样的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中。

create-assets-code

seal产物

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

assets

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方法去构建。

watcher1

在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数不清的事件名、看不完的内部插件,各类事件之间调过去调过来;~~~就这样吧,~_~

相关文章
相关标签/搜索