本文内容基于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.jsconst runCLI = require("../lib/bootstrap");runCLI(process.argv);// webpack-cli/lib/bootstrap.jsconst 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.jsconst 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.jsconst 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.applyEntryOptionstatic 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.jsconst webpack = (options, callback) => {  const { compiler, watch, watchOptions } = create(options);  compiler.run();  return compiler;}// node_modules/webpack/lib/Compiler.jsclass 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.jsnewCompilation(params) {    const compilation = this.createCompilation(params);    this.hooks.compilation.call(compilation, params);    return compilation;}// node_modules/webpack/lib/EntryPlugin.jscompiler.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.jsaddModuleTree() {    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类型的loaderResolverconst loaderResolver = this.getResolver("loader");
// 一般文件类型的loaderResolverconst 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.jsget(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.jsconst createCompiler = rawOptions => {    const options = getNormalizedWebpackOptions(rawOptions);    applyWebpackOptionsDefaults(options);    new WebpackOptionsApply().process(options, compiler);    return compiler;};// node_modules/webpack/lib/config/defaults.jsconst 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.jscompiler.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

其中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.jscompiler.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步:依据不同的配置创立不同的resolverconst resolver = Factory.createResolver(resolveOptions);

创立过程中会注册十分十分多的plugin,期待后续的resolver.resolve()调用来解析门路

// node_modules/enhanced-resolve/lib/ResolverFactory.jscreateResolver = 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.jsresolve(context, path, request, resolveContext, callback) {    return this.doResolve(this.hooks.resolve, ...args);}
// node_modules/enhanced-resolve/lib/Resolver.jsdoResolve(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.jscompile(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.jsconst 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.jsnormalModuleFactory.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.jsnormalModuleFactory.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.jsbuild(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.jsimport "./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.jsimport {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.jsresult = 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.jsimport {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.jscreate(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 源码详解 - 编译模块