chst365's blog chst365's blog
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

chst365

DIV工程师
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 浏览器

  • webpack

  • TypeScript

  • 性能

  • 工程化

    • webpack-loader机制
    • webpack-插件机制
    • webpack工作流程
      • 准备工作
    • Babel原理
    • uglify-js代码是如何被压缩的
    • Tree-Shaking性能优化
    • 前端模块化机制
    • 前端微服务
    • Vite为什么快?快在哪?
    • npm script了解多少?
  • React

  • JavaScript

  • 编程题

  • React技术揭秘

  • 算法

  • 前端
  • 工程化
chst365
2021-04-22
目录

webpack工作流程

# 准备工作

  1. vscode 中配置

  2. webpack.config.js 配置

    var path = require("path");
    var node_modules = path.resolve(__dirname, "node_modules");
    var pathToReact = path.resolve(node_modules, "react/dist/react.min.js");
    
    module.exports = {
      // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
      entry: {
        bundle: [
          "webpack/hot/dev-server",
          "webpack-dev-server/client?http://localhost:8080",
          path.resolve(__dirname, "app/app.js"),
        ],
      },
      // 文件路径指向(可加快打包过程)。
      resolve: {
        alias: {
          react: pathToReact,
        },
      },
      // 生成文件,是模块构建的终点,包括输出文件与输出路径。
      output: {
        path: path.resolve(__dirname, "build"),
        filename: "[name].js",
      },
      // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
      module: {
        loaders: [
          {
            test: /\.js$/,
            loader: "babel",
            query: {
              presets: ["es2015", "react"],
            },
          },
        ],
        noParse: [pathToReact],
      },
      // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
      plugins: [new webpack.HotModuleReplacementPlugin()],
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    webpack 的一些核心概念:

    • loader: 能转换各类资源,并处理成对应模块的加载器。loader 间可以串行使用。

    • chunk: code splitting 后的产物,也就是按需加载的分块,装载了不同的 module

      对于 module 和 chunk 的关系可参考 webpack 官方的这张图:

    • plugin: webpack 的插件实体,这里以 UglifyJsPlugin 为例

      function UglifyJsPlugin(options) {
        this.options = options;
      }
      
      module.exports = UglifyJsPlugin;
      
      UglifyJsPlugin.prototype.apply = function (compiler) {
        compiler.plugin("compilation", function (compilation) {
          compilation.plugin("build-module", function (module) {});
          compilation.plugin(
            "optimize-chunk-assets",
            function (chunks, callback) {
              // Uglify 逻辑
            }
          );
          compilation.plugin("normal-module-loader", function (context) {});
        });
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      在 webpack 中经常看到 compilation.plugin(‘xxx’, callback) ,可以当作是一个事件的绑定,这些事件在打包时由 webpack 来触发。

  3. 流程总览

# shell 与 config 解析

每次在命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack 这个 shell 脚本。这个脚本会去调用 ./node_modules/webpack/bin/webpack.js 并追加输入的参数,如 -p , -w 。 (图中 webpack.js 是 webpack 的启动文件,而 $@ 是后缀参数)

在 webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。

  1. optimist 和 commander 一样,optimist 实现了 node 命令行的解析,其 API 调用非常方便。
var optimist = require("optimist");

optimist
  .boolean("json").alias("json", "j").describe("json")
  .boolean("colors").alias("colors", "c").describe("colors")
  .boolean("watch").alias("watch", "w").describe("watch")
  ...
1
2
3
4
5
6
7

获取到后缀参数后,optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中,来看看 argv 究竟有什么?

// webpack --hot -w
{
  hot: true,
  profile: false,
  watch: true,
  ...
}
1
2
3
4
5
6
7
  1. config 合并与插件加载 在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到./node_modules/webpack/bin/convert-argv.js 中,通过判断 argv 中参数的值决定是否去加载对应插件。

    ifBooleanArg("hot", function() {
    ensureArray(options, "plugins");
    var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin");
    options.plugins.push(new HotModuleReplacementPlugin());
    });
    ...
    return options;
    
    1
    2
    3
    4
    5
    6
    7

    options 作为最后返回结果,包含了之后构建阶段所需的重要信息。

    {
         entry: {},//入口配置
         output: {}, //输出配置
         plugins: [], //插件集合(配置文件 + shell指令)
         module: { loaders: [ [Object] ] }, //模块配置
         context: //工程路径
         ...
     }
    
    1
    2
    3
    4
    5
    6
    7
    8

    这和 webpack.config.js 的配置非常相似,只是多了一些经 shell 传入的插件对象。插件对象一初始化完毕, options 也就传入到了下个流程中。

    var webpack = require("../lib/webpack.js");
    var compiler = webpack(options);
    
    1
    2

# 编译与构建流程

在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 lib/webpack.js 中,如下:

function webpack(options) {
  var compiler = new Compiler();
  ...// 检查options,若watch字段为true,则开启watch线程
  return compiler;
}
...
1
2
3
4
5
6

webpack 的实际入口是 Compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点。

  • compile 开始编译
  • make 从入口点分析模块及其依赖的模块,创建这些模块对象
  • build-module 构建模块
  • after-compile 完成构建
  • seal 封装构建结果
  • emit 把各个 chunk 输出到结果文件
  • after-emit 完成输出
  1. 核心对象 Compilation compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象

这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可从图中看到比较关键的步骤,如addEntry(),_addModuleChain(),buildModule(),seal(),createChunkAssets(),在每一个节点都会触发 webpack 事件去调用各插件。二是该对象内部存放所有 module,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息

  1. 编译与构建主流程 在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry方法,通过 options 对象的 entry 字段找到我们的入口 js 文件。之后,在 addEntry 中调用私有方法_addModuleChain,此方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。 构建模块作为最耗时的一步,又可细化为三步:

    • 调用各 loader 处理模块间的依赖 webpack 提供一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader,如css-loader等来让我们可直接在源文件中引用各类资源。webpack 调用 doBuild(),对每一个 require()用对应的 loader 进行加工,最后生成一个 js module.
      Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
         var start = this.profile && +new Date();
         ...
         // 根据模块的类型获取对应的模块工厂并创建模块
         var moduleFactory = this.dependencyFactories.get(dependency.constructor);
         ...
         moduleFactory.create(context, dependency, function(err, module) {
           var result = this.addModule(module);
           ...
           this.buildModule(module, function(err) {
             ...
             // 构建模块,添加依赖模块
           }.bind(this));
         }.bind(this));
       };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
    • 调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST
      Parser.prototype.parse = function parse(source, initialState) {
        var ast;
        if (!ast) {
          // acorn以es6的语法进行解析
          ast = acorn.parse(source, {
            ranges: true,
            locations: true,
            ecmaVersion: 6,
            sourceType: "module"
          });
        }
        ...
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
    • 遍历 AST,构建该模块所依赖的模块 对于当前模块,或许存在多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 是,将 require()中的模块通过addDependency()添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies开始递归处理依赖的 module
      Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
         // 根据依赖数组(dependencies)创建依赖模块对象
         var factories = [];
         for (var i = 0; i < dependencies.length; i++) {
           var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
           factories[i] = [factory, dependencies[i]];
         }
         ...
         // 与当前模块构建步骤相同
       }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
  2. 构建细节 module 是 webpack 构建的核心实体,也是所有 module 的父类,它有几种不同子类:NormalModule,MultiModule,ContextModule,DelegatedModule等。但这些核心实体都是在构建中都会去调用对应方法,也就是build()

    // 初始化module信息,如context,id,chunks,dependencies等。
    NormalModule.prototype.build = function build(
      options,
      compilation,
      resolver,
      fs,
      callback
    ) {
      this.buildTimestamp = new Date().getTime(); // 构建计时
      this.built = true;
      return this.doBuild(
        options,
        compilation,
        resolver,
        fs,
        function (err) {
          // 指定模块引用,不经acorn解析
          if (options.module && options.module.noParse) {
            if (Array.isArray(options.module.noParse)) {
              if (
                options.module.noParse.some(function (regExp) {
                  return typeof regExp === "string"
                    ? this.request.indexOf(regExp) === 0
                    : regExp.test(this.request);
                }, this)
              ) {
                return callback();
              }
            } else if (
              typeof options.module.noParse === "string"
                ? this.request.indexOf(options.module.noParse) === 0
                : options.module.noParse.test(this.request)
            ) {
              return callback();
            }
          }
          // 由acorn解析生成ast
          try {
            this.parser.parse(this._source.source(), {
              current: this,
              module: this,
              compilation: compilation,
              options: options,
            });
          } catch (e) {
            var source = this._source.source();
            this._source = null;
            return callback(new ModuleParseError(this, source, e));
          }
          return callback();
        }.bind(this)
      );
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    对于每一个 module,它都会有这样一个构建方法。它还包括了从构建到输出的一系列的有关 module 生命周期的函数,我们通过 module 父类类图其子类类图(这里以 NormalModule 为例)来观察其真实形态:

# 打包输出

在所有模块及其依赖模块 build 完成后,webpack 会监听seal事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash。同时这是我们在开发是进行代码优化和功能添加的关键环节。

Compilation.prototype.seal = function seal(callback) {
  this.applyPlugins("seal"); // 触发插件的seal事件
  this.preparedChunks.sort(function(a, b) {
    if (a.name < b.name) {
      return -1;
    }
    if (a.name > b.name) {
      return 1;
    }
    return 0;
  });
  this.preparedChunks.forEach(function(preparedChunk) {
    var module = preparedChunk.module;
    var chunk = this.addChunk(preparedChunk.name, module);
    chunk.initial = chunk.entry = true;
    // 整理每个Module和chunk,每个chunk对应一个输出文件。
    chunk.addModule(module);
    module.addChunk(chunk);
  }, this);
  this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err) {
    if (err) {
      return callback(err);
    }
    ... // 触发插件的事件
    this.createChunkAssets(); // 生成最终assets
    ... // 触发插件的事件
  }.bind(this));
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 生成最终 assets 在封装过程中,webpack 会调用 Compilation 中的createChunkAssets方法进行打包后代码的生成。createChunkAssets 流程如下:
    • 不同的 Template 从上图可看出通过判断是入口 js 还是需要异步加载的 js 来选择不同的模板对象进行封装,入口 js 会采用 webpack 事件流的 render 事件来触发 Template 类中的renderChunkModules()(异步加载的 js 会调用 chunkTemplate 中的 render 方法)
      if (chunk.entry) {
        source = this.mainTemplate.render(
          this.hash,
          chunk,
          this.moduleTemplate,
          this.dependencyTemplates
        );
      } else {
        source = this.chunkTemplate.render(
          chunk,
          this.moduleTemplate,
          this.dependencyTemplates
        );
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      在 webpack 中 y0ou 四个 Template 的子类,分别是MainTemplate.js,ChunkTemplate.js,ModuleTemplate.js,HotUpdateChunkTemplate.js,前两者先前已介绍,而 ModuleTemplate 模块进行一个代码生成,HotUpdateChunkTemplate 是对热替换模块的一个处理。
    • 模块封装 模块在封装的时候和他在构建时一样,都是调用各模块类中的方法。封装通过调用 module.source()来进行各操作,如 require()的替换:
       MainTemplate.prototype.requireFn = "__webpack_require__";
       MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
           var buf = [];
           // 每一个module都有一个moduleId,在最后会替换。
           buf.push("function " + this.requireFn + "(moduleId) {");
           buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
           buf.push("}");
           buf.push("");
           ... // 其余封装操作
       };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
    • 生成 assets 各模块进行 doBlock 后,把 module 的最终代码循环添加到 source 中。一个 source 对应着一个 asset 对象,该对象保存了单个文件的文件名( name )和最终代码( value )。
  2. 输出 最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。
#前端#工程化
上次更新: 2021/12/14, 23:27:23
webpack-插件机制
Babel原理

← webpack-插件机制 Babel原理→

最近更新
01
面试官
03-27
02
this&指针&作用域&闭包
03-27
03
前端
03-27
更多文章>
Theme by Vdoing | Copyright © 2019-2025 chst365 | 豫ICP备17031889号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式