webpack - 核心原理

344 阅读7分钟

一. webpack 介绍

企业微信截图_16370556222730.png

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

webpack 思维导图.png

二. webpack 编译流程解析

核心流程.png

  • init - 初始化阶段

    初始化阶段.png

    初始化思维导图.png

    启动.png

    • process.args + webpack.config.js 合并成用户配置

    • 调用 validateSchema 校验配置

    • 调用 getNormalizedWebpackOptions(获取标准化的配置项) + applyWebpackOptionsBaseDefaults(获取默认配置项) 合并出最终配置

    • 创建 compiler 对象

      createCompiler.png

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

    • 调用定义在 lib/WebpackOptionsApply.js 文件中的 WebpackOptionsApply.process 方法,内部根据 entry 配置决定注入 entry 相关的插件

    • 实例化 Compiler 触发 lib/Compiler.js 的 compile 函数,compile 函数内部定义了**this.hooks.make.callAsync** 用于监听编译模块时的异步回调

      this.hooks.make.callAsync.png

    • 在 调用 WebpackOptionsApply.process 触发 EntryPlugin监听 compiler.make 钩子 的 make 回调时,在回调中执行 compilation.addEntry 函数

      compiler.make.png

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

      addEntry.png

      addModuleTree.png

      handleModuleCreation.png

    • Compiler 和 Compilation 的区别

      webpack 打包离不开 Compiler 和 Compilation,它们两个分工明确,理解它们是我们理清 webpack 构建流程重要的一步。

      Compiler 负责监听文件和启动编译它可以读取到 webpack 的 config 信息,整个 Webpack 从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的 run,make,compile,finish,seal,emit 等,我们写的插件就是作用在这些暴露方法的 hook 上。

      Compilation 负责构建编译。每一次编译(文件只要发生变化,)就会生成一个 Compilation 实例,Compilation 可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展。

  • build - 构建阶段

    编译阶段.png

    build .png

    • 启动编译(run/watch 阶段)

      如果是监听文件(如:--watch)的模式,则会传递监听的 watchOptions,生成 Watching 实例,每次变化都重新触发回调。

      如果不是监视模式就调用 Compiler 对象的 run 方法,befornRun->beforeCompile->compile->thisCompilation->compilation开始构建整个应用。

      build1.png

    • 编译模块:(make 阶段)

      • 从 entry 入口配置文件出发, 调用所有配置的 Loader 对模块进行处理,

        addEntry.png

        handleModuleCreation.png

      • 再找出该模块依赖的模块, 通过 acorn 库生成模块代码的 AST 语法树,形成依赖关系树(每个模块被处理后的最终内容以及它们之间的依赖关系)

        ast.png

      • 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环每个依赖;再递归本步骤直到所有入口依赖的文件都经过了对应的 loader 处理。

      • 解析结束后,webpack 会把所有模块封装在一个函数里,并放入一个名为 modules 的数组里。

      • 将 modules 传入一个自执行函数中,自执行函数包含一个 installedModules 对象,已经执行的代码模块会保存在此对象中。

      • 最后自执行函数中加载函数(webpack__require)载入模块。

    • build - 构建阶段 Webpack 问题反思:

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

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

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

      • Grant、Gulp 仅执行开发者预定义的任务流;而 webpack 则深入处理资源的内容,功能上更强大
  • generate - 生成阶段

    生成阶段.png

    • 输出资源:(seal 阶段)

      在编译完成后,调用 compilation.seal 方法封闭,生成资源,这些资源保存在 compilation.assetscompilation.chunk

      1. 遍历 compilation.modules ,记录下模块与 chunk 关系

        遍历 compilation.modules ,记录下模块与 chunk 关系.png

      2. 触发各种模块优化钩子,这一步优化的主要是模块依赖关系

        触发各种模块优化钩子.png

      3. 遍历 module 构建 chunk 集合

        遍历 module 构建 chunk 集合.png

      4. 触发各种优化钩子,其中触发的 optimizeChunks 钩子,这个时候已经跑完主流程的逻辑,得到 chunks 集合,SplitChunksPlugin 正是使用这个钩子,分析 chunks 集合的内容,按配置规则增加一些通用的 chunk

        触发各种优化钩子.png

        然后便会调用 emit 钩子,根据 webpack config 文件的 output 配置的 path 属性,将文件输出到指定的 path.

    • 输出完成:done/failed 阶段

      done 成功完成一次完成的编译和输出流程。

      failed 编译失败,可以在本事件中获取到具体的错误原因

      在确定好输出内容后, 根据配置确定输出的路径和文件名, 把文件内容写入到文件系统。

    ```
    
    emitAssets(compilation, callback) {
        const emitFiles = (err) => {
          //...省略一系列代码
          // afterEmit:文件已经写入磁盘完成
          this.hooks.afterEmit.callAsync(compilation, (err) => {
            if (err) return callback(err)
            return callback()
          })
        }
    
        // emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会)
        this.hooks.emit.callAsync(compilation, (err) => {
          if (err) return callback(err)
          outputPath = compilation.getPath(this.outputPath, {})
          mkdirp(this.outputFileSystem, outputPath, emitFiles)
        })
      }
    ```
    
  • webpack 流程总结

资源形态流转.png

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 写入文件系统

  • webpack 输出文件代码分析

    (function (modules) {
      // webpackBootstrap
      // 缓存 __webpack_require__ 函数加载过的模块,提升性能,
      var installedModules = {}
    
      /**
       * Webpack 加载函数,用来加载 webpack 定义的模块
       * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js"
       * @returns {Object} exports 导出对象
       */
      function __webpack_require__(moduleId) {
        // 重复加载则利用缓存,有则直接从缓存中取得
        if (installedModules[moduleId]) {
          return installedModules[moduleId].exports
        }
        // 如果是第一次加载,则初始化模块对象,并缓存
        var module = (installedModules[moduleId] = {
          i: moduleId,
          l: false,
          exports: {},
        })
    
        // 把要加载的模块内容,挂载到module.exports上
        modules[moduleId].call(
          module.exports,
          module,
          module.exports,
          __webpack_require__
        )
        module.l = true // 标记为已加载
    
        // 返回加载的模块,直接调用即可
        return module.exports
      }
      // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ...
      __webpack_require__.m = modules
      __webpack_require__.c = installedModules
      __webpack_require__.d = function (exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
          Object.defineProperty(exports, name, {
            enumerable: true,
            get: getter,
          })
        }
      }
      __webpack_require__.n = function (module) {
        var getter =
          module && module.__esModule
            ? function getDefault() {
                return module['default']
              }
            : function getModuleExports() {
                return module
              }
        __webpack_require__.d(getter, 'a', getter)
        return getter
      }
      // __webpack_require__对象下的r函数
      // 在module.exports上定义__esModule为true,表明是一个模块对象
      __webpack_require__.r = function (exports) {
        if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
          Object.defineProperty(exports, Symbol.toStringTag, {
            value: 'Module',
          })
        }
        Object.defineProperty(exports, '__esModule', {
          value: true,
        })
      }
      __webpack_require__.o = function (object, property) {
        return Object.prototype.hasOwnProperty.call(object, property)
      }
      //  从入口文件开始执行
      return __webpack_require__((__webpack_require__.s = './src/index.js'))
    })({
      './src/index.js': function (
        module,
        __webpack_exports__,
        __webpack_require__
      ) {
        'use strict'
        eval(
          '__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ "./src/moduleA.js");\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_moduleA__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./moduleB */ "./src/moduleB.js");\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_moduleB__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\n//# sourceURL=webpack:///./src/index.js?'
        )
      },
      './src/moduleA.js': function (module, exports) {
        eval('console.log("moduleA")\n\n//# sourceURL=webpack:///./src/moduleA.js?')
      },
      './src/moduleB.js': function (module, exports) {
        // 代码字符串可以通过eval 函数运行
        eval('console.log("moduleB")\n\n//# sourceURL=webpack:///./src/moduleB.js?')
      },
    })
    

    上述代码的实现了⼀个 webpack_require 来实现⾃⼰的模块化把代码都缓存在 installedModules ⾥,代码⽂件以对象传递进来,key 是路径,value 是包裹的代码字符串,并且代码内部的 require,都被替换成了 webpack_require,代码字符串可以通过 eval 函数去执行。

    bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

    总结一下,生成的 bundle.js只包含一个立即调用函数(IIFE),这个函数会接受一个对象为参数,它其实主要做了两件事:

    1. 定义一个模块加载函数 webpack_require。
    2. 使用加载函数加载入口模块 "./src/index.js",从入口文件开始递归解析依赖,在解析的过程中,分别对不同的模块进行处理,返回模块的 exports