2

webpack源码阅读之主流程分析

图片描述

comipler是其webpack的支柱模块,其继承于Tapable类,在compiler上定义了很多钩子函数,贯穿其整个编译流程,这些钩子上注册了很多插件,用于在特定的时机执行特定的操作,同时,用户也可以在这些钩子上注册自定义的插件来进行功能拓展,接下来将围绕这些钩子函数来分析webpack的主流程。

1. compiler实例化

compiler对象的生成过程大致可以简化为如下过程,首先对我们传入的配置进行格式验证,接着调用Compiler构造函数生成compiler实例,自定义的plugins注册,最后调用new WebpackOptionsApply().process(options, compiler)进行默认插件的注册,comailer初始化等。

const webpack = (options,callback)=>{
    //options格式验证
  const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
  ...
  //生成compiler对象
    let compiler = new Compiler(options.context);
  
  //自定义插件注册
  if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
  
  //默认插件注册,默认配置等
  compiler.options = new WebpackOptionsApply().process(options, compiler);
}

Webpackoprionapply是一个重要的步骤,通常是此处插件注册在compiler.hooks.thisCompilation或compiler.hooks.compilation上,并在compilation钩子上调用时,进一步注册到parser(用于生成依赖及依赖模版)或者mainTemplate(用于seal阶段render)的钩子上:

process(options, compiler) {
  //当target是一个函数时,可以自定义该环境下使用哪些plugins
        if (typeof options.target === "string") {
//1.不同target下引入不同的plugin进行文件加载
            switch (options.target) {
                case "web":
          //JsonpTemplatePlugin插件注册在compiler.hooks.this.compilation上,并在该钩子调用时,在compilation.mainTemplate的多个钩子上注册事件以在最后生成的代码中加入Jsonp Script进行文件加载
                    new JsonpTemplatePlugin().apply(compiler);
                    new FetchCompileWasmTemplatePlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
          //在compiler.hooks.compilation上注册,并挂载在compilation.moduleTemplates.javascript上,在seal阶段template.hooks.render时调用
                    new FunctionModulePlugin().apply(compiler);
                    new NodeSourcePlugin(options.node).apply(compiler);
                    new LoaderTargetPlugin(options.target).apply(compiler);
                    break;
                case "node":
                case "async-node":
          //如果目标环境为node,可以用require方式加载文件,而不需要使用Jsonp
                    new NodeTemplatePlugin({
                        asyncChunkLoading: options.target === "async-node"
                    }).apply(compiler);
                    new ReadFileCompileWasmTemplatePlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
                    new FunctionModulePlugin().apply(compiler);
                    new NodeTargetPlugin().apply(compiler);
                    new LoaderTargetPlugin("node").apply(compiler);
                    break;
                ...........
        }
//2. output Library处理
          ...........
//3. devtool sourceMap处理
        ...........
//注册在compiler.hooks.compilation上,给normalModuleFactory的js模块提供Parser、JavascriptGenerator对象 ,并给seal阶段的template提供renderManifest数组(包含render方法)           
        new JavascriptModulesPlugin().apply(compiler);
//注册在compiler.hooks.compilation上,给normalModuleFactory的jso n模块提供Parser、JavascriptGenerator对象      
        new JsonModulesPlugin().apply(compiler);
//同理,webassembly模块      
        new WebAssemblyModulesPlugin({
            mangleImports: options.optimization.mangleWasmImports
        }).apply(compiler);

//4. 入口不同格式下的处理,注册在compiler.hooks.entryOption,在调用时新建SingleEntryPlugin或MultiEntryPlugin 
        new EntryOptionPlugin().apply(compiler);
        compiler.hooks.entryOption.call(options.context, options.entry);

//5. 不同模块写法的处理,一般注册在compiler.hooks.compilation上,调用时在normalModuleFactory.hooks.parse上注册,接着在parse的hooks上注册,在parse阶段,遇到不同的节点调用不同的plugin,从而在模块的dependencies数组中推入不同的dependencyFactory和dependencyTemplate
        new CompatibilityPlugin().apply(compiler);
      //es模块
        new HarmonyModulesPlugin(options.module).apply(compiler);
        if (options.amd !== false) {
      //AMD模块
            const AMDPlugin = require("./dependencies/AMDPlugin");
            const RequireJsStuffPlugin = require("./RequireJsStuffPlugin");
            new AMDPlugin(options.module, options.amd || {}).apply(compiler);
            new RequireJsStuffPlugin().apply(compiler);
        }
      //CommonJS模块
        new CommonJsPlugin(options.module).apply(compiler);
        new LoaderPlugin().apply(compiler);
        if (options.node !== false) {
            const NodeStuffPlugin = require("./NodeStuffPlugin");
            new NodeStuffPlugin(options.node).apply(compiler);
        }
        new ImportPlugin(options.module).apply(compiler);
        new SystemPlugin(options.module).apply(compiler);
     .........
     
//6. 优化
     .........
        
//7. modeId、chunkId相关
     .........
     
//8. resolve初始配置,在resolve时调用this.getResolver时调用     
        compiler.resolverFactory.hooks.resolveOptions
            .for("normal")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem
                    },
                    cachedCleverMerge(options.resolve, resolveOptions)
                );
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("context")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem,
                        resolveToContext: true
                    },
                    cachedCleverMerge(options.resolve, resolveOptions)
                );
            });
        compiler.resolverFactory.hooks.resolveOptions
            .for("loader")
            .tap("WebpackOptionsApply", resolveOptions => {
                return Object.assign(
                    {
                        fileSystem: compiler.inputFileSystem
                    },
                    cachedCleverMerge(options.resolveLoader, resolveOptions)
                );
            });
        compiler.hooks.afterResolvers.call(compiler);
        return options;
    }

2. compiler.run

生成compler实例后,cli.js中就会调用compiler.run方法了,compiler.run的流程大致可以简写如下(去掉错误处理等逻辑),其囊括了整个打包过程,首先依次触发beforeRun、run等钩子,接下来调用compiler.compile()进行编译过程,在回调中取得编译后的compilation对象,调用compiler.emitAssets()输出打包好的文件,最后触发done钩子。

run(){
  const onCompiled = (err, compilation) => {
    //打包输出
            this.emitAssets(compilation, err => {
                this.hooks.done.callAsync(stats)
        };
    // beforeRun => run => this.compile()                 
        this.hooks.beforeRun.callAsync(this, err => {
            this.hooks.run.callAsync(this, err => {
                this.readRecords(err => {
                    this.compile(onCompiled);
                });
            });
        });
}

3. compiler.compile

在这个方法中主要也是通过回调触发钩子进行流程控制,通过newCompilation=>make=>finsih=>seal流程来完成一次编译过程,compiler将具体一次编译过程放在了compilation实例上,可以将主流程与编译过程分割开来,当处于watch模式时,可以进行多次编译。

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            this.hooks.compile.call(params);
            const compilation = this.newCompilation(params);
            this.hooks.make.callAsync(compilation, err => {
                compilation.finish(err => {
                    compilation.seal(err => {
                        this.hooks.afterCompile.callAsync(compilation, err => {
                            return callback(null, compilation);
                        });
                    });
                });
            });
        });
    }

从图中可以看到make钩子上注册了singleEntryPlugin(单入口配置时),compilation作为参数传入该插件,接着在插件中调用compilation.addEntry方法开始编译过程。

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

                const dep = SingleEntryPlugin.createDependency(entry, name);
                compilation.addEntry(context, dep, name, callback);
            }
        );

4. compilation过程

编译过程的入口在compilation._addModuleChain函数,传入entry,context参数,在回调中得到编译生成的module。编译的过程包括文件和loader路径的resolve,loader对源文件的处理,递归的进行依赖处理等等.

addEntry(context, entry, name, callback) {
        this.hooks.addEntry.call(entry, name);
        this._addModuleChain(
            context,
            entry,
            module => {
                this.entries.push(module);
            },
            (err, module) => {
                this.hooks.succeedEntry.call(entry, name, module);
                return callback(null, module);
            }
        );
    }

this._addModuleChain中调用moduleFactory.create()来开始模块的创建,模块创建第一步需要通过resolve得到入口文件的具体路径。

4.1 resolve

webpack 中每涉及到一个文件,就会经过 resolve 的过程。webpack 使用 enhanced-resolve 来提供绝对路径、相对路径、模块路径的多样解析方式。

图片描述

moduleFactory.create()resolve过程从通过调用normalModuleFactory中factory函数开始。

factory(result, (err, module) => {
                    if (err) return callback(err);

                    if (module && this.cachePredicate(module)) {
                        for (const d of dependencies) {
                            dependencyCache.set(d, module);
                        }
                    }

                    callback(null, module);
                });

//传入的result的基本形式如下
result = {
  context: "/Users/hahaha/project/demo/webpack-demo"
    contextInfo: {issuer: "", compiler: undefined}
    dependencies: [SingleEntryDependency]
    request: "./src/index.js"
    resolveOptions: {}
}

factory方法拿到入口信息result后,将result传递给resolver方法,resolver方法先得到对普通文件和loader文件的resolve方法:

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);

然后检查路径中是否包含内联loaders, 通过调用loaderResolver和normalResolver并行的resolve文件路径和内联loaders路径。如果使用了内联loaders,则将其保存在loaders变量中,接着对得到的文件路径进行ruler匹配,得到匹配到的loader数值:

const result = this.ruleSet.exec({
                            resource: resourcePath,
                            realResource:
                                matchResource !== undefined
                                    ? resource.replace(/\?.*/, "")
                                    : resourcePath,
                            resourceQuery,
                            issuer: contextInfo.issuer,
                            compiler: contextInfo.compiler
                        });·

this.ruleSet是用户定义的loaders和默认loader的格式化的结果,通过其exec方法可以得到与资源文件匹配的loaders数组。

接下来并行的resolver这些loaders路径,并保存在loaders数组中;值得注意的是,在resolver钩子的回调中初始化了parser和generator对象:

parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),

parser的注册方法如下(webpackOptionapply中各种模块处理的插件就是这样注册的):

compiler.hooks.normalModuleFactory.tap('MyPlugin', factory => {
  factory.hooks.parser.for('javascript/auto').tap('MyPlugin', (parser, options) => {
    parser.hooks.someHook.tap(/* ... */);
  });
});

入口文件通过resolver后得到的结果类似如下(没使用loaders,所以为空数组):

{
  context: "/Users/hahaha/project/demo/webpack-demo"
  dependencies: [SingleEntryDependency]
  generator: JavascriptGenerator {}
  loaders: [] 
  matchResource: undefined
  parser: Parser {_pluginCompat: SyncBailHook, hooks: {…}, options: {…}, sourceType: "auto", scope: undefined, …}
  rawRequest: "./src/index.js"
  request: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
  resolveOptions: {}
  resource: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
  resourceResolveData: {
    context: {…}, 
         path: "/Users/hahaha/project/demo/webpack-demo/src/index.js",
    request: undefined, 
    query: "",
    module: false, …
  }
  settings: {type: "javascript/auto", resolve: {…}}
  type: "javascript/auto"
  userRequest: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
}

resolver过程得到的入口文件的路径,接下来在factory方法中会调用createdModule = new NormalModule(result)生成模块,改构造函数将resolver得到的信息保存到模块上,并提供了一些实例方法来进行后续的build过程。

本文并未深入resolve具体流程,详情可以参阅:

webpack系列之三resolve

4.2 build

图片描述

moduleFactory.create()方法的回调中得到resolve后生成的module后,将开始模块的build过程,我将代码主干保留如下:

moduleFactory.create(
                {
                    contextInfo: {
                        issuer: "",
                        compiler: this.compiler.name
                    },
                    context: context,
                    dependencies: [dependency]
                },
                (err, module) => {
                    const afterBuild = () => {
                        if (addModuleResult.dependencies) {
                            this.processModuleDependencies(module, err => {
                                if (err) return callback(err);
                                callback(null, module);
                            });
                        } else {
                            return callback(null, module);
                        }
                    };
                        this.buildModule(module, false, null, null, err =>                             {
                            afterBuild();
                        });
                
                }
            );

首先调用this.buildModule方法,由于moduleFactory.create()生成的module是normalModule(本例中)的实例,所以可以实际上是调用normalModule.doBuild()进行build,可以看到首先生成了一个loaderContext对象,在后面运行loader的时候,会通过call方法将loader的this指向loaderContext。

doBuild(options, compilation, resolver, fs, callback) {
        const loaderContext = this.createLoaderContext(
            resolver,
            options,
            compilation,
            fs
        );

        runLoaders(
            {
                resource: this.resource,
                loaders: this.loaders,
                context: loaderContext,
                readResource: fs.readFile.bind(fs)
            },
            (err, result) => {
                ...
                return callback();
            }
        );
    }

接下来就进入runloaders方法了,传入的参数包括模块路径,模块的loaders数组,loaderContext等。在该方法内,首先对相关参数进行初始化的操作,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不同的 loader 执行的阶段可以动态的获取一些参数。接下来进入iteratePitchingLoaders方法:

function iteratePitchingLoaders(options, loaderContext, callback) {
    //当处理完最后一个loader的pitch后,倒序开始处理loader的normal方法
    if(loaderContext.loaderIndex >= loaderContext.loaders.length)
        return processResource(options, loaderContext, callback);

    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

    // iterate
    if(currentLoaderObject.pitchExecuted) {
        loaderContext.loaderIndex++;
        return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 在loadLoader中,通过module = require(loader.path)加载loader,并将module上的normal、pitch、raw属性拷贝到loader对象上
    loadLoader(currentLoaderObject, function(err) {
        if(err) {
            loaderContext.cacheable(false);
            return callback(err);
        }
        var fn = currentLoaderObject.pitch;
        currentLoaderObject.pitchExecuted = true;
    //如果没有该loader上没有pitch,则跳到下一个loader的pitch
        if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
        //在runSyncOrAsync内执行loader上的pitch函数
        runSyncOrAsync(
            fn,
            loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
            function(err) {
                if(err) return callback(err);
                var args = Array.prototype.slice.call(arguments, 1);
                if(args.length > 0) {
                    loaderContext.loaderIndex--;
                    iterateNormalLoaders(options, loaderContext, args, callback);
                } else {
                    iteratePitchingLoaders(options, loaderContext, callback);
                }
            }
        );
    });
}

在深入runSyncOrAsync函数之前,我们先来介绍下webpack官网上的loader API

  • 同步loader

    //当不返回map,meta时可以直接返回
    module.exports = function(content,map,meta){
        return someSyncOperation(content)
    }
    
    //返回多个参数时,要通过this.callback调用
    module.exports = function(content,map,meta){  this.callback(null,someSyncOperation(content),map,meta)
        return
    }    
  • 异步loader

    //对于异步loader,使用this.async来获取callback函数
    module.exports = function(content,map,meta){
      let callback = this.async();
     someAsyncOperation(content,function(err,result,sourceMap,meta){
        if(err) return callback(err);
        callback(null,result,sourceMap,meta);
      })
    }
    
    //promise写法
    module.exports = function(content){
      return new Promise(resolve =>{
        someAsyncOperation(content,(err,result)=>{
          if(err) resolve(err)
          resolve(null,result)
        })
      })
    }

了解了loader的写法后,我们在来看看loader的执行函数runSyncOrAsync

function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true;
    var isDone = false;
    var isError = false; // internal error
    var reportedError = false;
    context.async = function async() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("async(): The callback was already called.");
        }
        isSync = false;
        return innerCallback;
    };
    var innerCallback = context.callback = function() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("callback(): The callback was already called.");
        }
        isDone = true;
        isSync = false;
        try {
            callback.apply(null, arguments);
        } catch(e) {
            isError = true;
            throw e;
        }
    };
    try {
        var result = (function LOADER_EXECUTION() {
            return fn.apply(context, args);
        }());
        if(isSync) {
            isDone = true;
            if(result === undefined)
                return callback();
            if(result && typeof result === "object" && typeof result.then === "function") {
                return result.then(function(r) {
                    callback(null, r);
                }, callback);
            }
            return callback(null, result);
        }
    } catch(e) {
        if(isError) throw e;
        if(isDone) {
            // loader is already "done", so we cannot use the callback function
            // for better debugging we print the error on the console
            if(typeof e === "object" && e.stack) console.error(e.stack);
            else console.error(e);
            return;
        }
        isDone = true;
        reportedError = true;
        callback(e);
    }

}

结合loader执行的各种写法,runSyncOrAsync的逻辑就很清晰了。

我们知道,loader上的方法有pitch和normal之分,它们都是用runSyncOrAsync执行的,执行顺序为:

//config中
use: [
          'a-loader',
          'b-loader',
          'c-loader'
        ]

//执行顺序
|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

loader的pitch方法一般写法如下:

//data对象保存在loaderContext对象的data属性中,可以用于在循环时,捕获和共享前面的信息。
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 42;
};

//pitch中有返回值时,会跳过后续的pitch和内层的normal方法
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
  }
};

例如在style-loader和css-loader一起使用时,先执行的style-loader的pitch方法,返回值如下:

var content = require("!!../node_modules/css-loader/dist/cjs.js!./style.css");

if (typeof content === 'string') {
  content = [[module.id, content, '']];
}

var options = {}

options.insert = "head";
options.singleton = false;

var update = require("!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);

if (content.locals) {
  module.exports = content.locals;
}

由于有返回值,会跳过后续的style-loader的pitch方法、css-loader的pitch方法、css-loader的normal方法和css-loader的normal方法。然后在后续处理依赖时处理内联loader的时候再进行css-loader的处理。

loaders处理之后,得到处理后的文件的内容字符串保存在module的_source变量中,如何从这个字符串中得到依赖呢?这就需要对这个字符串进行处理了,在回调函数中this.parser.parse 方法被执行:

parse(source, initialState) {
        let ast;
        let comments;
        if (typeof source === "object" && source !== null) {
            ast = source;
            comments = source.comments;
        } else {
            comments = [];
            ast = Parser.parse(source, {
                sourceType: this.sourceType,
                onComment: comments
            });
        }

        const oldScope = this.scope;
        const oldState = this.state;
        const oldComments = this.comments;
        this.scope = {
            topLevelScope: true,
            inTry: false,
            inShorthand: false,
            isStrict: false,
            definitions: new StackedSetMap(),
            renames: new StackedSetMap()
        };
        const state = (this.state = initialState || {});
        this.comments = comments;
        if (this.hooks.program.call(ast, comments) === undefined) {
            this.detectStrictMode(ast.body);
            this.prewalkStatements(ast.body);
            this.blockPrewalkStatements(ast.body);
            this.walkStatements(ast.body);
        }
        this.scope = oldScope;
        this.state = oldState;
        this.comments = oldComments;
        return state;
    }

先调用Parse.parse方法得到AST,然后就是对这个树进行遍历了,流程为: program事件 -> detectStrictMode -> prewalkStatements -> walkStatements。这个过程会通过遍历AST的各个节点,从而触发不同的钩子函数,在这些钩子函数上会触发一些模块处理的方法(这些方法大多是在webpackOptionapply中注册到parser上的)给 module 增加很多 dependency 实例,每个 dependency 类都会有一个 template 方法,并且保存了原来代码中的字符位置 range,在最后生成打包后的文件时,会用 template 的结果替换 range 部分的内容。

所以最终得到的 dependency 不仅包含了文件中所有的依赖信息,还被用于最终生成打包代码时对原始内容的修改和替换,例如将 return 'sssss' + A替换为 return 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]

program 事件中,会触发两个 plugin 的回调:HarmonyDetectionParserPlugin 和 UseStrictPlugin

HarmonyDetectionParserPlugin中,如果代码中有 import 或者 export 或者类型为 javascript/esm,那么会增加了两个依赖:HarmonyCompatibilityDependency, HarmonyInitDependency 依赖。

UseStrictPlugin用来检测文件是否有 use strict,如果有,则增加一个 ConstDependency 依赖。

整个 parse 的过程关于依赖的部分,我们总结一下:

  1. 将 source 转为 AST(如果 source 是字符串类型)
  2. 遍历 AST,遇到 import 语句就增加相关依赖,代码中出现 A(import 导入的变量) 的地方也增加相关的依赖。

所有的依赖都被保存在 module.dependencies 中。module.dependencies大致内容如下:

0:CommonJsRequireDependency
  loc: SourceLocation
  end: Position {line: 1, column: 77}
  start: Position {line: 1, column: 14}
  __proto__: Object
  module: null
  optional: false
  range: (2) [22, 76]
  request: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
  userRequest: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
  weak: false
  type: (...)
1: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}
2: ConstDependency {module: null, weak: false, optional: false, loc: SourceLocation, expression: "module.i", …}
3: CommonJsRequireDependency {module: null, weak: false, optional: false, loc: SourceLocation, request: "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js", …}
4: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}

如图中所示,接下来就是处理依赖了,进入回调中的processModuleDependencies方法:

processModuleDependencies(module, callback) {
        const dependencies = new Map();    
    const addDependency = dep => {
        const resourceIdent = dep.getResourceIdentifier();
    // 过滤掉没有 ident 的,就是请求路径,例如 constDependency 这些只用在最后打包文件生成的依赖
        if (resourceIdent) {
            const factory = this.dependencyFactories.get(dep.constructor);
            if (factory === undefined) {
                throw new Error(
                    `No module factory available for dependency type: ${dep.constructor.name}`
                );
            }
            let innerMap = dependencies.get(factory);
            if (innerMap === undefined) {
                dependencies.set(factory, (innerMap = new Map()));
            }
            let list = innerMap.get(resourceIdent);
            if (list === undefined) innerMap.set(resourceIdent, (list = []));
            list.push(dep);
        }
    };

    const addDependenciesBlock = block => {
        if (block.dependencies) {
            iterationOfArrayCallback(block.dependencies, addDependency);
        }
        if (block.blocks) {
            iterationOfArrayCallback(block.blocks, addDependenciesBlock);
        }
        if (block.variables) {
            iterationBlockVariable(block.variables, addDependency);
        }
    };

    try {
        addDependenciesBlock(module);
    } catch (e) {
        callback(e);
    }

    const sortedDependencies = [];

    for (const pair1 of dependencies) {
        for (const pair2 of pair1[1]) {
            sortedDependencies.push({
                factory: pair1[0],
                dependencies: pair2[1]
            });
        }
    }

    this.addModuleDependencies(
        module,
        sortedDependencies,
        this.bail,
        null,
        true,
        callback
    );
}

接下来进入this.addModuleDependencies,在该函数中,递归进行之前的resolve=》buildMoudule过程直到所有的依赖处理完成,到此build过程就完成了。

详情参阅https://juejin.im/post/5cc51b...

4.3 compilation.seal

在上一步build完成后,build好的module保存在compilation._modules对象中,接下来需要根据这些modules生成chunks,并生成最后打包好的代码保存到compilation.assets中。

去除优化的钩子和一些支线剧情,seal方法可以简写如下:

seal(callback) {
        this.hooks.seal.call();
  
  // 初始化chunk、chunkGroups等
        for (const preparedEntrypoint of this._preparedEntrypoints) {
            const module = preparedEntrypoint.module;
            const name = preparedEntrypoint.name;
            const chunk = this.addChunk(name);
            const entrypoint = new Entrypoint(name);
            entrypoint.setRuntimeChunk(chunk);
            entrypoint.addOrigin(null, name, preparedEntrypoint.request);
            this.namedChunkGroups.set(name, entrypoint);
            this.entrypoints.set(name, entrypoint);
            this.chunkGroups.push(entrypoint);
    //在chunkGroups的chunk数组中推入chunk,在chunk的_groups Set中加入chunhGroups,建立两者联系
            GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
  //在module的_chunks Set中加入chunk,chunk的_modules Set中加入module
            GraphHelpers.connectChunkAndModule(chunk, module);

            chunk.entryModule = module;
            chunk.name = name;
      //给各个依赖的module按照引用层级加上depth属性,如入口为的depth为0
            this.assignDepth(module);
        }
  //生成module graph 和chunk graph
        buildChunkGraph(
            this,
            /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
        );
        this.sortModules(this.modules);
  

        this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
            this.hooks.beforeModuleIds.call(this.modules);
            this.hooks.moduleIds.call(this.modules);
            this.applyModuleIds();
        
            this.hooks.beforeChunkIds.call(this.chunks);
            this.applyChunkIds();
            
      
            this.hooks.beforeHash.call();
            this.createHash();
            this.hooks.afterHash.call();

            return this.hooks.afterSeal.callAsync(callback);
        });
    }

首先是在compilation对象上初始化chunk、chunkGroups等变量,利用GraphHelpers方法建立module和chunk,chunk和chunkGroups之间的关系,调用assignDepth方法给每个module加上依赖层级depth,接着进入buildChunkGraph生成chunk graph。

const buildChunkGraph = (compilation, inputChunkGroups) => {
    // SHARED STATE

    /** @type {Map<ChunkGroup, ChunkGroupDep[]>} */
    const chunkDependencies = new Map();

    /** @type {Set<ChunkGroup>} */
    const allCreatedChunkGroups = new Set();

    /** @type {Map<ChunkGroup, ChunkGroupInfo>} */
    const chunkGroupInfoMap = new Map();

    /** @type {Set<DependenciesBlock>} */
    const blocksWithNestedBlocks = new Set();

    // PART ONE

    visitModules(
        compilation,
        inputChunkGroups,
        chunkGroupInfoMap,
        chunkDependencies,
        blocksWithNestedBlocks,
        allCreatedChunkGroups
    );

    // PART TWO

    connectChunkGroups(
        blocksWithNestedBlocks,
        chunkDependencies,
        chunkGroupInfoMap
    );

    // Cleaup work

    cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
};

主要逻辑在visitModules方法中,首先通过const blockInfoMap = extraceBlockInfoMap(compilation)得到module graph,module是一个Map,键名是各个module,键值是module的依赖,分为异步加载的依赖blocks和同步依赖modules:

0: {NormalModule => Object}
    key: NormalModule {dependencies: Array(4), blocks: Array(1), variables: Array(0), type: "javascript/auto", context: "/Users/hahaha/project/demo/webpack-demo/src", …}
    value:
      blocks: [ImportDependenciesBlock]
      modules: Set(1) {NormalModule}
1: {ImportDependenciesBlock => Object}
2: {NormalModule => Object}
3: {NormalModule => Object}
4: {NormalModule => Object}
5: {NormalModule => Object}
6: {NormalModule => Object}

然后利用两层循环将栈内的模块及其依赖一层层的加入到chunk的this._modules对象中,同步依赖放在内层循环处理,异步依赖放在外层循环处理。(利用栈处理递归依赖以及利用swtich进行流程管理)

接下来connectChunkGroupscleanupUnconnectedGroups,遍历 chunk graph,通过和依赖的 module 之间的使用关系来建立起不同 chunkGroup 之间的父子关系,同时剔除一些没有建立起联系的 chunk,没细看

详情:webpack系列之六chunk图生成

接下来就是生成module id和chunk id了,之前好像是生成的数字id,现在好像在NamedModulesPlugin和NamedChunksPlugin插件中将id命名成文件名了。

his.createHash方法中生成hash,包括本次编译的hash、chunkhash、modulehash。hash的生成步骤基本如下,首先create得到moduleHash方法,再在updateHash方法中不断的加各种内容,例如modulehash生成过程中就用到了module id、各种依赖、export信息等,最后调用digest方法生成hash:

const moduleHash = createHash(hashFunction);
            module.updateHash(moduleHash);
            module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
            module.renderedHash = module.hash.substr(0, hashDigestLength);

chunkhash生成过程中会用到chunk id、module id、name、template信息等。

最后就是调用

createChunkAssets() {
        const outputOptions = this.outputOptions;
        const cachedSourceMap = new Map();
        const alreadyWrittenFiles = new Map();
  //遍历chunks数组
        for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i];
            chunk.files = [];
            let source;
            let file;
            let filenameTemplate;
            try {
        //入口模块就是hasRuntime,相对于普通模块,加了一层webpack runtime bootstrap 自执行函数包裹
                const template = chunk.hasRuntime()
                    ? this.mainTemplate
                    : this.chunkTemplate;
        //在该函数内会触发相应template.hooks.renderManifest钩子,在webpackoptionapply中注册的javaScriptModulesPlugin(一般是这个)中执行逻辑,在返回结果中推入render方法。
                const manifest = template.getRenderManifest({
                    chunk,
                    hash: this.hash,
                    fullHash: this.fullHash,
                    outputOptions,
                    moduleTemplates: this.moduleTemplates,
                    dependencyTemplates: this.dependencyTemplates
                }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
                for (const fileManifest of manifest) {
          //缓存处理
          ........
          //调用上一步得到的render方法
                        source = fileManifest.render();    
                    }
                    this.assets[file] = source;
                    chunk.files.push(file);
                    this.hooks.chunkAsset.call(chunk, file);
                    alreadyWrittenFiles.set(file, {
                        hash: usedHash,
                        source,
                        chunk
                    });
                }
            }
        }
    }
  

当为chunkTemplate时,javaScriptModulesPlugin中的render方法:

  renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
    //获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码
        const moduleSources = Template.renderChunkModules(
            chunk,
            m => typeof m.source === "function",
            moduleTemplate,
            dependencyTemplates
        );
    //最终生成 chunk 代码前对 chunk 最修改
        const core = chunkTemplate.hooks.modules.call(
            moduleSources,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
    //外层添加包裹函数
        let source = chunkTemplate.hooks.render.call(
            core,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
        if (chunk.hasEntryModule()) {
            source = chunkTemplate.hooks.renderWithEntry.call(source, chunk);
        }
        chunk.rendered = true;
        return new ConcatSource(source, ";");
    }

moduleSources示例:

{
,
/***/ "./src/foo.js":
,/*!********************!*\
,  !*** ./src/foo.js ***!
,  \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n
/* harmony default export */
__webpack_exports__[\"default\"] = 
                     (function(){\n  
                     console.log('here are foo')\n
                         }
                     );
\n\n\n//# sourceURL=webpack:///./src/foo.js?");",
/***/ 
})

最后得到source的示例:

"(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],,{
,
/***/ "./src/foo.js":
,/*!********************!*\
,  !*** ./src/foo.js ***!
,  \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n    console.log('here are foo')\n});\n\n\n//# sourceURL=webpack:///./src/foo.js?");",

/***/ }),

},])"

当为mainTemplate时,调用的是mainTemplate中的render方法如下:

render(hash, chunk, moduleTemplate, dependencyTemplates) {
        const buf = this.renderBootstrap(
            hash,
            chunk,
            moduleTemplate,
            dependencyTemplates
        );
        let source = this.hooks.render.call(
            new OriginalSource(
                Template.prefix(buf, " \t") + "\n",
                "webpack/bootstrap"
            ),
            chunk,
            hash,
            moduleTemplate,
            dependencyTemplates
        );
        if (chunk.hasEntryModule()) {
            source = this.hooks.renderWithEntry.call(source, chunk, hash);
        }
        if (!source) {
            throw new Error(
                "Compiler error: MainTemplate plugin 'render' should return something"
            );
        }
        chunk.rendered = true;
        return new ConcatSource(source, ";");
    }

得到的Bootstrap如下:

// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(data);

    while(resolves.length) {
        resolves.shift()();
    }

};
,
// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
    "main": 0
};



// script path function
function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle." + "eecd41ca7ca8f56e3293" + ".js"
},,// The require function,function __webpack_require__(moduleId) {,
    // Check if module is in cache
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;,},,// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');
            var onScriptComplete;

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);

            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) {
                    if(chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
};

// define __esModule on exports
__webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
};

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    return ns;
};

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
        function getDefault() { return module['default']; } :
        function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
};

// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// __webpack_public_path__
__webpack_require__.p = "";

// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };,,var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

,// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");"

得到的source如下:

"/******/ (function(modules) { // webpackBootstrap
,[object Object],/******/ })
,/************************************************************************/
,/******/ (,{
,
/***/ "./node_modules/css-loader/dist/cjs.js!./src/style.css":
,/*!*************************************************************!*\
,  !*** ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
,  \*************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,[object Object],

/***/ }),,
,
/***/ "./node_modules/css-loader/dist/runtime/api.js":
,/*!*****************************************************!*\
,  !*** ./node_modules/css-loader/dist/runtime/api.js ***!
,  \*****************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
,/*!****************************************************************************!*\
,  !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
,  \****************************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./src/index.js":
,/*!**********************!*\
,  !*** ./src/index.js ***!
,  \**********************/
,/*! no exports provided */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {

,"use strict";
,[object Object],

/***/ }),,
,
/***/ "./src/style.css":
,/*!***********************!*\
,  !*** ./src/style.css ***!
,  \***********************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {

,[object Object],

/***/ }),

/******/ },)

5. compiler.emitAssets

经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation 的 assets 中,当 assets 资源相关的优化工作结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,callback回溯到compiler.run中,执行compiler.emitAssets.

在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始创建目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件

参考资料:

https://juejin.im/post/5d4d08...


karl
78 声望5 粉丝