关于webpack:万字总结-一文吃透-Webpack-核心原理

38次阅读

共计 14744 个字符,预计需要花费 37 分钟才能阅读完成。

如果感觉文章有用,欢送点赞关注,但写作实属不易,未经作者批准,禁止任何模式转载!!!

背景

Webpack 特地难学!!!

时至 5.0 版本之后,Webpack 功能集变得十分宏大,包含:模块打包、代码宰割、按需加载、HMR、Tree-shaking、文件监听、sourcemap、Module Federation、devServer、DLL、多过程等等,为了实现这些性能,webpack 的代码量曾经到了惊人的水平:

  • 498 份 JS 文件
  • 18862 行正文
  • 73548 行代码
  • 54 个 module 类型
  • 69 个 dependency 类型
  • 162 个内置插件
  • 237 个 hook

在这个数量级下,源码的浏览、剖析、学习老本十分高,加上 webpack 官网语焉不详的文档,导致 webpack 的学习、上手老本极其高。为此,社区围绕着 Webpack 衍生出了各种手脚架,比方 vue-cli、create-react-app,解决“用”的问题。

但这又导致一个新的问题,大部分人在工程化方面逐步变成一个配置工程师,停留在“会用会配”然而不晓得黑盒外面到底是怎么转的阶段,遇到具体问题就瞎了:

  • 想给根底库做个降级,呈现兼容性问题跑不动了,间接放弃
  • 想优化一下编译性能,然而不分明外部原理,无从下手

究其原因还是对 webpack 外部运行机制没有造成必要的整体认知,无奈迅速定位问题 —— 对,连问题的实质都经常看不出,所谓的不能透过景象看实质,那实质是啥?我集体将 webpack 整个宏大的体系形象为三方面的常识:

  1. 构建的外围流程
  2. loader 的作用
  3. plugin 架构与罕用套路

三者合作形成 webpack 的主体框架:

了解了这三块内容就算是入了个门,对 Webpack 有了一个最最根底的认知了,工作中再遇到问题也就能按图索骥了。补充一句,作为一份入门教程,本文不会开展太多 webpack 代码层面的细节 —— 我的精力也不容许,所以读者也不须要看到一堆文字就产生特地大的心理累赘。

外围流程解析

首先,咱们要了解一个点,Webpack 最外围的性能:

At its core, webpack is a static module bundler for modern JavaScript applications.

也就是将各种类型的资源,包含图片、css、js 等,转译、组合、拼接、生成 JS 格局的 bundler 文件。官网首页的动画很形象地表白了这一点:

这个过程外围实现了 内容转换 + 资源合并 两种性能,实现上蕴含三个阶段:

  1. 初始化阶段:

    1. 初始化参数:从配置文件、配置对象、Shell 参数中读取,与默认配置联合得出最终的参数
    2. 创立编译器对象:用上一步失去的参数创立 Compiler 对象
    3. 初始化编译环境:包含注入内置插件、注册各种模块工厂、初始化 RuleSet 汇合、加载配置的插件等
    4. 开始编译:执行 compiler 对象的 run 办法
    5. 确定入口:依据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象
  2. 构建阶段:

    1. 编译模块(make):依据 entry 对应的 dependence 创立 module 对象,调用 loader 将模块转译为规范 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都通过了本步骤的解决
    2. 实现模块编译 :上一步递归解决所有能触达到的模块后,失去了每个模块被翻译后的内容以及它们之间的 依赖关系图
  3. 生成阶段:

    1. 输入资源(seal):依据入口和模块之间的依赖关系,组装成一个个蕴含多个模块的 Chunk,再把每个 Chunk 转换成一个独自的文件退出到输入列表,这步是能够批改输入内容的最初机会
    2. 写入文件系统(emitAssets):在确定好输入内容后,依据配置确定输入的门路和文件名,把文件内容写入到文件系统

单次构建过程自上而下按程序执行,上面会开展聊聊细节,在此之前,对上述提及的各类技术名词不太熟悉的同学,能够先看看简介:

  • Entry:编译入口,webpack 编译的终点
  • Compiler:编译管理器,webpack 启动后会创立 compiler 对象,该对象始终存活晓得完结退出
  • Compilation:单次编辑过程的管理器,比方 watch = true 时,运行过程中只有一个 compiler 但每次文件变更触发从新编译时,都会创立一个新的 compilation 对象
  • Dependence:依赖对象,webpack 基于该类型记录模块间依赖关系
  • Module:webpack 外部所有资源都会以“module”对象模式存在,所有对于资源的操作、转译、合并都是以“module”为根本单位进行的
  • Chunk:编译实现筹备输入时,webpack 会将 module 按特定的规定组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输入一一对应
  • Loader:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器
  • Plugin:webpack 构建过程中,会在特定的机会播送对应的事件,插件监听这些事件,在特定工夫点染指编译过程

webpack 编译过程都是围绕着这些要害对象开展的,更具体残缺的信息,能够参考 Webpack 常识图谱。

初始化阶段

根本流程

学习一个我的项目的源码通常都是从入口开始看起,按图索骥缓缓摸索出套路的,所以先来看看 webpack 的初始化过程:

解释一下:

  1. process.args + webpack.config.js 合并成用户配置
  2. 调用 validateSchema 校验配置
  3. 调用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终配置
  4. 创立 compiler 对象
  5. 遍历用户定义的 plugins 汇合,执行插件的 apply 办法
  6. 调用 new WebpackOptionsApply().process 办法,加载各种内置插件

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

  • 注入 EntryOptionPlugin 插件,解决 entry 配置
  • 依据 devtool 值判断后续用那个插件解决 sourcemap,可选值:EvalSourceMapDevToolPluginSourceMapDevToolPluginEvalDevToolModulePlugin
  • 注入 RuntimePlugin,用于依据代码内容动静注入 webpack 运行时

到这里,compiler 实例就被创立进去了,相应的环境参数也预设好了,紧接着开始调用 compiler.compile 函数:

// 取自 webpack/lib/compiler.js 
compile(callback) {const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {compilation.seal(err => {...});
            });
          });
        });
      });
    });
  }

Webpack 架构很灵便,但代价是就义了源码的直观性,比如说下面说的初始化流程,从创立 compiler 实例到调用 make 钩子,逻辑链路很长:

  • 启动 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 函数,正式开始构建内容

这个过程须要在 webpack 初始化的时候预埋下各种插件,经验 4 个文件,7 次跳转才开始进入主题,前戏太足了,如果读者对 webpack 的概念、架构、组件没有足够理解时,源码浏览过程会很苦楚。

对于这个问题,我在文章最初总结了一些技巧和倡议,有趣味的能够滑到附录浏览模块。

构建阶段

根本流程

你有没有思考过这样的问题:

  • Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 别离实现了什么?
  • Webpack 编译过程中,如何辨认资源对其余资源的依赖?
  • 绝对于 grunt、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?

这些问题,基本上在构建阶段都能看出一些端倪。构建阶段从 entry 开始递归解析资源与资源的依赖,在 compilation 对象内逐渐构建出 module 汇合以及 module 之间的依赖关系,外围流程:

解释一下,构建阶段从入口文件开始:

  1. 调用 handleModuleCreate,依据文件类型构建 module 子类
  2. 调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本
  3. 调用 acorn 将 JS 文本解析为 AST
  4. 遍历 AST,触发各种钩子

    1. HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖
    2. 调用 module 对象的 addDependency 将依赖对象退出到 module 依赖列表中
  5. AST 遍历结束后,调用 module.handleParseResult 解决模块依赖
  6. 对于 module 新增的依赖,调用 handleModuleCreate,控制流回到第一步
  7. 所有依赖都解析结束后,构建阶段完结

这个过程中数据流 module => ast => dependences => module,先转 AST 再从 AST 找依赖。这就要求 loaders 解决完的最初后果必须是能够被 acorn 解决的规范 JavaScript 语法,比如说对于图片,须要从图像二进制转换成相似于 export default "data:image/png;base64,xxx" 这类 base64 格局或者 export default "http://xxx" 这类 url 格局。

compilation 按这个流程递归解决,逐渐解析出每个模块的内容以及 module 依赖关系,后续就能够依据这些内容打包输入。

示例:层级递进

如果有如下图所示的文件依赖树:

其中 index.jsentry 文件,依赖于 a/b 文件;a 依赖于 c/d 文件。初始化编译环境之后,EntryPlugin 依据 entry 配置找到 index.js 文件,调用 compilation.addEntry 函数触发构建流程,构建结束后外部会生成这样的数据结构:

此时失去 module[index.js] 的内容以及对应的依赖对象 dependence[a.js]dependence[b.js]。OK,这就失去下一步的线索:a.js、b.js,依据下面流程图的逻辑持续调用 module[index.js]handleParseResult 函数,持续解决 a.js、b.js 文件,递归上述流程,进一步失去 a、b 模块:

从 a.js 模块中又解析到 c.js/d.js 依赖,于是再再持续调用 module[a.js]handleParseResult,再再递归上述流程:

到这里解析完所有模块后,发现没有更多新的依赖,就能够持续推动,进入下一步。

总结

回顾章节开始时提到的问题:

  • Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 别离实现了什么?

    • 构建阶段会读取源码,解析为 AST 汇合。
    • Webpack 读出 AST 之后仅遍历 AST 汇合;babel 则对源码做等价转换
  • Webpack 编译过程中,如何辨认资源对其余资源的依赖?

    • Webpack 遍历 AST 汇合过程中,辨认 require/ import 之类的导入语句,确定模块对其余资源的依赖关系
  • 绝对于 grant、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?

    • Grant、Gulp 仅执行开发者预约义的工作流;而 webpack 则深刻解决资源的内容,性能上更弱小

生成阶段

根本流程

构建阶段围绕 module 开展,生成阶段则围绕 chunks 开展。通过构建阶段之后,webpack 失去足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面,就是开始执行 compilation.seal 函数:

// 取自 webpack/lib/compiler.js 
compile(callback) {const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      // ...
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        // ...
        this.hooks.finishMake.callAsync(compilation, err => {
          // ...
          process.nextTick(() => {
            compilation.finish(err => {**compilation.seal**(err => {...});
            });
          });
        });
      });
    });
  }

seal 原意密封、上锁,我集体了解在 webpack 语境下靠近于 “将模块装进蜜罐”seal 函数次要实现从 modulechunks 的转化,外围流程:

简略梳理一下:

  1. 构建本次编译的 ChunkGraph 对象;
  2. 遍历 compilation.modules 汇合,将 moduleentry/ 动静引入 的规定调配给不同的 Chunk 对象;
  3. compilation.modules 汇合遍历结束后,失去残缺的 chunks 汇合对象,调用 createXxxAssets 办法
  4. createXxxAssets 遍历 module/chunk,调用 compilation.emitAssets 办法将资 assets 信息记录到 compilation.assets 对象中
  5. 触发 seal 回调,控制流回到 compiler 对象

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

  • entry 及 entry 触达到的模块,组合成一个 chunk
  • 应用动静引入语句引入的模块,各自组合成一个 chunk

chunk 是输入的根本单位,默认状况下这些 chunks 与最终输入的资源一一对应,那按下面的规定大抵上能够推导出一个 entry 会对应打包出一个资源,而通过动静引入语句引入的模块,也对应会打包出相应的资源,咱们来看个示例。

示例:多入口打包

如果有这样的配置:

const path = require("path");

module.exports = {
  mode: "development",
  context: path.join(__dirname),
  entry: {
    a: "./src/index-a.js",
    b: "./src/index-b.js",
  },
  output: {filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  devtool: false,
  target: "web",
  plugins: [],};

实例配置中有两个入口,对应的文件构造:

index-a 依赖于 c,且动静引入了 e;index-b 依赖于 c/d。依据下面说的规定:

  • entry 及 entry 触达到的模块,组合成一个 chunk
  • 应用动静引入语句引入的模块,各自组合成一个 chunk

生成的 chunks 构造为:

也就是依据依赖关系,chunk[a] 蕴含了 index-a/c 两个模块;chunk[b] 蕴含了 c/index-b/d 三个模块;chunk[e-hash] 为动静引入 e 对应的 chunk。

不晓得大家留神到没有,chunk[a]chunk[b] 同时蕴含了 c,这个问题放到具体业务场景可能就是,一个多页面利用,所有页面都依赖于雷同的根底库,那么这些所有页面对应的 entry 都会蕴含有根底库代码,这岂不节约?为了解决这个问题,webpack 提供了一些插件如 CommonsChunkPluginSplitChunksPlugin,在根本规定之外进一步优化 chunks 构造。

SplitChunksPlugin 的作用

SplitChunksPlugin 是 webpack 架构高扩大的一个绝好的示例,咱们下面说了 webpack 主流程外面是按 entry / 动静引入 两种状况组织 chunks 的,这必然会引发一些不必要的反复打包,webpack 通过插件的模式解决这个问题。

回顾 compilation.seal 函数的代码,大抵上能够梳理成这么 4 个步骤:

  1. 遍历 compilation.modules,记录下模块与 chunk 关系
  2. 触发各种模块优化钩子,这一步优化的次要是模块依赖关系
  3. 遍历 module 构建 chunk 汇合
  4. 触发各种优化钩子

下面 1-3 都是预处理 + chunks 默认规定的实现,不在咱们探讨范畴,这里重点关注第 4 个步骤触发的 optimizeChunks 钩子,这个时候曾经跑完主流程的逻辑,失去 chunks 汇合,SplitChunksPlugin 正是应用这个钩子,剖析 chunks 汇合的内容,按配置规定减少一些通用的 chunk:

module.exports = class SplitChunksPlugin {constructor(options = {}) {// ...}

  _getCacheGroup(cacheGroupSource) {// ...}

  apply(compiler) {
    // ...
    compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
      // ...
      compilation.hooks.optimizeChunks.tap(
        {
          name: "SplitChunksPlugin",
          stage: STAGE_ADVANCED,
        },
        (chunks) => {// ...}
      );
    });
  }
};

了解了吗?webpack 插件架构的高扩展性,使得整个编译的主流程是能够固化下来的,分支逻辑和细节需要“外包”进来由第三方实现,这套规定架设起了宏大的 webpack 生态,对于插件架构的更多细节,上面 plugin 局部有具体介绍,这里先跳过。

写入文件系统

通过构建阶段后,compilation 会获知资源模块的内容与依赖关系,也就晓得“输出”是什么;而通过 seal 阶段解决后,compilation 则获知资源输入的图谱,也就是晓得怎么“输入”:哪些模块跟那些模块“绑定”在一起输入到哪里。seal 后大抵的数据结构:

compilation = {
  // ...
  modules: [/* ... */],
  chunks: [
    {
      id: "entry name",
      files: ["output file name"],
      hash: "xxx",
      runtime: "xxx",
      entryPoint: {xxx}
      // ...
    },
    // ...
  ],
};

seal 完结之后,紧接着调用 compiler.emitAssets 函数,函数外部调用 compiler.outputFileSystem.writeFile 办法将 assets 汇合写入文件系统,实现逻辑比拟波折,然而与主流程没有太多关系,所以这里就不开展讲了。

资源状态流转

OK,下面曾经把逻辑层面的结构主流程梳理完了,这里联合 资源状态流转 的角度从新考查整个过程,加深了解:

  • compiler.make 阶段:

    • entry 文件以 dependence 对象模式退出 compilation 的依赖列表,dependence 对象记录有 entry 的类型、门路等信息
    • 依据 dependence 调用对应的工厂函数创立 module 对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化后果若有其它依赖则持续读入依赖资源,反复此过程直到所有依赖均被转化为 module
  • compilation.seal 阶段:

    • 遍历 module 汇合,依据 entry 配置及引入资源的形式,将 module 调配到不同的 chunk
    • 遍历 chunk 汇合,调用 compilation.emitAsset 办法标记 chunk 的输入规定,即转化为 assets 汇合
  • compiler.emitAssets 阶段:

    • assets 写入文件系统

Plugin 解析

网上不少材料将 webpack 的插件架构归类为“事件 / 订阅”模式,我认为这种演绎有失偏颇。订阅模式是一种松耦合架构,公布器只是在特定机会公布事件音讯,订阅者并不或者很少与事件间接产生交互,举例来说,咱们平时在应用 HTML 事件的时候很多时候只是在这个机会触发业务逻辑,很少调用上下文操作。而 webpack 的钩子体系是一种强耦合架构,它在特定机会触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背地的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

学习插件架构,须要了解三个关键问题:

  • WHAT: 什么是插件
  • WHEN: 什么工夫点会有什么钩子被触发
  • HOW: 在钩子回调中,如何影响编译状态

What: 什么是插件

从状态上看,插件通常是一个带有 apply 函数的类:

class SomePlugin {apply(compiler) {}}

apply 函数运行时会失去参数 compiler,以此为终点能够调用 hook 对象注册各种钩子回调,例如:compiler.hooks.make.tapAsync,这外面 make 是钩子名称,tapAsync 定义了钩子的调用形式,webpack 的插件架构基于这种模式构建而成,插件开发者能够应用这种模式在钩子回调中,插入特定代码。webpack 各种内置对象都带有 hooks 属性,比方 compilation 对象:

class SomePlugin {apply(compiler) {compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});
        })
    }
}

钩子的外围逻辑定义在 Tapable 仓库,外部定义了如下类型的钩子:

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

不同类型的钩子依据其并行度、熔断形式、同步异步,调用形式会略有不同,插件开发者须要依据这些的个性,编写不同的交互逻辑,这部分内容也特地多,回头开展聊聊。

When: 什么时候会触发钩子

理解 webpack 插件的根本状态之后,接下来须要弄清楚一个问题:webpack 会在什么工夫节点触发什么钩子?这一块我认为是常识量最大的一部分,毕竟源码外面有 237 个钩子,但官网只介绍了不到 100 个,且官网对每个钩子的阐明都太简短,就我集体而言看完并没有太大播种,所以有必要开展聊一下这个话题。先看几个例子:

  • compiler.hooks.compilation

    • 机会:启动编译创立出 compilation 对象后触发
    • 参数:以后编译的 compilation 对象
    • 示例:很多插件基于此事件获取 compilation 实例
  • compiler.hooks.make

    • 机会:正式开始编译时触发
    • 参数:同样是以后编译的 compilation 对象
    • 示例:webpack 内置的 EntryPlugin 基于此钩子实现 entry 模块的初始化
  • compilation.hooks.optimizeChunks

    • 机会:seal 函数中,chunk 汇合构建结束后触发
    • 参数:chunks 汇合与 chunkGroups 汇合
    • 示例:SplitChunksPlugin 插件基于此钩子实现 chunk 拆分优化
  • compiler.hooks.done

    • 机会:编译实现后触发
    • 参数:stats 对象,蕴含编译过程中的各类统计信息
    • 示例:webpack-bundle-analyzer 插件基于此钩子实现打包剖析

这是我总结的钩子的三个学习因素:触发机会、传递参数、示例代码。

触发机会

触发机会与 webpack 工作过程严密相干,大体上从启动到完结,compiler 对象逐次触发如下钩子:

compilation 对象逐次触发:

所以,了解分明后面说的 webpack 工作的主流程,基本上就能够捋分明“什么时候会触发什么钩子”。

参数

传递参数与具体的钩子强相干,官网对这方面没有做出进一步解释,我的做法是间接在源码外面搜寻调用语句,例如对于 compilation.hooks.optimizeTree,能够在 webpack 源码中搜寻 hooks.optimizeTree.call 关键字,就能够找到调用代码:

// lib/compilation.js#2297
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {});

联合代码所在的上下文,能够判断出此时传递的是通过优化的 chunksmodules 汇合。

找到示例

Webpack 的钩子复杂程度不一,我认为最好的学习办法还是带着目标去查问其余插件中如何应用这些钩子。例如,在 compilation.seal 函数外部有 optimizeModulesafterOptimizeModules 这一对看起来很对偶的钩子,optimizeModules 从字面上能够了解为用于优化曾经编译出的 modules,那 afterOptimizeModules 呢?

从 webpack 源码中惟一搜寻到的用处是 ProgressPlugin,大体上逻辑如下:

compilation.hooks.afterOptimizeModules.intercept({
  name: "ProgressPlugin",
  call() {handler(percentage, "sealing", title);
  },
  done() {progressReporters.set(compiler, undefined);
    handler(percentage, "sealing", title);
  },
  result() {handler(percentage, "sealing", title);
  },
  error() {handler(percentage, "sealing", title);
  },
  tap(tap) {
    // p is percentage from 0 to 1
    // args is any number of messages in a hierarchical matter
    progressReporters.set(compilation.compiler, (p, ...args) => {handler(percentage, "sealing", title, tap.name, ...args);
    });
    handler(percentage, "sealing", title, tap.name);
  }
});

基本上能够猜测出,afterOptimizeModules 的设计初衷就是用于告诉优化行为的完结。

apply 尽管是一个函数,然而从设计上就只有输出,webpack 不 care 输入,所以在插件中只能通过调用类型实体的各种办法来或者更改实体的配置信息,变更编译行为。例如:

  • compilation.addModule:增加模块,能够在原有的 module 构建规定之外,增加自定义模块
  • compilation.emitAsset:直译是“提交资产”,性能能够了解将内容写入到特定门路

到这里,插件的工作机理和写法曾经有一个很浅显的介绍了,回头单拎进去细讲吧。

How: 如何影响编译状态

解决上述两个问题之后,咱们就能了解“如何将特定逻辑插入 webpack 编译过程”,接下来才是重点 —— 如何影响编译状态?强调一下,webpack 的插件体系与平时所见的 订阅 / 公布 模式差异很大,是一种十分强耦合的设计,hooks 回调由 webpack 决定何时,以何种形式执行;而在 hooks 回调外部能够通过批改状态、调用上下文 api 等形式对 webpack 产生 side effect

比方,EntryPlugin 插件:

class EntryPlugin {apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory}) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {const { entry, options, context} = this;

      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {callback(err);
      });
    });
  }
}

上述代码片段调用了两个影响 compilation 对象状态的接口:

  • compilation.dependencyFactories.set
  • compilation.addEntry

操作的具体含意能够先疏忽,这里要了解的重点是,webpack 会将上下文信息以参数或 this (compiler 对象) 模式传递给钩子回调,在回调中能够调用上下文对象的办法或者间接批改上下文对象属性的形式,对原定的流程产生 side effect。所以想熟练地编写插件,除了要了解调用机会,还须要理解咱们能够用哪一些 api,例如:

  • compilation.addModule:增加模块,能够在原有的 module 构建规定之外,增加自定义模块
  • compilation.emitAsset:直译是“提交资产”,性能能够了解将内容写入到特定门路
  • compilation.addEntry:增加入口,性能上与间接定义 entry 配置雷同
  • module.addError:增加编译错误信息

Loader 介绍

Loader 的作用和实现比较简单,容易了解,所以简略介绍一下就行了。回顾 loader 在编译流程中的失效的地位:

流程图中,runLoaders 会调用用户所配置的 loader 汇合读取、转译资源,此前的内容能够千奇百怪,但转译之后实践上应该输入规范 JavaScript 文本或者 AST 对象,webpack 能力持续解决模块依赖。

了解了这个根本逻辑之后,loader 的职责就比拟清晰了,不外乎是将内容 A 转化为内容 B,然而在具体用法层面还挺多考究的,有 pitch、pre、post、inline 等概念用于应答各种场景。

为了帮忙了解,这里补充一个示例:Webpack 案例 — vue-loader 原理剖析。

附录

源码浏览技巧

  • 避重就轻:挑软柿子捏,比方初始化过程尽管绕,然而相对来说是概念起码、逻辑最清晰的,那从这里动手摸清整个工作过程,能够习得 webpack 的一些通用套路,例如钩子的设计与作用、编码规定、命名习惯、内置插件的加载逻辑等,相当于先入了个门
  • 学会调试:多用 ndb 单点调试性能追踪程序的运行,尽管 node 的调试有很多种办法,然而我集体更举荐 ndb,灵便、简略,配合 debugger 语句是大杀器
  • 了解架构:某种程度上能够将 webpack 架构简化为 compiler + compilation + plugins,webpack 运行过程中只会有一个 compiler;而每次编译 —— 包含调用 compiler.run 函数或者 watch = true 时文件产生变更,都会创立一个 compilation 对象。了解这三个外围对象的设计、职责、合作,差不多就能了解 webpack 的外围逻辑了
  • 抓大放小: plugin 的要害是“钩子”,我倡议策略上器重,战术上漠视!钩子毕竟是 webpack 的要害概念,是整个插件机制的根基,学习 webpack 基本不可能绕过钩子,然而相应的逻辑跳转切实太绕太不直观了,看代码的时候始终揪着这个点的话,复杂性会剧增,我的教训是:

    • 认真看一下 tapable 仓库的文档,或者粗略看一下 tapable 的源码,了解同步钩子、异步钩子、promise 钩子、串行钩子、并行钩子等概念,对 tapable 提供的事件模型有一个较为精密的认知,这叫策略上器重
    • 遇到不懂的钩子别慌,我的教训我连这个类都不分明干啥的,要去了解这些钩子切实太难了,不如先略过钩子自身的含意,去看那些插件用到了它,而后到插件哪里去加 debugger 语句单点调试,等你缕清后续逻辑的时候,大概率你也晓得钩子的含意了,这叫战术上漠视
  • 放弃好奇心:学习过程放弃旺盛的好奇心和韧性,长于 \& 敢于提出问题,而后基于源码和社区材料去总结出本人的答案,问题可能会很多,比方:

    • loader 为什么要设计 pre、pitch、post、inline?
    • compilation.seal 函数外部设计了很多优化型的钩子,为什么须要辨别的这么细?webpack 设计者对不同钩子有什么预期?
    • 为什么须要那么多 module 子类?这些子类别离在什么时候被应用?

ModuleModule 子类

从上文能够看出,webpack 构建阶段的外围流程基本上都围绕着 module 开展,置信接触过、用过 Webpack 的读者对 module 应该曾经有一个理性认知,然而实现上 module 的逻辑是非常复杂沉重的。

以 webpack\@5.26.3 为例,间接或间接继承自 Module (webpack/lib/Module.js 文件) 的子类有 54 个:

无奈复制加载中的内容

要一个一个捋分明这些类的作用切实太累了,咱们须要抓住实质:module 的作用是什么?

module 是 webpack 资源解决的根本单位,能够认为 webpack 对资源的门路解析、读入、转译、剖析、打包输入,所有操作都是围绕着 module 开展的。有很多文章会说 module = 文件,其实这种说法并不精确,比方子类 AsyncModuleRuntimeModule 就只是一段内置的代码,是一种资源而不能简略等价于理论文件。

Webpack 扩展性很强,包含模块的解决逻辑上,比如说入口文件是一个一般的 js,此时首先创立 NormalModule 对象,在解析 AST 时发现这个文件里还蕴含了异步加载语句,例如 requere.ensure,那么相应地会创立 AsyncModuleRuntimeModule 模块,注入异步加载的模板代码。下面类图的 54 个 module 子类都是为适配各种场景设计的。

正文完
 0