跳至主要內容

Webpack 原理

Mr.LRH大约 11 分钟

Webpack 原理

Webpack 本质上是一种事件流机制。通过事件流将各种插件串联起来,最终完成 Webpack 的全流程。实现事件流机制的核心是 Tapable 模块。Webpack 负责编译的 Compiler 和创建 Bundle 的 Compilation 都是继承自 Tapable

Tapable

Tapable 和 Node.js 中的 EventEmitter 类似,包括多种类型,通过事件的注册和监听,触发 Webpack 生命周期中的函数方法。

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require('tapable');

Hook 同步与异步分类

Hook 类型可以分为同步(Sync)和异步(Async),异步又分为并行和串行

  • 同步 Hook
    • SyncHook - 串行
    • SyncBailHook - 串行
    • SyncWaterfallHook - 串行
    • SyncLoopHook - 循环
  • 异步 Hook
    • AsyncSeries* 串行
      • AsyncSeriesHook
      • AsyncSeriesBailHook
      • AsyncSeriesWaterfallHook
    • AsyncParallel* 并行
      • AsyncParallelHook
      • AsyncParallelBailHook

Hook 使用方式分类

  • Basic : 基础类型,不关心函数的返回值,不根据返回值做事情,会一直执行到底。包括 SyncHookAsyncParallelHookAsyncSeriesHook
  • Bail : 按回调栈顺序依次执行回调,但是如果其中一个回调函数返回结果 result !== undefined ,则退出回调栈调。包括 SyncBailHookAsyncSeriesBailHookAsyncParallelBailHook
  • Waterfal : 瀑布式,如果上一个回调函数的结果 result !== undefined,则会被作为下一个回调函数的第一个参数。包括 SyncWaterfallHookAsyncSeriesWaterfallHook
  • Loop : 循环类型,如果该监听函数返回 true ,则这个监听函数会反复执行;如果返回 undefined 则退出循环。包括 SyncLoopHook

webpack_tapable

Tapable 原理解析

Tapable 的执行流程可以分为四步:

  1. 使用 tap* 对事件进行注册绑定。根据类型不同,提供三种绑定的方式:taptapPromisetapAsync ,其中 tapPromisetapAsync 为异步类 Hook 的绑定方法;
  2. 使用 call* 对事件进行触发,根据类型不同,也提供了三种触发的方式: callpromisecallAsync
  3. 生成对应类型的代码片段(要执行的代码实际是拼字符串拼出来的);
  4. 生成第三步生成的代码片段。

Compiler 和 Compilation

CompilerCompilation 都是继承自 Tapable

  • Compiler : 每个 Webpack 的配置,对应一个 Compiler 对象,记录着整个 Webpack 的生命周期。webpack 启动后会创建 compiler 对象,该对象一直存活知道结束退出。
  • Compilation : 在构建的过程中,每次构建都会产生一次 CompilationCompilation 是构建周期的产物。

Compiler

整个 Compiler 完整地展现了 Webpack 的构建流程:

  • 准备阶段 : make 之前做的事情都属于准备阶段,这阶段的 callback 入参以 compiler 为主;
  • 编译阶段 : 该阶段以 compilation 的钩子为主,callback 入参以 compilation 为主;
  • 产出阶段 : 该阶段从 compilation 开始,最后回到 Compiler 钩子上,callback 传入参数是跟结果相关的数据,包括 statserror

Compiler 钩子

钩子名Tapable 类型触发时机传入 callback 的参数
entryOptionSyncBailHook在 webpack 中的 entry 配置处理过之后context,entry
afterPluginsSyncHook初始化完内置插件之后compiler
afterResolversSyncHookresolver 完成之后(后面解释resolver是什么)compiler
environmentSyncHook准备编译环境,webpack plugins配置初始化完成之后compiler
afterEnvironmentSyncHook编译环境准备好之后compiler
beforeRunAsyncSeriesHook开始正式编译之前compiler
runAsyncSeriesHook开始编译之后,读取 records 之前;监听模式触发watch-runcompiler
watchRunAsyncSeriesHook监听模式下,一个新的编译触发之后compiler
normalModuleFactorySyncHookNormalModuleFactory 创建之后normalModuleFactory实例
contextModuleFactorySyncHookContextModuleFactory 创建之后contextModuleFactory实例
beforeCompileAsyncSeriesHookcompilation 实例化需要的参数创建完毕之后compilationParams
compileSyncHook一次 compilation 编译创建之前compilationParams
thisCompilationSyncHook触发 compilation 事件之前执行compilation,compilationParams
compilationSyncHookcompilation创建成功之后compilation,compilationParams
makeAsyncParallelHook完成编译之前compilation
afterCompileAsyncSeriesHook完成编译和封存(seal)编译产出之后compilation
shouldEmitSyncBailHook发布构建后资源之前触发,回调必须返回true/false,true则继续compilation
emitAsyncSeriesHook生成资源到 output 目录之前compilation
afterEmitAsyncSeriesHook生成资源到 output 目录之后compilation
doneAsyncSeriesHookcompilation完成之后stats
failedSyncHookcompilation失败error
invalidSyncHook监听模式下,编译无效时fileName,changeTime
watchCloseSyncHook监听模式停止

Compilation

Compilation 阶段,模块会被 加载(loaded)封存(sealed)优化(optimized)分块(chunked)哈希(hashed)重新创建(restored)

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以监听(watch)模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展,通过 Compilation 也能读取到 Compiler 对象。

Compilation 中处理的对象分别是 modulechunkasset,由 modules 组成chunks,由 chunks 生成 assets,处理顺序是:modulemoduleschunksassets(先从单个 module 开始处理,查找依赖关系,最后完成单个 module 处理,完成全部modules 之后,开始 chunks 阶段处理,最后在根据优化配置,按需生成 assets)。

Compilation 钩子

钩子名Tapable 类型触发时机传入 callback 的参数
buildModuleSyncHook在模块构建开始之前触发module
rebuildModuleSyncHook在重新构建一个模块之前触发module
failedModuleSyncHook模块构建失败时执行module,error
succeedModuleSyncHook模块构建成功时执行module
finishModulesSyncHook所有模块都完成构建module
finishRebuildingModuleSyncHook一个模块完成重新构建module
sealSyncHook★编译(compilation)停止接收新模块时触发module
unsealSyncHook编译(compilation)开始接收新模块时触发module
optimizeDependenciesSyncBailHook依赖优化开始时触发modules
afterOptimizeDependenciesSyncHook依赖优化结束时触发modules
optimizeSyncHook★优化阶段开始时触发modules
optimizeModulesSyncBailHook★模块的优化modules
afterOptimizeModulesSyncHook模块优化结束时触发modules
optimizeChunksSyncBailHook★优化 chunkschunks
afterOptimizeChunksSyncHookchunk 优化完成之后触发chunks
optimizeTreeAsyncSeriesHook异步优化依赖树chunks,modules
afterOptimizeTreeSyncHook异步优化依赖树完成时chunks,modules
optimizeChunkModulesSyncBailHook优化单个chunk中的 modules 开始chunks
afterOptimizeChunkModulesSyncHook优化单个chunk中的 modules 结束chunks
shouldRecordSyncHookchunks
reviveModulesSyncHook从 records 中恢复模块信息modules,records
optimizeModuleOrderSyncHook将模块从最重要的到最不重要的进行排序chunks
beforeModuleIdsSyncHook处理 modulesId 之前modules
moduleIdsSyncHook处理 modulesIdmodules
optimizeModuleIdsSyncHook优化 modulesIdchunks
afterOptimizeModuleIdsSyncHook优化 modulesId之后chunks
reviveChunksSyncHook从 records 中恢复 chunk 信息modules,records
optimizeChunkOrderSyncHook将 chunk 从最重要的到最不重要的进行排序chunks
beforeOptimizeChunkIdsSyncHookchunk id 优化之前触发chunks
optimizeChunkIdsSyncHookchunk id 优化开始触发chunks
afterOptimizeChunkIdsSyncHookchunk id 优化结束触发chunks
recordModulesSyncHook将模块信息存储到 recordsmodules,records
recordChunksSyncHook将 chunk 信息存储到 recordschunks,records
beforeHashSyncHook在编译被哈希(hashed)之前-
afterHashSyncHook在编译被哈希(hashed)之后-
recordSyncHook将 compilation 相关信息存储到 records 中compilation,records
beforeChunkAssetsSyncHook在创建 chunk 资源(asset)之前chunks
additionalChunkAssetsSyncHook为 chunk 创建附加资源(asset)chunks
additionalAssetsAsyncSeriesHook★为编译(compilation)创建附加资源(asset)-
optimizeChunkAssetsAsyncSeriesHook★优化所有 chunk 资源(asset)chunks
afterOptimizeChunkAssetsSyncHookchunk 资源(asset)已经被优化chunks
optimizeAssetsAsyncSeriesHook★优化存储在 compilation.assets 中的所有资源(asset)assets
afterOptimizeAssetsSyncHook优化compilation.assets 中的所有资源(asset)之后assets
moduleAssetSyncHook一个模块中的一个资源被添加到编译中module,filename
chunkAssetSyncHook一个 chunk 中的一个资源被添加到编译中chunk,filename
assetPathSyncWaterfallHookasset 路径确认之后filename,data
childCompilerSyncHook子编译(compiler)触发childCompiler,compilerName,compilerIndex
normalModuleLoaderSyncHook★普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数loaderContext,module

Stats 对象

在 Webpack 的回调函数中会得到 stats 对象。这个对象实际来自于 Compilation.getStats(),返回的是主要含有 moduleschunksassets 三个属性值的对象。

  • modules :记录了所有解析后的模块。在每个 module 中,包含如下信息:

    • 基本信息:包括最基本的内容、大小、id
    • 依赖关系:module.reasons 对象描述了这个模块被加入依赖图表的理由,包含了引入的方式、引入的 module 信息及其对应代码在第几行第几列等,可以通过这个计算出 module 之间的依赖关系图表(graph);
    • chunksassets 关系:module.chunksmodule.assets 包含到 chunksassets 中的对应 id 等;
    • 被 webpack 处理的后的信息:包含 module.failedmodule.errorsmodule.warnings等。
  • chunks :记录了所有 chunk;在每个 chunk 中,包含如下信息:

    • 基本信息:包括最基本的内容、大小、id
    • 来源:chunk.origins 对象描述了这个模块被加入的理由,包含了引入的方式、引入的 module 信息及其对应代码在第几行第几列等,可以通过这个计算出 module 之间的依赖关系图表(graph);
    • 引用关系:chunk.parentschunk.children 被引用和引用的 ids
    • 包含和被包含:chunk.fileschunk.modules 包含到 assets 和自己包含 modules 中信息等。
  • assets :记录了所有要生成的文件。

    {
      "chunkNames": [], // 这个 asset 包含的 chunk
      "chunks": [10, 6], // 这个 asset 包含的 chunk 的 id
      "emitted": true, // 表示这个 asset 是否会让它输出到 output 目录
      "name": "10.web.js", // 输出的文件名
      "size": 1058 // 文件的大小
    }
    

Stats 对象本质上来自于 lib/Stats.js 的类实例,常用的方法:

  • stats.hasWarnings()
  • stats.hasErrors()
  • stats.toJson()
  • stats.toString()

Webpack 工作流程

  1. 初始化阶段:
    • 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
    • 创建编译器对象:用上一步得到的参数创建 Compiler 对象
    • 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
    • 开始编译:执行 compiler 对象的 run 方法
    • 确定入口:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象(依赖对象,webpack 基于该类型记录模块间依赖关系)
  2. 构建阶段:
    • 编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
    • 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
  3. 生成阶段:
    • 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
    • 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack

  1. 初始化阶段
    • process.args + webpack.config.js 合并成用户配置

    • 调用 validateSchema 校验配置

    • 调用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终配置

    • 创建 compiler 对象

    • 遍历用户定义的 plugins 集合,执行插件的 apply 方法

    • 调用 new WebpackOptionsApply().process 方法,加载各种内置插件

      主要逻辑集中在 WebpackOptionsApply 类,webpack 内置了数百个插件,这些插件并不需要我们手动配置,WebpackOptionsApply 会在初始化阶段根据配置内容动态注入对应的插件,包括:

      • 注入 EntryOptionPlugin 插件,处理 entry 配置
      • 根据 devtool 值判断后续用那个插件处理 sourcemap,可选值:EvalSourceMapDevToolPluginSourceMapDevToolPluginEvalDevToolModulePlugin
      • 注入 RuntimePlugin ,用于根据代码内容动态注入 webpack 运行时
    • 启动 webpack ,触发 lib/webpack.js 文件中 createCompiler 方法

    • createCompiler 方法内部调用 WebpackOptionsApply 插件

    • WebpackOptionsApply 定义在 lib/WebpackOptionsApply.js 文件,内部根据 entry 配置决定注入 entry 相关的插件,包括:DllEntryPluginDynamicEntryPluginEntryPluginPrefetchPluginProgressPluginContainerPlugin

    • Entry 相关插件,如 lib/EntryPlugin.jsEntryPlugin 监听 compiler.make 钩子

    • lib/compiler.jscompile 函数内调用 this.hooks.make.callAsync

    • 触发 EntryPluginmake 回调,在回调中执行 compilation.addEntry 函数

    • compilation.addEntry 函数内部经过一坨与主流程无关的 hook 之后,再调用 handleModuleCreate 函数,正式开始构建内容

  2. 构建阶段
    • 调用 handleModuleCreate ,根据文件类型构建 module 子类
    • 调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本
    • 调用 acorn 将 JS 文本解析为 AST
    • 遍历 AST,触发各种钩子
    • HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖
    • 调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中
    • AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖
    • 对于 module 新增的依赖,调用 handleModuleCreate ,控制流回到第一步
    • 所有依赖都解析完毕后,构建阶段结束
  3. 生成阶段
    • 构建本次编译的 ChunkGraph 对象;

    • 遍历 compilation.modules 集合,将 moduleentry/动态引入 的规则分配给不同的 Chunk 对象;

    • compilation.modules 集合遍历完毕后,得到完整的 chunks 集合对象,调用 createXxxAssets 方法

    • createXxxAssets 遍历 module/chunk ,调用 compilation.emitAssets 方法将资 assets 信息记录到 compilation.assets 对象中

    • 触发 seal 回调,控制流回到 compiler 对象

      这一步的关键逻辑是将 module 按规则组织成 chunks ,webpack 内置的 chunk 封装规则比较简单:

      • entryentry 触达到的模块,组合成一个 chunk
      • 使用动态引入语句引入的模块,各自组合成一个 chunk
      • chunk 是输出的基本单位,默认情况下这些 chunks 与最终输出的资源一一对应,而通过动态引入语句引入的模块,也对应会打包出相应的资源。

参考

上次编辑于:
贡献者: lingronghai