乐趣区

关于webpack:Webpack5源码make阶段流程图分析

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

因为 webpack5 整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于 js 文件类型进行剖析,不会对其它类型(cssimage)进行剖析,所举的例子也都是基于 js 类型

为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码,并且文章默认读者曾经把握 tapableloaderplugin 等基础知识,对文章中呈现 asyncQueuetapableloaderplugin 相干代码都会间接展现,不会减少过多阐明

文章内容

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

1. 初始化

1.1 npm run build

1.1.1 源码剖析

当咱们执行 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.1.2 流程图

下面 1.1 npm run build的剖析能够稀释为上面这张流程图:

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()

如上面代码所示,次要执行了 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.2 compiler.run()

1.2.1 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.2.3 整体流程图

下面的一系列剖析能够稀释为上面这张流程图:

下图中的 compiler.compile() 相干内容会在下一大节进行剖析

1.3 小结

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

2. make 阶段 - 整体流程图

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

因为 webpack5 细节过于繁冗,各种 callback 调用,因而不会进行各个子流程办法调用具体的流程推导过程,间接以下面总结的流程图为根底,对其中的外围步骤进行剖析

从下面流程图能够晓得,make阶段有三种次要流程:resolvebuildprocessModuleDependencies,上面的剖析将这三种外围步骤进行剖析

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

下面流程能够总结为:

  • 解析 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 进行门路的整顿,如上面的图片所示
  • 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

type normal loader
resolveOptions

其中 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)获取到的数据如下图所示

而后触发 convertToResolveOptions() 办法,经验几个办法的调用执行后,最终触发 resolveByProperty(),如下图所示,会依据下面EntryDependency 失去的 "esm" 进行参数的合并,最终失去残缺的配置参数

第 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() 之前就执行的,如上面流程图所示

从下面流程图能够晓得,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 的参数配置

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()


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"}

3.4 resolve-getParser()

默认设置 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 整体流程图

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.8.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.8.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()

源码逻辑比拟清晰简略,能够应用一个流程图展现

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 拼接的代码,咱们间接应用下面具体例子断点取出这一串代码

上面代码块就是下面截图所获取到的代码,最终 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 的执行

因而此时 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


因为 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 流程解析逆向推导

咱们从下面的流程图能够晓得,咱们在 buildMode 的下一个阶段会进行 processModuleDependencies() 的解决,而 processModuleDependencies() 办法中最外围的代码就是 module.dependenciesmodule.blocks,换句话说,咱们在 JavascriptParser._parse() 的流程中,应该最关注的就是怎么拿到 moduledependenciesblocks

咱们在下面的剖析能够晓得,进行 buildMode() 时,实际上曾经转化为 NormalModule 类的解决,因而咱们能够很快从 NormalModule.js-> 继承Module.js-> 继承DepencenciesBlock.js 中找到对应的 addBlock()addDependency(),咱们间接进行debugger

而后咱们就能够轻易拿到每一次为以后 NormalModule 增加 dependenciesblocks时的代码流程,如上面图所示,咱们能够分明看到,具体的流程为:

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

然而这个语句到底是对应源代码哪一句呢?咱们须要晓得哪一句源代码,咱们能力更好了解 JavascriptParser.parse() 这个流程到底做了什么

ast.body 内容

这个时候咱们能够借助 AST 在线解析网站,咱们间接把调试代码放上去,咱们就能够失去十分清晰源码对应的 AST Body,而且咱们也能够看到了下面图中所呈现的ImportDeclaration 字段

通过示例剖析 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);
import 语句和办法调用
import {getC1} from "./item/index_item-parent1.js";
// 下面的语句会触发 addDependency(HarmonyImportSideEffectDependency)

var test = _.add(6, 4) + getC1(1, 3)
// 下面的语句会触发 addDependency(HarmonyImportSpecifierDependency)
require 语句和办法调用
var _ = require("lodash");
// 下面的语句会触发 addDependency(CommonJsRequireDependency)

var test = _.add(6, 4);
// 下面的语句不会触发任何 addDependency()和 addBlock()
异步 import 语句和办法调用
function getAsyncValue() {import("./item/index_item-async.js")
        .then((fn)=> {console.log("async", fn());
    });
    // 下面的语句会触发 addBlock(new AsyncDepenciesBlock(ImportDependency))
}
export 语句
import {getTemp} from "babel-loader!./index_item-inline";
export function getC1(a, b) {return getTemp() + 33 + a + b;
}
// 下面的语句会触发 addDependency(new HarmonyExportSpecifierDependency())

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);

因而无论咱们多少次调用 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;
      //....
    }
}

参考

  1. 精通 Webpack 外围原理专栏
  2. webpack@4.46.0 源码剖析 专栏
  3. webpack loader 从上手到了解系列:style-loader
  4. webpack5 源码详解 – 先导
  5. webpack5 源码详解 – 编译模块
退出移动版