Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Webpack打包流程构建原理 #6

Open
impeiran opened this issue Apr 28, 2020 · 0 comments
Open

Webpack打包流程构建原理 #6

impeiran opened this issue Apr 28, 2020 · 0 comments

Comments

@impeiran
Copy link
Owner

前言:此文经过研究《深入浅出Webpack》和 目前webpack源码 来编写

webpack里的几个基本概念:

  • Entry:执行构建的入口,可抽象成输入,并且可以有多个entry。
  • Module:在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始,递归找出所有依赖的模块。
  • Loader:模块加载器,用于对模块的原内容按照需求进行加载转换。
  • Plugin:插件,在Webpack构建流程中的特定时,广播对应的事件,此时插件可以监听这些事件的发生,在特定的时机做对应的事情。
  • Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  • Bundle:打包后产出的一个/多个文件,常见配以[chunk-id] + Hash命名。

整个构建流程大致分为三个部分:

  1. 初始化阶段
  2. 编译阶段
  3. 输出阶段

接下来将从这三个阶段细分,讲讲webpack做了哪些工作:

初始化阶段

  1. 初始化参数

    从配置文件和Shell语句中读取参数并合并,得出最终的配置。

    Shell语句的处理一般由webpack-cli命令行库工具执行,包括--config读取的配置文件,最后才将参数option传递给webpack。这就是为什么使用Webpack时需要安装这两个lib。

    期间如果配置文件(如: webpack.config.js)中Plugins使用了new plugin()之类的语句,则会一并调用,实例化插件对象。

  2. 实例化Compiler

    用得到的参数option初始化Compiler实例,实例中包含了完整的Webpack默认配置。简化代码如下:

    const webpack = (options, callback) => {
      let compiler
      if (Array.isArray(options)) {
       	// ...
        compiler = createMultiCompiler(options);
      } else {
        compiler = createCompiler(options);
      }
      // ...
      return compiler; 
    }

    一般全局只有一个compiler(多份配置option则有多个compiler),并向外暴露run方法进行启动编译。Compiler是负责管理webpack整个打包流程的“ 主人公 ”。

    Compiler主要负责进行:文件监听与编译,初始化编译过程中的事件Hook,到了v4末版本的时候,Hook已多达二十多个,具体点击可查看

    Compiler类中还声明了用于创建子编译对象childCompiler的方法

    /**
      * @param {Compilation} compilation the compilation
      * @param {string} compilerName the compiler's name
      * @param {number} compilerIndex the compiler's index
      * @param {OutputOptions} outputOptions the output options
      * @param {WebpackPluginInstance[]} plugins the plugins to apply
      * @returns {Compiler} a child compiler
    */
    createChildCompiler(
      compilation,
      compilerName,
      compilerIndex,
      outputOptions,
      plugins
    ) {}

    用于Loader/插件有需要时创建,执行模块的分开编译。

  3. Environment

    应用Node的文件系统到compiler对象,方便后续的文件查找和读取

    new NodeEnvironmentPlugin({
      infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
  4. 加载插件

    依次调用插件的apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。

    if (Array.isArray(options.plugins)) {
      for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
          plugin.call(compiler, compiler);
        } else {
          plugin.apply(compiler);
        }
      }
    }
  5. 应用默认的Webpack配置

    applyWebpackOptionsDefaults(options);
    // 随即之后,触发一些Hook
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();

    除了一些默认的文件系统上下文contextresolver,以及处理的文件输出方式,这里的要应用的默认配置在v4包含新的performance性能优化、Optimization打包优化。

至此,完成了整个第一阶段的初始化。

编译阶段

  1. 启动编译

    这里有个小逻辑区分是否是watch,如果是非watch,则会正常执行一次compiler.run()

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

    function watch(watchOptions, handler) {
      if (this.running) {
        return handler(new ConcurrentCompilationError());
      }
    
      this.running = true;
      this.watchMode = true;
      return new Watching(this, watchOptions, handler);
    }
  2. 触发compile事件

    该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。

  3. Compilation

    这是整个webpack构建打包的关键。每一次的编译(包括watch检测到文件变化时),compiler都会创建一个Compilation对象,标识当前的模块资源、编译生成资源、变化的文件等。同时也提供很多事件回调给插件进行拓展。

    Compilation的生成,是在compiler执行compile方法时构造的,主要流程大概是:触发compile事件后,执行this.newCompilation获取新一轮的compilation,并作为参数触发make事件。然后异步执行此次

    compile (callback) {
      const params = this.newCompilationParams();
      this.hooks.beforeCompile.callAsync(params, err => {
        // ...
      	this.hooks.compile.call(params);
        const compilation = this.newCompilation(params);
        
        // ...
        this.hooks.make.callAsync(compilation, err => {
          //...
          process.nextTick(() => {
    					compilation.finish(err => {
              // ...完成
                this.hooks.afterCompile()
              }
          }                           
      }
    }

    当中还设计到两个主要的钩子:

    • complication:这其实是一个同名的hook,是在上述代码this.newComplication()中调用的,当其调用时已完成complication的实例化。

    • make:表示一个新的Complication创建完毕。

    • after-compile:表示一次Compilation执行完成

    在complication实例化的阶段,调用了Loader转换模块,并将原有的内容结合输出对应的抽象语法树(AST),并递归的分析其导入语句(如import等),最终梳理所有模块的依赖关系形成依赖图谱。

    当所有模块都经过Loader转换完成,此时触发complication的seal事件,根据依赖关系和配置开始着手生成chunk

输出阶段

  1. should-emit事件

    所有需要输出的文件已经生成,询问插件有哪些文件需要输出,哪些不需要输出。

  2. emit事件

    确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出的内容。

  3. after-emit事件

    文件输出完毕

  4. done事件

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

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant