入口文件

查找入口文件

  • 通过配置npm script的npm run build等
  • 通过Webpack直接运行

webpack entry.js bundle.js

看这张图:
image.png

为什么说实际入口文件是webpack包下面的bin文件夹?因为.bin目录下的文件执行之后还是去在对应的依赖包文件夹下执行对应的命令比如(webpack),根据package.json文件里面的配置找到webpack\bin\webpack.js,然后执行该文件,开始打包构建工作。

入口文件的代码

image.png

结果

webpack最终找到了webpack-cli(webpack-command)这个npm包,并且执行CLI

webpack-cli做的事情

  1. 引入yargs,对命令行进行定制
  2. 分析命令行参数,对各个参数进行转换,组成编译配置项
  3. 引入webpack,根据配置项进行编译和构建

源码解析

起步:处理不需要经过编译的命令

image.png

先将这些不需要经过的命令以静态变量的形式存放在webpack-cli\bin\utils\constants.js。
image.png

如果输入的命令属于这些不需要的编译的命令,那么就require一个文件bin\utils\prompt-command.js,在这个文件中,会根据输入的命令(不需要经过编译的命令)去引入对应的包(@webpack-cli/ + pageages),如果局部webpack-cli中没有,就去require global-modules这个模块,然后再从这个global-modules这个模块里面找。如果还是没有就报错。

报错之后会提示是否要安装这个命令对应的模块。然后输入的是yes,y,1其中一个都会进行安装。

yargs

image.png
bin\config\config-args

将yargs这个依赖包引入之后,调用这个依赖导出的对象中的useage方法,传参数得到一个新的对象。再将这个新的对象传进bin\config\config-args导出的中。我猜应该是将yargs进行一波补强了(可能是添加了parse这个函数)。

通过process.argv.slice(2)获取在命令行中输入的信息,通过yargs.parse()对输入的参数进行解析

输入的参数

image.png

如果获取到的输入的参数命令属于查看的比如—help,那么就会在yargs.parse()的第二个参数就是函数中有传入output这个参数,可以直接获取到输出直接return。如果是配置相关的参数,那么parse内部会将这些参数和配置文件webpack.config.js中的配置进行合并,在第二个参数就是函数中 传入argv这个参数。这个就是合并好的配置。
将配置赋值给option,然后引入convert-argv.js,将option配置转换成webpack能跑的配置。

将option处理完了之后,然后经过processOption函数,将option.stas赋值给outputOption,这个函数和convert-argv.js类似就是逐个去判断outputOption对象里面的属性,对每种需要判断的属性进行处理。比如如果json属性有,那么就将json属性值再赋值为true。

处理完成outputOptions之后(也就是对option的stas属性部分进行了处理)。然后创建一个webpack的实例。
就是引入webpack,然后将前面组装的option传入,将这个webpack的实例赋值给compiler对象。

  1. const webpack = require("webpack");
  2. let lastHash = null;
  3. let compiler;
  4. try {
  5. compiler = webpack(options);
  6. } catch (err) {
  7. if (err.name === "WebpackOptionsValidationError") {
  8. if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
  9. else console.error(err.message);
  10. // eslint-disable-next-line no-process-exit
  11. process.exit(1);
  12. }
  13. throw err;
  14. }
  15. if (argv.progress) {
  16. const ProgressPlugin = require("webpack").ProgressPlugin;
  17. new ProgressPlugin({
  18. profile: argv.profile
  19. }).apply(compiler);
  20. }

至于底下的这个progress,是在argv上面进行判断的,也就是命令行参数和webpack.config.js的合体。应该是前面对于option组装过程中的“漏网之鱼”,progress这个对象留到这里来进行判断处理。可以看到这里引入了webpack自带的一个插件,并且new了一个插件的实例,调用实例中的apply方法,将compiler传了进去。

最后还有一个compilerCallBack,就是将结果输出的。
然后进行判断,如果有watch那就按照watch的步骤走,其余的调用compilerCallBack,输出结果。

总结来说:
image.png

Webpack & Compiler & Compilation

上面最后讲到了cli运行到后面是进入到了webpack的创建实例阶段,下面我们继续接着讲webpack。

Webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

  1. if (firstOptions.watch || options.watch) {
  2. const watchOptions =
  3. firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
  4. if (watchOptions.stdin) {
  5. process.stdin.on("end", function(_) {
  6. process.exit(); // eslint-disable-line
  7. });
  8. process.stdin.resume();
  9. }
  10. compiler.watch(watchOptions, compilerCallback);
  11. if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
  12. } else {
  13. compiler.run((err, stats) => {
  14. if (compiler.close) {
  15. compiler.close(err2 => {
  16. compilerCallback(err || err2, stats);
  17. });
  18. } else {
  19. compilerCallback(err, stats);
  20. }
  21. });
  22. }

这是cli最后的一段代码,可以看到最后是compiler.run(),然后把compilerCallBack输出函数传了进去。

Webpack

webpack\lib\webpack.js
这里就是看上面,将options传入给webpack这个函数之后,是怎么创建一个webpack的实例的。

这段代码就是说,如果传入的options是数组有多个option,就创建多个compiler实例

  1. const Compiler = require("./Compiler");
  2. ...
  3. let compiler;
  4. if (Array.isArray(options)) {
  5. compiler = new MultiCompiler(
  6. Array.from(options).map(options => webpack(options))
  7. );
  8. } else if (typeof options === "object") {
  9. options = new WebpackOptionsDefaulter().process(options);
  10. compiler = new Compiler(options.context);
  11. compiler.options = options;
  12. ...
  13. compiler.hooks.environment.call();
  14. compiler.hooks.afterEnvironment.call();
  15. compiler.options = new WebpackOptionsApply().process(options, compiler);
  16. }

可以看到这里有个compiler,再去看看Compiler.js这个文件

Compiler

Compiler这个类继承了Tapable(水龙头)类,Compiler这个类中有很多hooks,这些hooks在construct函数中有定义。
在这个文件中还能见到另一个很重要的对象 Compilation

Compilation

Compilation也继承自Tapable

Tapable 的插件架构和 Hooks设计

Tapable原理类似于发布订阅模式,也可以知道webpack内部也是由各种各样的插件,监听compiler和compilation中定义的一些关键的事件节点。
image.png

Tapable是一个类似Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着Webpack的插件系统。

Tapable的钩子

Tapable库暴露了很多Hook类,为插件提供挂载的钩子,如下:
image.png
对于这九种钩子可以分为以下几种类型:
image.png

Tapable的使用——new Hook 新建钩子

image.png

绑定钩子

Tapable提供了同步 & 异步绑定钩子的方法,并且他们都有绑定时间和执行事件对应的方法。
image.png

hook的使用示例

简单例子:

image.png

实际例子:

image.png

Tapable 与 Webpack的联系

image.png
先从这段webpack\lib\webpack.js里面的代码开始看。传入options之后,对是否多个options进行判断。如果只有一个,那么就进行webpack的初始化工作,然后new一个新的compiler对象。

然后引入NodeEnbironmentPlugin这个插件,可以看出,插件的使用就是new一个新实例,然后调用apply方法(自定义的),传入一个compiler对象。

接着对传入的options配置对象进行插件的判断,将插件一个个apply到这个新的compiler对象上

除了配置上指定的插件,还有一些内部默认启动的插件,看这个new WebpackOtionsApply(),就是把webpack内部的插件都注入进这个compiler中。

模拟Compiler.js

image.png
Compiler的代码在npm上面也能找得到。这段是根据上面的具体例子Car改造的。

模拟小型插件

image.png

模拟插件运行

image.png
当然,webpack的run函数就是开始构建打包,run伴随着内部的事件机制的执行。

WebpackOtionsApply

说完了webpack执行前大概步骤之后,来回头讲一下这个WebpackOtionsApply,也就是为一个compiler注入一些Webpack内置的插件的方法。

主要是根据option.target进行判断,不同的字段比如web, webworker,node等等字段,就apply不同的插件。

其次还有一些优化的插件,代码需要看一下。

Webpack的构建流程

上面讲完了webpack开始构建之前的工作,最后一步就是新建一个compiler实例,然后将配置中的插件apply进这个实例当中,也就是给tapable挂上了很多钩子函数Hooks。那么接下来会做什么事情呢?

webpack的编译按照下面的钩子调用顺序执行
image.png

compiler上有近百个Hook,但是有很多都是同类型的,这里对这些同类型的hook做了一个收归,大体的调用顺序就是如此。

下面按照三个大的基本部分来讲解流程:

  1. 初始化阶段,即准备阶段
  2. 模块构建打包阶段
  3. 构建结束之后,优化和输出到磁盘的阶段

准备阶段

还记得webpack\lib\webpack.js里面的一开始的这段代码吗,就是将option配置对象中的插件apply进compiler对象也就是注册tapable上面的钩子,前后的这一大段代码,就是属于准备阶段。

  1. if (Array.isArray(options)) {
  2. compiler = new MultiCompiler(
  3. Array.from(options).map(options => webpack(options))
  4. );
  5. } else if (typeof options === "object") {
  6. // 看这一行
  7. options = new WebpackOptionsDefaulter().process(options);
  8. compiler = new Compiler(options.context);
  9. compiler.options = options;
  10. new NodeEnvironmentPlugin({
  11. infrastructureLogging: options.infrastructureLogging
  12. }).apply(compiler);
  13. if (options.plugins && Array.isArray(options.plugins)) {
  14. for (const plugin of options.plugins) {
  15. if (typeof plugin === "function") {
  16. plugin.call(compiler, compiler);
  17. } else {
  18. plugin.apply(compiler);
  19. }
  20. }
  21. }
  22. compiler.hooks.environment.call();
  23. compiler.hooks.afterEnvironment.call();
  24. compiler.options = new WebpackOptionsApply().process(options, compiler);

WebpackOptionsDefaulter.js,对option设置一些默认的参数,属于初始化的操作。

NodeEnvironmentPlugin插件,在compiler对象上挂载一个beforeRun的钩子,这个钩子会在entry-option和run两个阶段之间执行。
他的作用就是清理构建的缓存:(就是判断compiler.inputFileSystem在这个钩子挂载时和触发时是否没有变化)

        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
            if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
        });

下面就是对option配置对象中的插件进行apply。

然后就是WebpackOptionsApply,把webpack内部默认的插件都注入进这个compiler中(挂载钩子),其中就包括了entry-option这个hook。

WebpackOptionsApply

image.png
可以看到,他这些默认的插件的注入与否,是以option的参数为基准的。比如devtool,mode这些webpack.config.js里面配置的参数,在这里就转换成对应的插件并apply到compiler对象上。

除了根据参数apply插件,还有上面说的entry-option的hook。

new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

这里apply了之后立即就进行了call。

这是EntryOptionPlugin的apply函数,大概就是在这个阶段对option.entry进行了处理。

    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            if (typeof entry === "string" || Array.isArray(entry)) {
                itemToPlugin(context, entry, "main").apply(compiler);
            } else if (typeof entry === "object") {
                for (const name of Object.keys(entry)) {
                    itemToPlugin(context, entry[name], name).apply(compiler);
                }
            } else if (typeof entry === "function") {
                new DynamicEntryPlugin(context, entry).apply(compiler);
            }
            return true;
        });
    }

那么entry-option就结束了。那么准备阶段就到此结束。

compiler

在此对compiler进行补充。compiler中有compilation对象,实例化NormalModuleFactory,ContextModuleFactory这两个工厂方法,以及其他的东西。

const Compilation = require("./Compilation");
const Stats = require("./Stats");
const Watching = require("./Watching");
const NormalModuleFactory = require("./NormalModuleFactory");
const ContextModuleFactory = require("./ContextModuleFactory");
...

class Compiler extends Tapable {
  construct() {
  this.hooks = {...}
                ...
  }
    ...
    run () {
    ...
    this.hooks.beforeRun.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.readRecords(err => {
                    if (err) return finalCallback(err);

                    this.compile(onCompiled);
                });
            });
        });
    }

compiler.run方法,执行的时候会触发beforeRun这个钩子,在回调函数中接着触发run钩子。最后的this.compile方法则是触发了beforeCompilecompilemakeafterCompile这三个钩子。make这个钩子意味着构建流程要开始了。

模块构建和Chunk生成阶段

概念补充:流程

前面提到的流程图,还可以再补充为这样:
image.png

概念补充:Compilation

流程中的某一些hooks,并不是全部由订阅方订阅的时候来确定回调函数的。还有call的时候会附带一些别的逻辑。比如在这几个阶段是调用了compilation的生命周期方法的。
image.png

概念补充:ModuleFactory

上面有提到,准备阶段创建compiler的实例的时候,创建了两个工厂对象:NormalModuleFactory和ContextModuleFactory。
image.png
NormalModuleFactory是像module.exports = function () {}
ContextModuleFactory 是比如import * from ‘./index’;

除此之外,还有这些,就是不同的模块使用方式:
image.png

使用NormalModuleFactory的时候,开始构建模块的流程:
image.png

回到代码

上面说到compiler.compile()方法则是触发了beforeCompilecompilemakeafterCompile这三个钩子。make这个钩子意味着构建流程要开始了。

    run () {
  ...
          const onCompiled = (err, compilation) => {...}
          this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);

        this.hooks.run.callAsync(this, err => {
          if (err) return finalCallback(err);

          this.readRecords(err => {
            if (err) return finalCallback(err);

            this.compile(onCompiled);
          });
        });
            });
  ...
  }

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);

                compilation.finish(err => {
                    if (err) return callback(err);

                    compilation.seal(err => {
                        if (err) return callback(err);

                        this.hooks.afterCompile.callAsync(compilation, err => {
                            if (err) return callback(err);

                            return callback(null, compilation);
                        });
                    });
                });
            });
        });
    }

newCompilationParams就是创建了NormalModuleFactory和ContextModuleFactory。

    newCompilationParams() {
        const params = {
            normalModuleFactory: this.createNormalModuleFactory(),
            contextModuleFactory: this.createContextModuleFactory(),
            compilationDependencies: new Set()
        };
        return params;
    }

那么到了make这个阶段,就是利用params进行模块依赖的构建。让我们来看一下有哪些插件订阅了make这个钩子。
image.png
这个是视频上的,grep是linux内核的系统命令,用于匹配文件中的字符。window需要使用findstr
image.png

  • SingleEntryPlugin.js

          compiler.hooks.make.tapAsync(
              "SingleEntryPlugin",
              (compilation, callback) => {
                  const { entry, name, context } = this;
    
                  const dep = SingleEntryPlugin.createDependency(entry, name);
                  compilation.addEntry(context, dep, name, callback);
              }
          );
    

    compilation.addEntry():当SingleEntryPlugin订阅在make钩子上面的这个回调函数中的compilation.addEntry()就说明make阶段正式开始,做一个入口的处理。

  • ….其他的这些钩子注册函数可以自己看

那么这些钩子函数如果挂载到compiler上面的话,当make钩子被触发的时候,这些函数都会触发执行一遍。

触发这些make的钩子函数之后,就开始模块的构建。比如触发了SingleEntryPlugin,这个钩子回调会调用compilation.addEntry方法。compilation.addEntry会调用compilation._addModuleChain这个方法。而compilation._addModuleChain最终就调用Compilation.buildModule()。

让我们来看Compilation.buildModule()方法
这里调用了module.build()方法,如果成功的话就触发Compilation.hooks.succeedModule钩子,否则的话触发failedModule钩子。

    buildModule(module, optional, origin, dependencies, thisCallback) {
        ...
        module.build(
            this.options,
            this,
            this.resolverFactory.get("normal", module.resolveOptions),
            this.inputFileSystem,
            error => {
                ...
        if (error) {
                    this.hooks.failedModule.call(module, error);
                    return callback(error);
                }
        this.hooks.succeedModule.call(module);
                return callback();
            }
        );
    }

那么module.build()做了啥呢,其实是进入到了NormalModule里面的doBuild()方法

const { getContext, runLoaders } = require("loader-runner");
class NormalModule extends Module {
  ...
      doBuild(options, compilation, resolver, fs, callback) {
      const loaderContext = this.createLoaderContext(
        resolver,
        options,
        compilation,
        fs
      );

      runLoaders(
          {
            // 静态资源的文件路径
            resource: this.resource,
            // 使用的loader
            loaders: this.loaders,
            // loader的上下文
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
            },
        (err, result) => {
          ...
          return callback();
        }
      );
        }

    build(options, compilation, resolver, fs, callback) {
     ...
     return this.dobuild(options, compilation, resolver, fs, err => {
      ... 
      try {
        // 看这个parse
                const result = this.parser.parse(
                    this._ast || this._source.source(),
                    {
                        current: this,
                        module: this,
                        compilation: compilation,
                        options: options
                    },
                    (err, result) => {
                        if (err) {
                            handleParseError(err);
                        } else {
                            handleParseResult(result);
                        }
                    }
                );
                if (result !== undefined) {
                    // parse is sync
                    handleParseResult(result);
                }
            } catch (e) {
                handleParseError(e);
            }
     })
     }
}

可以看到当dobuild完成之后会执行后面传入的callback,这个callback里面调用了parse这个方法。这个this.parser.parse是将代码里面引入的依赖,添加到模块集合modules(在Compilation中的定义的一个数组变量)中

seal阶段,第一做的就是优化。第二就是有个createHash()(调用Compilation.modifyHash()),比如图片就是hash,css是contentHash,js是ChunkHash,创建不同的hash值。
然后就是createModuleAssets()这个方法,将模块构建过程中生成的模块进行一个处理,生成一个对象,属性名为将要输出文件名,值为对应的资源内容。

    seal(callback) {
        this.hooks.seal.call();

        while (
            this.hooks.optimizeDependenciesBasic.call(this.modules) ||
            this.hooks.optimizeDependencies.call(this.modules) ||
            this.hooks.optimizeDependenciesAdvanced.call(this.modules)
        ) {
            /* empty */
        }
    // 触发了很多优化相关的hook
        ...

            this.hooks.beforeHash.call();
            this.createHash();
            this.hooks.afterHash.call();

            if (shouldRecord) {
                this.hooks.recordHash.call(this.records);
            }

            this.hooks.beforeModuleAssets.call();
            this.createModuleAssets();

            ...
    }

 createModuleAssets() {
        for (let i = 0; i < this.modules.length; i++) {
            const module = this.modules[i];
            if (module.buildInfo.assets) {
                const assetsInfo = module.buildInfo.assetsInfo;
                for (const assetName of Object.keys(module.buildInfo.assets)) {
                    const fileName = this.getPath(assetName);

          // 看这里
                    this.emitAsset(
                        fileName,
                        module.buildInfo.assets[assetName],
                        assetsInfo ? assetsInfo.get(assetName) : undefined
                    );


                    this.hooks.moduleAsset.call(module, fileName);
                }
            }
        }
    }

这个是compilation 中的一些hooks
image.png

当模块构建完成之后,需要进行文件生成,也就是Chunk生成的阶段
image.png

文件生成

image.png
下面的代码就是emit阶段,文件如何生成,写入到磁盘中。

        this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
        });