4
本文内容基于webpack 5.74.0版本进行分析

webpack5核心流程专栏共有5篇,使用流程图的形式分析了webpack5的构建原理

  1. 「Webpack5源码」make阶段(流程图)分析
  2. 「Webpack5源码」enhanced-resolve路径解析库源码分析
  3. 「Webpack5源码」seal阶段(流程图)分析(一)
  4. 「Webpack5源码」seal阶段分析(二)-SplitChunksPlugin源码
  5. 「Webpack5源码」seal阶段分析(三)-生成代码&runtime

前言

  1. 由于webpack5整体代码过于复杂,为了减少复杂度,本文所有分析将只基于js文件类型进行分析,不会对其它类型(cssimage)进行分析,所举的例子也都是基于js类型
  2. 为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
  3. 文章默认读者已经掌握tapableloaderplugin等基础知识,对文章中出现asyncQueuetapableloaderplugin相关代码都会直接展示,不会增加过多说明
  4. 由于webpack5整体代码过于复杂,因此本文会采取抽离出核心代码的模式进行分析讲解
核心代码是笔者认为核心代码的部分,肯定会造成部分内容(读者也觉得是核心代码)缺失,如果发现缺失部分,请参考其它文章或者私信/评论区告知我

文章内容

npm run build命令开始,将webpack编译入口到make阶段的所有流程抽离出核心代码形成流程图,然后针对核心代码进行具体的分析,主要分为:

  1. npm run build命令开始,分析webpack入口文件的源码执行流程,分析是从npm run build入口开始是如何执行到make阶段
  2. 分析make阶段的factorizeModule()的执行流程
  3. 分析make阶段的buildMode()的执行流程
  4. 分析make阶段的processModuleDependencies()的执行流程

1. 初始化

1.1 npm run build

1.1.1 流程图

npm-run-build入口分析.svg

1.1.2 流程图源码分析

当我们执行npm run build的时候,实际就是执行bin/webpack.js

{
    "scripts": {
        "build-debugger": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --config  webpack.config.js --progress",
        "build": "webpack"
    }
}

bin/webpack.js,最终会加载webpack-cli/package.jsonbin字段,也就是./bin/cli.js

const cli = {
    name: "webpack-cli",
    package: "webpack-cli",
    binName: "webpack-cli",
    installed: isInstalled("webpack-cli"),
    url: "https://github.com/webpack/webpack-cli"
};
const runCli = cli => {
  const path = require("path");
  const pkgPath = require.resolve(`${cli.package}/package.json`);
  // eslint-disable-next-line node/no-missing-require
  const pkg = require(pkgPath);
  // eslint-disable-next-line node/no-missing-require
  require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};


runCli(cli);


// webpack-cli/package.json
"bin": {
  "webpack-cli": "./bin/cli.js"
}

webpack-cli/bin/cli.js中,触发了new WebpackCLI()run()方法

// webpack-cli/bin/cli.js
const runCLI = require("../lib/bootstrap");
runCLI(process.argv);

// webpack-cli/lib/bootstrap.js
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
    // Create a new instance of the CLI object
    const cli = new WebpackCLI();
    try {
        await cli.run(args);
    }
    catch (error) {
        cli.logger.error(error);
        process.exit(2);
    }
};
cli=new WebpackCLI()cli.run()方法就非常绕了

下面执行的流程可以概括为:

  • await this.program.parseAsync(args, parseOptions)触发this.program.action(fn)fn执行
  • this.program.action(fn)fn主要包括loadCommandByName()根据名称创建命令以及再次this.program.parseAsync()触发命令
  • loadCommandByName():触发makeCommand()执行

    • 一开始会先触发options()执行,也就是this.webpack的初始化this.loadWebpack(),从而触发require("webpack"),从而找到了webpack/package.jsonmain字段,最终找到了webpack/lib/index.js,然后触发了webpack/lib/webpack.js的执行
    • 执行完options()后,执行command.action(action)
  • 再次this.program.parseAsync()触发命令:触发loadCommandByName()注册的command.action(action)action()核心就是触发this.runWebpack(),最终触发的是上面loadCommandByName()->options()拿到的this.webpack()this.webpack()会触发整个编译流程的执行
this.webpack()会触发整个编译流程的执行逻辑请看下面1.2 webpack.js的分析
const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack";
class WebpackCLI {
    // ==============================makeCommand==========================
    async loadWebpack(handleError = true) {
        // WEBPACK_PACKAGE="webpack"
        return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
    }
    async tryRequireThenImport(module, handleError = true) {
        result = require(module);
    }

    async runWebpack(options, isWatchCommand) {
        compiler = await this.createCompiler(options, callback);
    }
    async createCompiler(options, callback) {
        let config = await this.loadConfig(options);
        config = await this.buildConfig(config, options);
        let compiler = this.webpack(config.options, ...);
        return compiler;
    }
    // ==============================makeCommand==========================

    async run() {
        const loadCommandByName = async (commandName, allowToInstall = false) => {
            //...
            await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
                this.webpack = await this.loadWebpack();
                return isWatchCommandUsed
                    ? this.getBuiltInOptions().filter((option) => option.name !== "watch")
                    : this.getBuiltInOptions();
            }, async (entries, options) => {
                if (entries.length > 0) {
                    options.entry = [...entries, ...(options.entry || [])];
                }
                await this.runWebpack(options, isWatchCommandUsed);
            });
        }

        this.program.action(async (options, program) => {
            if (isKnownCommand(commandToRun)) {
                // commandToRun = "build"
                await loadCommandByName(commandToRun, true);
            }
            await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
                from: "user",
            });
        });
        await this.program.parseAsync(args, parseOptions);
    }
    async makeCommand(commandOptions, options, action) {
        const command = this.program.command(commandOptions.name, {
            noHelp: commandOptions.noHelp,
            hidden: commandOptions.hidden,
            isDefault: commandOptions.isDefault,
        });
        if (options) {
            options();
        }
        command.action(action);
        return command;
    }
}

// webpack/package.json
"main": "lib/index.js"

// webpack/lib/index.js
const fn = lazyFunction(() => require("./webpack"));
module.exports = mergeExports(fn, {
    get webpack() {
        return require("./webpack");
    },
    //...
});

1.2 webpack.js

webpack/lib/webpack.js中,我们会使用create()->createCompiler()进行compiler对象的初始化,然后触发compiler.run()

//node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
  const { compiler, watch, watchOptions } = create(options);
  compiler.run();
  return compiler;
}
const create = () => {
  const webpackOptions = options;
  compiler = createCompiler(webpackOptions);
  //...
  return { compiler, watch, watchOptions };
}
createCompiler()具体执行了什么逻辑呢?

1.2.1 createCompiler()流程图

下图中的compiler.run()compiler.compile()相关内容会在1.3中进行分析

Webpack5-1-init.svg

1.2.2 createCompiler()源码分析

如下面代码所示,主要执行了5个步骤:

  • 进行webpack配置数据的整理:比如entry如果没有在webpack.config.js声明,则会自动填补entry:{main:{}}
  • 初始化Compiler对象
  • 处理webpack.config.jsplugins注册
  • 初始化默认参数配置,比如getResolveDefaults(后面resolver.resolve会用到的参数)
  • 注册内置插件
const createCompiler = rawOptions => {
    // 1.整理webpack.config.js的参数
    const options = getNormalizedWebpackOptions(rawOptions);
    
    // 2.初始化Compiler对象
    const compiler = new Compiler(options.context, options);
    
    // 3.处理webpack.config.js的plugins注册
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    // 4.初始化默认参数配置,比如getResolveDefaults(后面resolver.resolve会用到的参数)
    applyWebpackOptionsDefaults(options);

    // 5.注册内置插件
    new WebpackOptionsApply().process(options, compiler);

    return compiler;
};

其中第5步new WebpackOptionsApply().process(options, compiler)会注册非常非常多的内置插件,包括多种typeresolveOptions拼接的相关插件,如下面代码所示

后面make阶段的resolver.resolve()会用到resolveOptions
class WebpackOptionsApply extends OptionsApply {
    process(options, compiler) {
        //...
        new EntryOptionPlugin().apply(compiler);
        
        compiler.resolverFactory.hooks.resolveOptions
            .for("normal")
            .tap("WebpackOptionsApply", resolveOptions => {
                resolveOptions = cleverMerge(options.resolve, resolveOptions);
                resolveOptions.fileSystem = compiler.inputFileSystem;
                return resolveOptions;
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("context")
            .tap("WebpackOptionsApply", resolveOptions => {
                resolveOptions = cleverMerge(options.resolve, resolveOptions);
                resolveOptions.fileSystem = compiler.inputFileSystem;
                resolveOptions.resolveToContext = true;
                return resolveOptions;
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("loader")
            .tap("WebpackOptionsApply", resolveOptions => {
                resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
                resolveOptions.fileSystem = compiler.inputFileSystem;
                return resolveOptions;
            });
    }
}

其中最应该关注的是new EntryOptionPlugin().apply(compiler),它是入口相关的一个插件

class WebpackOptionsApply extends OptionsApply {
    process(options, compiler) {
        new EntryOptionPlugin().apply(compiler);
    }
}
class EntryOptionPlugin {
    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            EntryOptionPlugin.applyEntryOption(compiler, context, entry);
            return true;
        });
    }
}

EntryOptionPlugin插件会对entry入口文件类型进行判断,从而触发注册对应的EntryPlugin插件

// EntryOptionPlugin.applyEntryOption
static applyEntryOption(compiler, context, entry) {
    if (typeof entry === "function") {
        const DynamicEntryPlugin = require("./DynamicEntryPlugin");
        new DynamicEntryPlugin(context, entry).apply(compiler);
    } else {
        const EntryPlugin = require("./EntryPlugin");
        for (const name of Object.keys(entry)) {
            const desc = entry[name];
            const options = EntryOptionPlugin.entryDescriptionToOptions(
                compiler,
                name,
                desc
            );
            for (const entry of desc.import) {
                new EntryPlugin(context, entry, options).apply(compiler);
            }
        }
    }
}

EntryPlugin插件中注册了两个hooks,一个是获取对应的NormalModuleFactory,一个是监听compiler.hooks.make然后进行compilation.addEntry()流程

apply(compiler) {
    compiler.hooks.compilation.tap(
        "EntryPlugin",
        (compilation, { normalModuleFactory }) => {
            compilation.dependencyFactories.set(
                EntryDependency,
                normalModuleFactory
            );
        }
    );

    const { entry, options, context } = this;
    const dep = EntryPlugin.createDependency(entry, options);

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
        compilation.addEntry(context, dep, options, err => {
            callback(err);
        });
    });
}

1.2.3 compiler.run()

createCompiler()之后,我们就可以得到了compiler对象,然后使用compiler.run()开始make阶段和seal阶段的执行

//node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
  const { compiler, watch, watchOptions } = create(options);
  compiler.run();
  return compiler;
}

// node_modules/webpack/lib/Compiler.js
class Compiler {
    run(callback) {
        const run = () => {
            this.compile(onCompiled);
        }
        run();
    }
    compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            const compilation = this.newCompilation(params);
            this.hooks.make.callAsync(compilation, err => {
                compilation.seal(err => {
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        return callback(null, compilation);
                    });
                });
            });
        });
    }
}

1.3 小结

  1. 我们从上面的分析中知道了整体的初始化流程以及如何触发下一阶段make阶段
  2. 在上面初始化流程,我们没有分析初始化ruleSet的主要代码逻辑,这一块是make阶段的resolve环节所涉及到的逻辑,将放在下面段落讲解
  3. 我们在初始化阶段中要注意:applyWebpackOptions会形成一些默认的options参数,后面会有很多地方涉及到options的不同导致的逻辑不同,在后续的开发中,如果发现配置参数不是在webpack.config.js中书写,应该要想到这个方法是否自动帮我们添加了一些参数
  4. 我们在初始化阶段中要注意:new WebpackOptionsApply().process()会注册非常非常多的内置插件,这些插件在后续流程中有非常大的作用,每当无法知道某一个流程的插件在哪里注册时,应该想要这个方法是否提前注册了一些内置插件

2. make阶段-整体流程图

webpack5-2-make.svg

3. make阶段-流程图源码分析

从上面流程图可以知道,make阶段有三种主要流程:resolvebuildprocessModuleDependencies,直接用上面流程图分析还是过于复杂,以上面流程图为基础,简化出来的流程图如下所示:

make流程总结.svg

下面将按照上面流程图进行具体的源码分析

3.1 resolve-获取NormalModuleFactory

addModuleTree() {
    const Dep = dependency.constructor;
    const moduleFactory = this.dependencyFactories.get(Dep);
    //...一系列方法跳转然后触发this._factorizeModule({factory: moduleFactory})
}
而这个moduleFactory是怎么获取到的呢?我们什么时候进行this.dependencyFactories.set操作?

在初始化流程中,我们可以知道,我们在EntryPlugin注册了compiler.hooks.compilation事件的监听,在这个事件监听中,我们可以获取EntryDependency对应的normalModuleFactory,因此我们只要知道compiler.hooks.compilation事件什么时候触发,就能找到normalModuleFactory构建的地方

compiler.hooks.compilation.tap(
    "EntryPlugin",
    (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
            EntryDependency,
            normalModuleFactory
        );
    }
);

在更前面的流程compile()中,我们会进行const params = this.newCompilationParams(),这个时候我们会顺便初始化NormalModuleFactory

newCompilationParams()可以参考上面1.2.3 整体流程图
newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory()
    };
    return params;
}
createNormalModuleFactory() {
    const normalModuleFactory = new NormalModuleFactory({
        resolverFactory: this.resolverFactory,
    });
    this.hooks.normalModuleFactory.call(normalModuleFactory);
    return normalModuleFactory;
}

初始化完成params,我们会直接进行const compilation = this.newCompilation(params)的创建,这个时候会触发compiler.hooks.compilation事件,从而触发上面EntryPlugin.js提及的compilation.dependencyFactories.set(xxDependency, xxxFactory)操作

// node_modules/webpack/lib/Compiler.js
newCompilation(params) {
    const compilation = this.createCompilation(params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
}

// node_modules/webpack/lib/EntryPlugin.js
compiler.hooks.compilation.tap(
    "EntryPlugin",
    (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
            EntryDependency,
            normalModuleFactory
        );
    }
);

3.2 resolve-NormalModuleFactory.create()

如下面代码所示,在3.1 获取NormalModuleFactory后,我们会触发factory.create()->this.hooks.factorize.callAsync()

//node_modules/webpack/lib/Compilation.js
addModuleTree() {
    const Dep = dependency.constructor;
    const moduleFactory = this.dependencyFactories.get(Dep);
    //...一系列方法跳转然后触发this._factorizeModule({factory: moduleFactory})
}
_factorizeModule({factory}) {
    factory.create();
}
//node_modules/webpack/lib/NormalModuleFactory.js 
create(data, callback) {
  this.hooks.factorize.callAsync()
}

3.3 resolve-getResolver()&resolver.resolve()

3.3.1 整体流程图

this.hooks.factorize.callAsync经过一系列的跳转之后,会触发NormalModuleFactory constructor()注册的事件

class NormalModuleFactory extends ModuleFactory {
    constructor(){
        this.hooks.factorize.tapAsync(
            {
                name: "NormalModuleFactory",
                stage: 100
            },
            (resolveData, callback) => {
                this.hooks.resolve.callAsync(resolveData, (err, result) => {
                    //....
                });
            }
        );
    }
}

然后触发this.hooks.resolve,这个事件执行逻辑较为复杂,先使用一个流程图展示下核心流程:

为了减少复杂度,暂时只考虑normalLoaders,不考虑preLoaders和postLoaders,因此下面的流程图也只展示userLoaders的相关逻辑,而没有useLoadersPost和useLoadersPre

Webpack5-3-make-resolve.svg

上面流程可以总结为:

  • 解析inline-loader:使用startsWith"-!""!""!!"判断是否存在inline情况,如果存在,则解析为elementsunresolvedResource,其中elements代表解析后的loadersunresolvedResource表示文件的请求链接
  • resolveRequestArray:对loaders进行路径resolve,获取getResolver("loader"),调用resolver.resolve()进行路径处理
resolveRequestArray是为了处理文件链接中内联模式的loader,比如上面流程图的"babel-loader!./index-item.line",如果整个项目都不使用内联模式的loader,那么resolveRequestArray传入的参数就是一个空数组!
  • defaultResolve

    • 对文件请求路径request进行路径resolve,获取getResolver("normal"),调用resolver.resolve()进行路径处理
    • 使用this.ruleSet.exec进行筛选出适配文件请求路径requestloaderswebpack.config.js配置了多条rules规则,但是有一些文件只需要其中的1、2个rules规则,比如.js文件需要babel-loaderrule,而不需要css-loaderrule
    • 上面步骤筛选出来的loaders再使用resolveRequestArray进行路径的整理,如下面的图片所示

截屏2023-01-07 20.37.33.png

  • elementsunresolvedResource完成后,则进行最终数据的整合,将loaderparsergenerator整合到data.createData

    • elementsunresolvedResource合并为最终的loaders数组数据
    • getParser()初始化
    • getGenerator()初始化

this.hooks.resolve整体流程如上面所示,下面我们将根据几个方向进行详细具体地分析:

  • getResolver(): 根据不同type获取对应的resolver对象
  • resolver.resolve(): 执行resolver对象的resolve()方法
  • ruleSet.exec(): 为普通文件筛选出适配的loader列表,比如.scss文件需要sass-loadercss-loader等多个loader的处理
注:由于webpack5的代码逻辑实在太过繁杂,因此文章中有几个地方会采取先概述再分点详细分析的模式

3.3.2 getResolver()

两种类型resolver,一种是处理loaderresolver,一种是处理普通文件请求的resolver

// loader类型的loaderResolver
const loaderResolver = this.getResolver("loader");
// 普通文件类型的loaderResolver
const normalResolver = this.getResolver(
    "normal",
    dependencyType
        ? cachedSetProperty(
            resolveOptions || EMPTY_RESOLVE_OPTIONS,
            "dependencyType",
            dependencyType
        )
        : resolveOptions
);

getResolver()简化后的核心代码为:

getResolver(type, resolveOptions) {
   return this.resolverFactory.get(type, resolveOptions);
}
// node_modules/webpack/lib/NormalModuleFactory.js
get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
    //...cache处理
    const newResolver = this._create(type, resolveOptions);
    return newResolver;
}
// node_modules/webpack/lib/ResolverFactory.js
_create(type, resolveOptionsWithDepType) {
    // 第1步:根据type获取不同的options配置
    const resolveOptions = convertToResolveOptions(
        this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
    );
    // 第2步:根据不同的配置创建不同的resolver
    const resolver = Factory.createResolver(resolveOptions);
    return resolver;
}
第1步:根据不同type获取对应的options

通过webpack官方文档-模块解析webpack官方文档-解析,我们可以知道,resolve一共分为两种配置,一种是文件类型的路径解析配置,一种是webpack 的 loader 包的解析配置

文件路径解析可以分为三种:绝对路径、相对路径和模块路径

我们可以从上面的分析知道,第1步会根据type获取不同的options配置,在webpack.config.js中,我们可以配置resolve参数,如果没有配置,webpack也有默认的配置resolve参数

配置参数是在哪里配置的呢?又是如何区分不同type的呢?
// 第1步:根据type获取不同的options配置
const resolveOptions = convertToResolveOptions(
  this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
);

在最上面的初始化流程中,一开始我们创建Compiler对象的时候,会触发applyWebpackOptionsDefaults(),在下面的代码块中,我们可以看到,进行options.resolveoptions.resolveLoader不同类型的初始化,对应的就是文件类型的路径解析配置,以及webpack 的 loader 包的解析配置

// node_modules/webpack/lib/webpack.js
const createCompiler = rawOptions => {
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsDefaults(options);
    new WebpackOptionsApply().process(options, compiler);
    return compiler;
};
// node_modules/webpack/lib/config/defaults.js
const applyWebpackOptionsDefaults = options => {
    options.resolve = cleverMerge(
        getResolveDefaults({
            cache,
            context: options.context,
            targetProperties,
            mode: options.mode
        }),
        options.resolve
    );

    options.resolveLoader = cleverMerge(
        getResolveLoaderDefaults({ cache }),
        options.resolveLoader
    );
}

初始化resolveOptions后,我们会触发new WebpackOptionsApply().process(options, compiler)进行不同类型:normalcontextloader类型的resolver的构建,this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)中的type就是normalcontextloader

// node_modules/webpack/lib/webpack.js
compiler.resolverFactory.hooks.resolveOptions
   .for("normal")
   .tap("WebpackOptionsApply", resolveOptions => {
      resolveOptions = cleverMerge(options.resolve, resolveOptions);
      resolveOptions.fileSystem = compiler.inputFileSystem;
      return resolveOptions;
   });
compiler.resolverFactory.hooks.resolveOptions
   .for("context")
   .tap("WebpackOptionsApply", resolveOptions => {
      resolveOptions = cleverMerge(options.resolve, resolveOptions);
      resolveOptions.fileSystem = compiler.inputFileSystem;
      resolveOptions.resolveToContext = true;
      return resolveOptions;
   });
compiler.resolverFactory.hooks.resolveOptions
   .for("loader")
   .tap("WebpackOptionsApply", resolveOptions => {
      resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
      resolveOptions.fileSystem = compiler.inputFileSystem;
      return resolveOptions;
   });
为了减少复杂度,暂时只分析normalloader类型,不分析context类型的resolver
typenormalloader
resolveOptions截屏2023-03-13 19.42.30.png截屏2023-03-13 19.42.57.png

其中normal的参数获取是合并了webpack.config.js和默认options的结果
入口文件是EntryDependency,它具有默认的category="esm"

class EntryDependency extends ModuleDependency {
    /**
     * @param {string} request request path for entry
     */
    constructor(request) {
        super(request);
    }
    get type() {
        return "entry";
    }
    get category() {
        return "esm";
    }
}

而在初始化normal类型的Resolver时,会触发hooks.resolveOptions进行webpack.config.js和一些默认参数的初始化

// node_modules/webpack/lib/ResolverFactory.js
_create(type, resolveOptionsWithDepType) {
    /** @type {ResolveOptionsWithDependencyType} */
    const originalResolveOptions = { ...resolveOptionsWithDepType };

    const resolveOptionsTemp = this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType);
    const resolveOptions = convertToResolveOptions(
        resolveOptionsTemp
    );
}
// node_modules/webpack/lib/WebpackOptionsApply.js
compiler.resolverFactory.hooks.resolveOptions
    .for("normal")
    .tap("WebpackOptionsApply", resolveOptions => {
        resolveOptions = cleverMerge(options.resolve, resolveOptions);
        resolveOptions.fileSystem = compiler.inputFileSystem;
        return resolveOptions;
    });

this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)获取到的数据如下图所示

截屏2023-03-13 19.34.13.png

然后触发convertToResolveOptions()方法,经历几个方法的调用执行后,最终触发resolveByProperty(),如下图所示,会根据上面EntryDependency得到的"esm"进行参数的合并,最终得到完整的配置参数

截屏2023-03-13 19.09.03.png

第2步:根据不同的options初始化Resolver对象
// 第2步:根据不同的配置创建不同的resolver
const resolver = Factory.createResolver(resolveOptions);

创建过程中会注册非常非常多的plugin,等待后续的resolver.resolve()调用来解析路径

// node_modules/enhanced-resolve/lib/ResolverFactory.js
createResolver = function (options) {
    const normalizedOptions = createOptions(options);
    // pipeline //
    resolver.ensureHook("resolve");
    //...省略ensureHook("xxxx")

    // raw-resolve
    if (alias.length > 0) {
        plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
    }
    //...省略很多很多plugin的注册

    for (const plugin of plugins) {
        if (typeof plugin === "function") {
            plugin.call(resolver, resolver);
        } else {
            plugin.apply(resolver);
        }
    }
    return resolver;
}

3.3.3 resolver.resolve()

enhanced-resolve封装库:多个插件之间的处理,使用doResolve()进行串联,涉及多种文件系统插件的路径查找,第一个插件找不到,就使用第二个插件,直到找到停止这种管道的查找

从下面代码块可以知道,resolver.resolve实际就是调用doResolve()

// node_modules/enhanced-resolve/lib/Resolver.js
resolve(context, path, request, resolveContext, callback) {
    return this.doResolve(this.hooks.resolve, ...args);
}
// node_modules/enhanced-resolve/lib/Resolver.js
doResolve(hook, request, message, resolveContext, callback) {
    const stackEntry = Resolver.createStackEntry(hook, request);
    if (resolveContext.stack) {
        newStack = new Set(resolveContext.stack);
        newStack.add(stackEntry);
    } else {
        newStack = new Set([stackEntry]);
    }
    return hook.callAsync(request, innerContext, (err, result) => {
        if (err) return callback(err);
        if (result) return callback(null, result);
        callback();
    });
}

一开始调用doResolve()时,我们传入的第一个参数hook=this.hooks.resolve,我们从上面代码块可以知道,会直接触发hook.callAsync,也就是this.hooks.resolve.callAsync(),而在初始化创建resolver时,如下面代码块所示,我们使用"resolve"作为ParsePlugin参数传入

ResolverFactory.createResolver = function (options) {
    const normalizedOptions = createOptions(options);
    // resolve
    for (const { source, resolveOptions } of [
        { source: "resolve", resolveOptions: { fullySpecified } },
        { source: "internal-resolve", resolveOptions: { fullySpecified: false } }
    ]) {
        if (unsafeCache) {
            //...
        } else {
            plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
        }
    }
    // parsed-resolve
    plugins.push(
        new DescriptionFilePlugin(
            "parsed-resolve",
            descriptionFiles,
            false,
            "described-resolve"
        )
    );
}

而在ParsePlugin的源代码,如下面的代码块可以知道,最终

  • resolver.resolve(this.hooks.resolve)触发resolver.doResolve("resolve")然后执行hooks["resolve"].callAsync()
  • 触发订阅监听的ParsePlugin.applyParsePlugin订阅了resolve),触发resolver.doResolve(target="parsed-resolve")然后执行hooks["parsed-resolve"].callAsync()
  • 触发订阅监听的DescriptionFilePlugin.apply(DescriptionFilePlugin订阅了parsed-resolve
  • ......
  • 这样不断重复下去,就可以创建出一个插件接着一个插件的串行事件处理
为了方便记忆,我们可以简单理解为ResolverFactory.createResolver时注册的插件,第一个参数就是订阅的名称,最后一个参数是下一个订阅的触发名称,比如上面代码块的parsed-resolve,订阅parsed-resolve->处理->触发下一个订阅described-resolve
class ParsePlugin {
    constructor(source, requestOptions, target) {
        this.source = source;
        this.requestOptions = requestOptions;
        this.target = target;
    }
    apply(resolver) {
        // this.source = "resolve"
        // this.target = "parsed-resolve"
        const target = resolver.ensureHook(this.target);
        resolver
            .getHook(this.source)
            .tapAsync("ParsePlugin", (request, resolveContext, callback) => {
                resolver.doResolve(target, obj, null, resolveContext, callback);
            });
    }
}

对待不同类型的请求,比如文件类型、目录类型、模块类型在resolver.resolve()有不同的执行流程,由于篇幅过长,感兴趣可以查看下一篇文章「Webpack5源码」enhanced-resolve路径解析库源码分析,本质上就是替换别称,寻找文件所在目录路径,拼凑成最终的绝对路径

3.3.4 ruleSetCompiler.exec()

整体流程图

我们在上面3.3.1 整体流程图可以知道,当处理好依赖文件(普通文件路径和loader路径)后,会触发this.ruleSet.exec()为普通文件进行loader的筛选

那么this.ruleSet是在哪里初始化的呢?

this.ruleSet的初始化是在this.hooks.make.callAsync()之前就执行的,如下面流程图所示
webpack5-4-make-ruleSetCompiler.svg
从上面流程图可以知道,this.ruleSet的初始化为:

  • 初始化RuleSetCompiler
  • ruleSetCompiler.compile

    • compileRules()
    • 返回对象数据{exec: function(){}}
初始化RuleSetCompiler

初始化RuleSetCompiler时初始化了大量的plugin,每一个plugin都进行了ruleSetCompiler.hooks.rule.tap()的监听,在后面的compileRule()中拼凑compiledRule时触发this.hooks.rule.call()

const ruleSetCompiler = new RuleSetCompiler([
   new BasicMatcherRulePlugin("test", "resource"),
   new BasicMatcherRulePlugin("scheme"),
   new BasicMatcherRulePlugin("mimetype"),
   new BasicMatcherRulePlugin("dependency"),
   new BasicMatcherRulePlugin("include", "resource"),
   new BasicMatcherRulePlugin("exclude", "resource", true),
   new BasicMatcherRulePlugin("resource"),
   new BasicMatcherRulePlugin("resourceQuery"),
   new BasicMatcherRulePlugin("resourceFragment"),
   new BasicMatcherRulePlugin("realResource"),
   new BasicMatcherRulePlugin("issuer"),
   new BasicMatcherRulePlugin("compiler"),
   new BasicMatcherRulePlugin("issuerLayer"),
   new ObjectMatcherRulePlugin("assert", "assertions"),
   new ObjectMatcherRulePlugin("descriptionData"),
   new BasicEffectRulePlugin("type"),
   new BasicEffectRulePlugin("sideEffects"),
   new BasicEffectRulePlugin("parser"),
   new BasicEffectRulePlugin("resolve"),
   new BasicEffectRulePlugin("generator"),
   new BasicEffectRulePlugin("layer"),
   new UseEffectRulePlugin()
]);
初始化ruleSet: ruleSetCompiler.compile(rules)

ruleSetCompiler.compile()主要由this.compileRules()execRule()两个方法组成

this.ruleSet = ruleSetCompiler.compile([{rules: Array<{}>}]);

// node_modules/webpack/lib/rules/RuleSetCompiler.js
compile(ruleSet) {
    const refs = new Map();
    const rules = this.compileRules("ruleSet", ruleSet, refs);
    const execRule = (data, rule, effects) => {};

    return {
        references: refs,
        exec: data => {
            const effects = [];
            for (const rule of rules) {
                execRule(data, rule, effects);
            }
            return effects;
        }
    };
}
ruleSetCompiler.compile()传入的参数rules如下所示,是两个数组集合,第一个是默认的配置参数,第二个是我们在webpack.config.js中设置的loaders的参数配置

ruleSet.png

compileRules()方法解析

从下面的代码可以看出,这是为了拼接exec()方法中的rules对象,具有conditionseffectsrulesoneOf四个属性

compileRules(path, rules, refs) {
    return rules.map((rule, i) =>
        this.compileRule(`${path}[${i}]`, rule, refs)
    );
}
compileRule(path, rule, refs) {
    const compiledRule = {
        conditions: [],
        effects: [],
        rules: undefined,
        oneOf: undefined
    };
    // 拼接compiledRule.conditions数据
    // 拼接compiledRule.effects数据
    this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
    if (unhandledProperties.has("rules")) {
        // 拼接compiledRule.rules数据
        compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
    }
    if (unhandledProperties.has("oneOf")) {
        // 拼接compiledRule.oneOf数据
        compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
    }
    return compiledRule;
}

可以看出,compileRules()方法主要分为两块内容:

  • compiledRule对象4个属性的构建
  • this.hooks.rule.call()的调用

compiledRule对象属性分析
const compiledRule = {
    conditions: [],
    effects: [],
    rules: undefined, 
    oneOf: undefined
};

compileRules.conditions

// webpack.config.js
const path = require('path');
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        include: [
          // will include any paths relative to the current directory starting with `app/styles`
          // e.g. `app/styles.css`, `app/styles/styles.css`, `app/stylesheet.css`
          path.resolve(__dirname, 'app/styles'),
          // add an extra slash to only include the content of the directory `vendor/styles/`
          path.join(__dirname, 'vendor/styles/'),
        ],
      },
    ],
  },
};

条件可以是这些之一:

  • 字符串:匹配输入必须以提供的字符串开始,目录绝对路径或文件绝对路径。
  • 正则表达式:test 输入值。
  • 函数:调用输入的函数,必须返回一个真值(truthy value)以匹配。
  • 条件数组:至少一个匹配条件。
  • 对象:匹配所有属性。每个属性都有一个定义行为。
也就是test、include、exclude、resourceQuery等条件的筛选,放在conditions中,具体可以参考webpack官方文档

compileRules.effects

{
    type: "use",
    value: {
        ident: "ruleSet[1].rules[0]"
        loader: "babel-loader"
        options: {
            presets: [ '@babel/preset-env', {...}]
        }
    }
}
也就是loader、options等条件的筛选,指定要使用哪个loader以及对应的配置参数以及对应的路径放在effects中

compileRules.rules

存放子规则,从上面的分析我们可以知道,一开始传入的ruleSet是两个数组集合,我们需要解析出来,然后将数组里面的每一个item都解析成一个rules,也就是

const compiledRule = {
    conditions: [],
    effects: [],
    rules: [
        {
            conditions: [],
            effects: [],
            rules: undefined,
            oneOf: undefined
        }
    ],
    oneOf: undefined
};

compileRules.oneOf

规则数组,当规则匹配时,只使用第一个匹配规则。

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.css$/,
        oneOf: [
          {
            resourceQuery: /inline/, // foo.css?inline
            use: 'url-loader',
          },
          {
            resourceQuery: /external/, // foo.css?external
            use: 'file-loader',
          },
        ],
      },
    ],
  },
};

this.hooks.rule.call()
this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);

触发UseEffectRulePluginBasicEffectRulePluginBasicMatcherRulePluginObjectMatcherRulePlugin进行传入的compiledRule的数据添加

本质就是根据注册的插件,往compiledRule集合数据添加数据,以便于后面执行exec()

截屏2023-03-10 15.52.02.png


this.ruleSet.exec

在经历了上面RuleSetCompiler的初始化、ruleSetCompiler.compile(rules)获取this.ruleSet后,最终会执行this.ruleSet.exec()

exec: data => {
  /** @type {Effect[]} */
  const effects = [];
  for (const rule of rules) {
    execRule(data, rule, effects);
  }
  return effects;
}

const execRule = (data, rule, effects) => {
    for (const condition of rule.conditions) {
        //...
    }
    for (const effect of rule.effects) {
        if (typeof effect === "function") {
            const returnedEffects = effect(data);
            for (const effect of returnedEffects) {
                effects.push(effect);
            }
        } else {
            effects.push(effect);
        }
    }
    if (rule.rules) {
        for (const childRule of rule.rules) {
            execRule(data, childRule, effects);
        }
    }
    if (rule.oneOf) {
        for (const childRule of rule.oneOf) {
            if (execRule(data, childRule, effects)) {
                break;
            }
        }
    }
    return true;
};

根据上面初始化时的rules数据集合,传入数据data,然后进行每一个rule的筛选,本质就是传入一个路径,然后根据路径检测出需要使用什么loader
如下图所示,我们传入一个文件对象,包括了路径等数据,然后不断遍历rules,将符合题意的effect加入到数组中,比如下面这个effect:{value:"javascript/auto"}

截屏2023-03-10 16.01.04.png

3.4 resolve-getParser()

Webpack5-3-make-resolve.svg

默认设置settings.type = "javascript/auto",因此createParser的type都是"javascript/auto"

本质是获取JavascriptParser对象

后续流程再仔细分析这里的getParser()有何用处,目前只要知道是JavascriptParser对象即可
getParser(type, parserOptions = EMPTY_PARSER_OPTIONS) {
    // ...省略缓存逻辑
    parser = this.createParser(type, parserOptions);
    return parser;
}

createParser() {
    parserOptions = mergeGlobalOptions(
        this._globalParserOptions,
        type,
        parserOptions
    );
    const parser = this.hooks.createParser.for(type).call(parserOptions);
    return parser;
}
// createCompiler()->new WebpackOptionsApply().process(options, compiler)
new JavascriptModulesPlugin().apply(compiler);

// node_modules/webpack/lib/javascript/JavascriptModulesPlugin.js
normalModuleFactory.hooks.createParser
                    .for("javascript/auto")
                    .tap("JavascriptModulesPlugin", options => {
                        return new JavascriptParser("auto");
                    });

3.5 resolve-getGenerator()

跟上面getParser()逻辑一摸一样,省略重复结构代码

本质是获取JavascriptGenerator对象

后续流程再仔细分析这里的getGenerator()有何用处,目前只要知道是JavascriptGenerator对象即可
// node_modules/webpack/lib/javascript/JavascriptModulesPlugin.js
normalModuleFactory.hooks.createGenerator
                    .for("javascript/auto")
                    .tap("JavascriptModulesPlugin", () => {
                        return new JavascriptGenerator();
                    });

3.6 build整体流程图

5-webpack5-make-buildModule.svg

3.7 build流程图源码分析-核心步骤整体概述

从上面的流程图可以知道,经过resolve流程,我们获取到了所有请求的绝对路径,拿到了factoryResult数据,然后兜兜转转经过很多弯到达了buildModule(),最终触发NormalModule._doBuild()方法

this.addModule(newModule, (err, module) => {

    // ... 处理moduleGraph

    this._handleModuleBuildAndDependencies(
        originModule,
        module,
        recursive,
        callback
    );
}
const _handleModuleBuildAndDependencies = ()=> {
    // AsyncQueue,实际就是调用_buildModule
    // 增加可读性,callback改为await/async
    this.buildModule(module, err => {});
}
const _buildModule = (module, callback) => {
    // 增加可读性,callback改为await/async
    module.build();
}

// NormalModule.js
build(options, compilation, resolver, fs, callback) {
    return this._doBuild(options, compilation, resolver, fs, hooks, err => {
       let result;
        // js使用JavascriptParser._parse进行解析,内部使用了require("acorn")进行parser.parse(code, parserOptions)
        // ast = JavascriptParser._parse(source, {
        //   sourceType: this.sourceType,
        //   onComment: comments,
        //   onInsertedSemicolon: pos => semicolons.add(pos)
        // });
        result = this.parser.parse(this._ast || source, {
            source,
            current: this,
            module: this,
            compilation: compilation,
            options: options
        });
        handleParseResult(result);
    });
}

3.7.1 _doBuild()具体内容

  • 调用loader-runner进行loaderContext的解析
  • 处理sourceMap逻辑和初始化ast对象进行下一步调用
_doBuild(options, compilation, resolver, fs, hooks, callback) {
    // 拼接上下文参数
    const loaderContext = this._createLoaderContext(
        resolver,
        options,
        compilation,
        fs,
        hooks
    );

    // const { getContext, runLoaders } = require("loader-runner");
    runLoaders(
        {
            resource: this.resource, // 模块路径
            loaders: this.loaders, // loaders集合
            context: loaderContext, // 上面拼凑的上下文
            processResource: (loaderContext, resourcePath, callback) => {
                // 根据文件类型进行不同Plugin的处理,比如入口文件.js,触发了FileUriPlugin的apply(),进行文件的读取
                //     hooks.readResource
                          //   .for(undefined)
                          //   .tapAsync("FileUriPlugin", (loaderContext, callback) => {
                          //          const { resourcePath } = loaderContext;
                          //          loaderContext.addDependency(resourcePath);
                          //          loaderContext.fs.readFile(resourcePath, callback);
                          // });
                const resource = loaderContext.resource;
                const scheme = getScheme(resource);
                hooks.readResource
                    .for(scheme)
                    .callAsync(loaderContext, (err, result) => {
                        if (err) return callback(err);
                        if (typeof result !== "string" && !result) {
                            return callback(new UnhandledSchemeError(scheme, resource));
                        }
                        return callback(null, result);
                    });
            }
        },
        (err, result) => {
            // 将loaders放入buildInfo中
            for (const loader of this.loaders) {
                this.buildInfo.buildDependencies.add(loader.loader);
            }
            this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
            processResult(err, result.result);
        }
    );
}

const processResult = (err, result) => {
    // 处理sourceMap逻辑
    this._source = this.createSource(
        options.context,
        this.binary ? asBuffer(source) : asString(source),
        sourceMap,
        compilation.compiler.root
    );

    // 初始化AST
    this._ast =
        typeof extraInfo === "object" &&
            extraInfo !== null &&
            extraInfo.webpackAST !== undefined
            ? extraInfo.webpackAST
            : null;
    // _doBuild的callback()
    return callback();
}

3.7.2 Parse.parse-AST相关逻辑

_doBuild()执行完毕后,会执行this.parser.parse(this._ast||source),然后执行handleParseResult()

调用resolve阶段生成的parse,最终解析生成AST语法树

build(options, compilation, resolver, fs, callback) {
    return this._doBuild(options, compilation, resolver, fs, hooks, err => {
        let result;

        // js使用JavascriptParser._parse进行解析,内部使用了require("acorn")进行parser.parse(code, parserOptions)
        // ast = JavascriptParser._parse(source, {
        //   sourceType: this.sourceType,
        //   onComment: comments,
        //   onInsertedSemicolon: pos => semicolons.add(pos)
        // });
        result = this.parser.parse(this._ast || source, {
            source,
            current: this,
            module: this,
            compilation: compilation,
            options: options
        });

        handleParseResult(result);
    });
}
build流程到此整体流程的分析已经结束,之后会进行依赖的递归调用handleModuleCreate()的处理
由于build流程这一大块还是存在很多复杂的小模块内容,下面几个小节将着重分析这些复杂的小模块

3.8 build流程小模块-概述

在整个build流程中,我们下面将针对:

  • _doBuild()->runLoaders()
  • noParse
  • this.parser.parse实际就是JavascriptParser._parse

三个小点进行详细地分析

build(options, compilation, resolver, fs, callback) {
    return this._doBuild(options, compilation, resolver, fs, hooks, err => {
        let result;

        const noParseRule = options.module && options.module.noParse;
        if (this.shouldPreventParsing(noParseRule, this.request)) {
            // We assume that we need module and exports
            this.buildInfo.parsed = false;
            this._initBuildHash(compilation);
            return handleBuildDone();
        }
        // js使用JavascriptParser._parse进行解析,内部使用了require("acorn")进行parser.parse(code, parserOptions)
        // ast = JavascriptParser._parse(source, {
        //   sourceType: this.sourceType,
        //   onComment: comments,
        //   onInsertedSemicolon: pos => semicolons.add(pos)
        // });
        result = this.parser.parse(this._ast || source, {
            source,
            current: this,
            module: this,
            compilation: compilation,
            options: options
        });

        handleParseResult(result);
    });
}
_doBuild(options, compilation, resolver, fs, hooks, callback) {

    // const { getContext, runLoaders } = require("loader-runner");
    runLoaders(
        {
            ...
        },
        (err, result) => {
            // 将loaders放入buildInfo中
            for (const loader of this.loaders) {
                this.buildInfo.buildDependencies.add(loader.loader);
            }
            this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
            processResult(err, result.result);
        }
    );
}

const processResult = (err, result) => {
    // 处理sourceMap逻辑
    this._source = this.createSource(
        options.context,
        this.binary ? asBuffer(source) : asString(source),
        sourceMap,
        compilation.compiler.root
    );

    // 初始化AST
    this._ast =
        typeof extraInfo === "object" &&
            extraInfo !== null &&
            extraInfo.webpackAST !== undefined
            ? extraInfo.webpackAST
            : null;
    // _doBuild的callback()
    return callback();
}

3.9 build流程小模块-runLoaders流程分析

runLoaders是loader-runner库提供的一个方法,本次分析的loader-runner库版本为4.3.0

通过runLoaders执行所有loaders,获取原始_source

function runLoaders(options, callback) {
    // 1.为每一个loader创建一个状态Object数据,具有多个属性
    loaders = loaders.map(createLoaderObject);

    // 2.给loaderContext添加其它属性和方法
    loaderContext.context = contextDirectory;
    loaderContext.loaderIndex = 0;
    loaderContext.loaders = loaders;
    // ......

    // 3.进行loaders的pitch循环
    iteratePitchingLoaders(processOptions, loaderContext)
}

iteratePitchingLoaders()

源码逻辑比较清晰简单,可以使用一个流程图展示
6-webpack5-make-runLoaders-1.svg
6-webpack5-make-runLoaders-2.svg

style-loader源码解析

示例具体代码
// entry1.js
import "./index.less"

console.info("这是entry1");
// index.less
#box1{
    width: 100px;
    height: 100px;
    background: url('./1.jpg') no-repeat 100% 100%;
}
#box2{
    width: 200px;
    height: 200px;
    background: url('./2.jpg') no-repeat 100% 100%;
}
#box3{
    width: 300px;
    height: 300px;
    background: url('./3.jpg') no-repeat 100% 100%;
}
style-loader源码概述

精简后的style-loaderindex.js如下所示,代码流程并不复杂,只要弄清楚injectType这个参数的作用,本质就是通过injectType类型判断,然后形成不同的代码,进行return返回

var _path = _interopRequireDefault(require("path"));
var _utils = require("./utils");
var _options = _interopRequireDefault(require("./options.json"));
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {default: obj};
}
const loaderAPI = () => {
};
loaderAPI.pitch = function loader(request) {
    const options = this.getOptions(_options.default);
    const injectType = options.injectType || "styleTag";
    const esModule = typeof options.esModule !== "undefined" ? options.esModule : true;
    const runtimeOptions = {};
    const insertType = typeof options.insert === "function" ? "function" : options.insert && _path.default.isAbsolute(options.insert) ? "module-path" : "selector";
    const styleTagTransformType = typeof options.styleTagTransform === "function" ? "function" : options.styleTagTransform && _path.default.isAbsolute(options.styleTagTransform) ? "module-path" : "default";

    switch (injectType) {
        case "linkTag": { return ...}
        case "lazyStyleTag":
        case "lazyAutoStyleTag":
        case "lazySingletonStyleTag": {
            return ...
        }
        case "styleTag":
        case "autoStyleTag":
        case "singletonStyleTag":
        default: {
            return ...
        }
    }
};
var _default = loaderAPI;
exports.default = _default;
injectType种类以及作用
参考webpack官网:https://webpack.js.org/loaders/style-loader
  • styleTag: 引入的css文件都会形成单独的<style></style>标签插入到DOM
  • singletonStyleTag: 引入的css文件会合并形成1个<style></style>标签插入到DOM
  • linkTag: 会通过<link rel="stylesheet" href="path/to/file.css">的形式插入到DOM
  • lazyStyleTaglazyAutoStyleTaglazySingletonStyleTag: 延迟加载的样式,可以通过style.use()手动触发加载
injectType=default时的返回结果分析
默认injectType="styleTag"

从源码中,我们可以看到,当injectType="styleTag"时,会返回一大串使用utils.xxx拼接的代码,我们直接使用上面具体例子断点取出这一串代码

截屏2023-03-11 17.04.12.png

下面代码块就是上面截图所获取到的代码,最终style-loaderpitch()会返回下面这一串代码的字符串

import API from "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
import domAPI from "!../node_modules/style-loader/dist/runtime/styleDomAPI.js";
import insertFn from "!../node_modules/style-loader/dist/runtime/insertBySelector.js";
import setAttributes from "!../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js";
import insertStyleElement from "!../node_modules/style-loader/dist/runtime/insertStyleElement.js";
import styleTagTransformFn from "!../node_modules/style-loader/dist/runtime/styleTagTransform.js";
import content, * as namedExport
    from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
export * from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
export default content && content.locals ? content.locals : undefined;

我们从上面iteratePitchingLoaders()的分析可以知道,当一个loaderpitch()方法由返回值时,会中断后面loader的执行
6-webpack5-make-runLoaders-2.svg
因此此时style-loader pitch()->css-loader pitch()的执行会被中断,由于style-loader是处理css的第一个loader,因此style-loaderpitch()返回的字符串会交由webpack处理

style-loader的pitch()返回字符串形成Module
从上面代码,我们也可以看出,tyle-loaderpitch()返回的本质就是一串可执行的代码,而不是一个数据处理后的结果

最终webpack会将style-loaderpitch()返回的结果进行处理,最终打包形成一个Module(如main.js所示),而所使用的工具方法因为符合node_modules的分包规则,因此会被打包进vendors-node_modules_css-loader_xxx

截屏2023-03-11 17.44.42.png


由于style-loader返回的是一系列可以执行的代码,所以我们等同认为处理js文件,下面将展开分析

下面引入了两个文件,一个是index.less,一个是test.js

// entry1.js
import {getC1} from "./test.js";
import "./index.less";

console.info("这是entry1");

test.js返回的数据可能是

import xxx from "xxx"
import xxxx from "xxxxx";
export {getC1};

而现在我们引入index.less,从上面的分析可以知道,我们使用runLoaders解析得到跟test.js其实是类似的代码结构,既有import,也有export

import API from "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
import domAPI from "!../node_modules/style-loader/dist/runtime/styleDomAPI.js";
import insertFn from "!../node_modules/style-loader/dist/runtime/insertBySelector.js";
import setAttributes from "!../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js";
import insertStyleElement from "!../node_modules/style-loader/dist/runtime/insertStyleElement.js";
import styleTagTransformFn from "!../node_modules/style-loader/dist/runtime/styleTagTransform.js";
import content, * as namedExport
    from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
export * from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
export default content && content.locals ? content.locals : undefined;

最终形成的打包文件中,也会形成同样结构的{"xxx.js":xxx, "xxx.less": xxx}

var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
        return cachedModule.exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
        exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
}
var __webpack_modules__ = ({
    /***/ "test.js":
    /***/ (function (__unused_webpack_module, exports, __webpack_require__) {
           
            /***/
        }),
    /***/ "./src/index.less":
    /***/ (function (__unused_webpack_module, exports, __webpack_require__) {
           
            /***/
        })
    /******/
});

但是跟普通的js文件处理有一点是不同的,就是里面进行了一个内联loader的指定,即

import content, * as namedExport
    from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";

"!!"是忽略webpack.config.js的规则,直接使用"!!"配置的规则,也就是说遇到xxx.less文件,不再使用webpack.config.js配置的style-loader,而是使用css-loaderless-loader
最终从css-loaderless-loader转化得到的content内容会通过之前的js代码插入到DOM

import content, * as namedExport
    from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
得到css-loader结果后,将style插入到DOM
import content, * as namedExport
    from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var update = API(content, options);
通过import获取CSS样式数据后,API()又是什么?

根据对打包文件的代码精简,本质API就是下面的module.exports = function (list, options){}方法,最终是遍历list,然后调用addElementStyle()方法

module.exports = function (list, options) {
    //...
    var lastIdentifiers = modulesToDom(list, options);
    return function update(newList) {
       //...初始化不会调用内部的update(),因此省略
    }
}
function modulesToDom(list, options) {
    for (var i = 0; i < list.length; i++) {
        var item = list[i];
        var obj = {
            css: item[1],
            media: item[2]
            //...
        };
        var updater = addElementStyle(obj, options);
        //...
    }
    //...
}

addElementStyle()方法经过一系列方法的调用,本质也是利用了document.createElement("style"),然后调用options.setAttributesoptions.insert进行styleDOM插入

options.setAttributesoptions.insert不知道又是什么哪个文件的工具函数=_=这里不再分析
function addElementStyle(obj, options) {
    var api = options.domAPI(options);
    api.update(obj);
    var updater = function updater(newObj) {
      //...
    };
    return updater;
}
function domAPI(options) {
    var styleElement = options.insertStyleElement(options);
    return {
        update: function update(obj) {
            apply(styleElement, options, obj);
        },
        remove: function remove() {
            removeStyleElement(styleElement);
        }
    };
}
function insertStyleElement(options) {
    var element = document.createElement("style");
    options.setAttributes(element, options.attributes);
    options.insert(element, options.options);
    return element;
}

3.10 build流程小模块-noParse

可以在webpack.config.js配置不需要解析的文件,然后在this.parser.parse()之前会使用noParseRule跟目前的请求进行比对,如果符合则不进行下一步的解析逻辑

// check if this module should !not! be parsed.
// if so, exit here;
const noParseRule = options.module && options.module.noParse;
if (this.shouldPreventParsing(noParseRule, this.request)) {
  // We assume that we need module and exports
  this.buildInfo.parsed = false;
  this._initBuildHash(compilation);
  return handleBuildDone();
}

3.11 build流程小模块-JavascriptParser._parse源码分析

通过AST内容,遍历特定key,收集Module的依赖Dependency,为后续processModuleDependencies()方法处理做准备
// node_modules/webpack/lib/NormalModule.js
result = this.parser.parse(this._ast || source, {
  source,
  current: this,
  module: this,
  compilation: compilation,
  options: options
});

由上面3.4 resolve-getParse()的分析,我们可以知道,this.parser=JavascriptParser,因此我们需要分析JavascriptParser类的parse()方法,如下面代码块所示

const { Parser: AcornParser } = require("acorn");
const parser = AcornParser.extend(importAssertions);

class JavascriptParser extends Parser {
    parse(source, state) {
        ast = JavascriptParser._parse(source, {
            sourceType: this.sourceType,
            onComment: comments,
            onInsertedSemicolon: pos => semicolons.add(pos)
        });
        this.detectMode(ast.body);
        this.preWalkStatements(ast.body);       
        this.blockPreWalkStatements(ast.body);
        this.walkStatements(ast.body);
        return state;
    }

    static _parse(code, options) {
        ast = /** @type {AnyNode} */ (parser.parse(code, parserOptions));
        return ast
    }
}

从上面代码块可以知道,主要分为两个部分:

  • 使用acorn库将js转化为AST
  • 使用preWalkStatementsblockPreWalkStatementswalkStatements对转化后的AST.body进行分析

acornjs转化为AST的流程难度较高,本文不对这方面进行具体的分析,感兴趣可以另外寻找文章学习

解析ast.body的流程,本质上是对多种条件进行列举处理,比如do-while语句形成的AST type=DoWhileStatementfor..in语句形成的AST type=ForInStatement,由于内容过大且繁杂,本文不会对所有的流程进行分析,对解析ast.body所有的流程感兴趣的用户,可以参考这篇文章:模块构建之解析_source获取dependencies

ast.body流程解析逆向推导

11-webpack5-make-parse逆向推导.svg
我们从上面的流程图可以知道,我们在buildMode的下一个阶段会进行processModuleDependencies()的处理,而processModuleDependencies()方法中最核心的代码就是module.dependenciesmodule.blocks,换句话说,我们在JavascriptParser._parse()的流程中,应该最关注的就是怎么拿到moduledependenciesblocks

我们在上面的分析可以知道,进行buildMode()时,实际上已经转化为NormalModule类的处理,因此我们可以很快从NormalModule.js->继承Module.js->继承DepencenciesBlock.js中找到对应的addBlock()addDependency(),我们直接进行debugger

截屏2023-01-14 22.07.40.png

然后我们就可以轻易拿到每一次为当前NormalModule添加dependenciesblocks时的代码流程,如下面图所示,我们可以清楚看到,具体的流程为:

  • JavascriptParser.parse()
  • blockPreWalkStatements()
  • blockPreWalkStatement()
  • blockPreWalkImportDeclaration()
  • addDependency()

截屏2023-01-14 22.11.44.png

但是这个语句到底是对应源代码哪一句呢?我们需要知道哪一句源代码,我们才能更好理解JavascriptParser.parse()这个流程到底做了什么

ast.body内容

这个时候我们可以借助AST在线解析网站,我们直接把调试代码放上去,我们就可以得到非常清晰源码对应的AST Body,而且我们也可以看到了上面图中所出现的ImportDeclaration字段

截屏2023-01-14 22.18.22.png

通过示例分析dependency和block的添加流程

凭借上面debugger和AST在线解析网站,我们可以完整地知道示例代码中哪一句形成了什么AST语句,以及后面根据这些语句进行了怎样的流程

下面通过几个流程图展示示例源码中依赖收集流程

// src/index.js
import {getC1} from "./item/index_item-parent1.js";
import "./index.scss";
var _ = require("lodash");
import {getContextItem} from "./contextItem";

var test = _.add(6, 4) + getC1(1, 3)

function getAsyncValue() {
    var temp = getContextItem();
    import("./item/index_item-async.js").then((fnGetValue)=> {
        console.log("async", fnGetValue());
    });
    return temp;
}

setTimeout(()=> {
    console.log(getAsyncValue());
}, 1000);

12-webpack5-make-入口文件index.svg

import语句和方法调用
import {getC1} from "./item/index_item-parent1.js";
// 上面的语句会触发addDependency(HarmonyImportSideEffectDependency)

var test = _.add(6, 4) + getC1(1, 3)
// 上面的语句会触发addDependency(HarmonyImportSpecifierDependency)

7-webpack-make-分析module的依赖.svg

require语句和方法调用
var _ = require("lodash");
// 上面的语句会触发addDependency(CommonJsRequireDependency)

var test = _.add(6, 4);
// 上面的语句不会触发任何addDependency()和addBlock()

7-webpack-make-分析module的依赖-require.svg

异步import语句和方法调用
function getAsyncValue() {
    import("./item/index_item-async.js")
        .then((fn)=> {
        console.log("async", fn());
    });
    // 上面的语句会触发addBlock(new AsyncDepenciesBlock(ImportDependency))
}

8-webpack-make-分析module的依赖-异步.svg

export语句
import {getTemp} from "babel-loader!./index_item-inline";
export function getC1(a, b) {
    return getTemp() + 33 + a + b;
}
// 上面的语句会触发addDependency(new HarmonyExportSpecifierDependency())

9-webpack-make-分析module的依赖-export.svg

3.12 processModuleDependencies

从上面3.11的分析,我们知道:
import形成的依赖是:HarmonyImportSideEffectDependency
require形成的依赖是:CommonJsRequireDependency
import的方法调用形成的依赖是:HarmonyImportSpecifierDependency
export的方法调用形成的依赖是:HarmonyExportSpecifierDependency

从下面代码可以知道,buildMode()结束后,我们会处理_processModuleDependencies()

  • 遍历module.dependencies->调用processDependency()处理依赖
  • 异步的依赖module.blocks,则当作module压入queue中继续处理module.blocks[i]dependenciesblocks
  • 如果全部处理完毕,则调用onDependenciesSorted()
_processModuleDependencies(module, callback) {
    let inProgressSorting = 1;

    const queue = [module];
    do {
        const block = queue.pop();
        // import依赖
        if (block.dependencies) {
            currentBlock = block;
            let i = 0;
            for (const dep of block.dependencies) processDependency(dep, i++);
        }
        // 异步的依赖
        if (block.blocks) {
            for (const b of block.blocks) queue.push(b);
        }
    } while (queue.length !== 0);

    // inProgressSorting: 正在进行排序,inProgressSorting=0说明已经排序完成,即完成上面的processDependency()清空queue
    if (--inProgressSorting === 0) onDependenciesSorted();
}

3.12.1 processDependency

  • 建立moduledependency之间的关联(this.moduleGraph=new ModuleGraph()
  • 筛选出resourceIdent为空的依赖
  • 将依赖存入sortedDependencies中,为下面的onDependenciesSorted()做准备
const processDependency = (dep, index) => {
    // 建立module与dependency之间的关联
    this.moduleGraph.setParents(dep, currentBlock, module, index);
    // ...省略一系列的缓存逻辑
    // 将dependency放入到sortedDependencies数组中
    processDependencyForResolving(dep);
};
const processDependencyForResolving = dep => {
    const resourceIdent = dep.getResourceIdentifier();
    if (resourceIdent !== undefined && resourceIdent !== null) {
        const category = dep.category;
        const constructor = dep.constructor;
        const factory = this.dependencyFactories.get(constructor);
        sortedDependencies.push({
            factory: factoryCacheKey2,
            dependencies: list,
            context: dep.getContext(),
            originModule: module
        });
        list.push(dep);
        listCacheValue = list;
    }
};

processDependencyForResolveing()主要是进行resourceIdent的筛选以及同一个requestdependency的合并

resourceIdent筛选
const processDependencyForResolving = dep => {
    const resourceIdent = dep.getResourceIdentifier();
    if (resourceIdent !== undefined && resourceIdent !== null) {
       //.....
    } else {
      debugger;
    }
};
直接在processDependencyForResolving()进行debugger调试(如上面代码块所示)

我们会发现,exports所形成的dependencyresourceIdent都为空,然后我们看了下其中一种exports类型HarmonyExportSpecifierDependency的源码,我们可以从下面代码块发现,getResourceIdentifier直接返回了null,因此在processDependencyForResolving()中会直接过滤掉exports所形成的dependency

class HarmonyExportSpecifierDependency extends NullDependency {}
class NullDependency extends Dependency {}
class Dependency {
  getResourceIdentifier() {
        return null;
    }
}
同一个request的dependency的合并

经过上面processDependencyForResolveing()的处理,最终形成以requestkey的依赖数据对象,比如下面代码块,以request="./item/index_item-parent1.js"的所有dependency都会合并到sortedDependencies[0]dependencies

HarmonyImportSideEffectDependency代表的是import {getC1} from "./item/index_item-parent1.js"形成的依赖
HarmonyImportSpecifierDependency代表的是var test = getC1()形成的依赖
sortedDependencies[0] = {
    dependencies: [
        { // HarmonyImportSideEffectDependency
            request: "./item/index_item-parent1.js",
            userRequest: "./item/index_item-parent1.js"
        },
        { // HarmonyImportSpecifierDependency
            name: "getC1",
            request: "./item/index_item-parent1.js",
            userRequest: "./item/index_item-parent1.js"
        }
    ],
    originModule: {
        userRequest: "/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js/src/index.js",
        dependencies: [
            //...10个依赖,包括上面那两个Dependency
        ]
    }
}

3.12.2 onDependenciesSorted

  • 遍历循环sortedDependencies数组,拿出依赖对象dependency进行handleModuleCreation()重复上面resolve()->build()形成NormalModule数据的流程
  • 递归调用handleModuleCreation()完成所有依赖对象dependency的转化后,触发onTransitiveTasksFinished()方法
  • 结束最外层的handleModuleCreation()回调,结束make流程
const onDependenciesSorted = err => {
    // 处理所有的依赖,进行handleModuleCreation的调用,处理完成后,调用callback(),回到最初的handleModuleCreation()的回调
    for (const item of sortedDependencies) {
        inProgressTransitive++;
        this.handleModuleCreation(item, err => {
            if (--inProgressTransitive === 0) onTransitiveTasksFinished();
        });
    }
    if (--inProgressTransitive === 0) onTransitiveTasksFinished();
}

const onTransitiveTasksFinished = err => {
    if (err) return callback(err);
    this.processDependenciesQueue.decreaseParallelism();

    return callback();
};
依赖对象dependency进行handleModuleCreation()

我们在上面的sortedDependencies数组拼接中可以知道,我们的sortedDependencies[i]dependencies实际上是一个数组集合,那我们继续调用handleModuleCreation()是如何处理这种数组集合的呢?

// node_modules/webpack/lib/NormalModuleFactory.js
create(data, callback) {
    const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
    const dependency = dependencies[0];
    const request = dependency.request;
    const dependencyType =
        (dependencies.length > 0 && dependencies[0].category) || "";

    const resolveData = {
        request,
        dependencies,
        dependencyType
    };
    // 利用resolveData进行一系列的resolve()和buildModule()操作...
}

我们从上面的分析知道,handleModuleCreation()一开始调用的NormalModuleFactory.create()的相关逻辑,如上面代码块所示,我们会从dependencies中拿到第一个元素,dependencies[0],实际上对应的也是顶部的import语句,如import {getC1} from "./item/index_item-parent1.js"所形成的依赖HarmonyImportSideEffectDependency,我们会将HarmonyImportSideEffectDependency对应的request路径作为入口,进行整个NormalModule数据的创建

import {getC1} from "./item/index_item-parent1.js";
var test = _.add(6, 4) + getC1(1, 3);
var test1 = _.add(6, 4) + getC1(1, 3);
var test2 =  getC1(4, 5);

截屏2023-01-15 14.55.26.png
因此无论我们多少次调用getC1()这个方法(形成多个HarmonyImportSpecifierDependency),我们只会取第一个HarmonyImportSideEffectDependency作为依赖对象的handleModuleCreation()构建

4. 其它细节分析

4.1 loader优先级以及inline写法跳过优先级

参考https://webpack.js.org/concepts/loaders/#inline

前缀为"!"将禁用所有已配置的normal loaders

import Styles from '!style-loader!css-loader?modules!./styles.css';


前缀为"!!"将禁用所有已配置的加载程序(preLoadersloaderspostLoaders

import Styles from '!!style-loader!css-loader?modules!./styles.css';

前缀为 "-!"将禁用所有已配置的preLoadersloaders,但不会禁用postLoaders

4.2 enhanced-resolve不同类型的处理分析

resolver.resolve()的具体流程

由于篇幅原因,放在下一篇文章「Webpack5源码」enhanced-resolve路径解析库源码分析中分析

5. 总结

5.1 resolve

5.1.1 enhanced-resolve处理路径

处理module、相对路径、绝对路径、使用别称等情况,将路径拼接为绝对路径

5.1.2 解析出目前路径path适合的loaders

根据webpack.config.js配置的规则,筛选目前路径应用的loaders,比如index.less适配style-loader+css-loader+less-loader

5.2 build

5.2.1 NormalModule._doBuild

调用runLoaders()进行loaders的转化,比如xxx.less转化为styleES6转化ES5等等

5.2.2 this.parser.parse

拿到转化后统一标准的数据后,调用require("acorn")对这些数据进行AST分析,得到对应的依赖关系,获取对应module的依赖dependencies

5.3 processModuleDependencies

5.3.1 转化依赖文件

根据this.parser.parse拿到的依赖关系,调用handleModuleCreate()进行上面流程的重复执行

5.3.2 建立依赖dependency与目前父module的关系

在调用handleModuleCreate()进行上面流程的重复执行时,originModule是当前的Moduledependencies是当前的Module的依赖
然后经历handleModuleCreation()->factorizeModule()->addModule()后,会使用moduleGraph进行依赖与目前父module的关系的绑定,如下面代码所示

从入口文件调用handleModuleCreation()originModule为空,只有dependency触发handleModuleCreation()时才会传入originModule
handleModuleCreation({
    factory,
    dependencies,
    originModule,
    contextInfo,
    context,
    recursive = true,
    connectOrigin = recursive
}){
    const factoryResult = await this.factorizeModule();
    await this.addModule();

    for (let i = 0; i < dependencies.length; i++) {
        const dependency = dependencies[i];
        moduleGraph.setResolvedModule(
            connectOrigin ? originModule : null,
            dependency,
            unsafeCacheableModule
        );
        unsafeCacheDependencies.set(dependency, unsafeCacheableModule);
    }
}
class ModuleGraphModule {
    setResolvedModule(originModule, dependency, module) {
        const connection = new ModuleGraphConnection(
            originModule,
            dependency,
            module,
            undefined,
            dependency.weak,
            dependency.getCondition(this)
        );
        const connections = this._getModuleGraphModule(module).incomingConnections;
        connections.add(connection);
        if (originModule) {
            const mgm = this._getModuleGraphModule(originModule);
            mgm.outgoingConnections.add(connection);
        } else {
            this._dependencyMap.set(dependency, connection);
        }
    }
}

而这个关系的绑定在后面seal阶段处理依赖时会发挥作用,比如下面的processBlock()会从当前moduleGraph拿到module对应的outgoingConnections

const processBlock = block => {
    const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
}

getBlockModules() {
    //...省略初始化blockModules和blockModulesMap的逻辑
    extractBlockModules(module, moduleGraph, runtime, blockModulesMap);
    blockModules = blockModulesMap.get(block);
    return blockModules;
}
const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
    for (const connection of moduleGraph.getOutgoingConnections(module)) {
      const d = connection.dependency;
              // We skip connections without dependency
        if (!d) continue;
      //....
    }
}

5.4 编译入口->make->seal流程图总结

make-seal详细版.svg

参考

  1. 精通 Webpack 核心原理专栏
  2. webpack@4.46.0 源码分析 专栏
  3. webpack loader 从上手到理解系列:style-loader
  4. webpack5 源码详解 - 先导
  5. webpack5 源码详解 - 编译模块

工程化文章

  1. 「Webpack5源码」热更新HRM流程浅析
  2. 「Webpack5源码」make阶段(流程图)分析
  3. 「Webpack5源码」enhanced-resolve路径解析库源码分析
  4. 「vite4源码」dev模式整体流程浅析(一)
  5. 「vite4源码」dev模式整体流程浅析(二)

白边
209 声望37 粉丝

源码爱好者,已经完成vue2和vue3的源码解析+webpack5整体流程源码+vite4开发环境核心流程源码+koa2源码