Skip to content
New issue

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

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

Already on GitHub? Sign in 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事件

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

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

No branches or pull requests

1 participant