webpack源碼閱讀之主流程分析

webpack源碼閱讀之主流程分析

圖片描述

comipler是其webpack的支柱模塊,其繼承於Tapable類,在compiler上定義了不少鉤子函數,貫穿其整個編譯流程,這些鉤子上註冊了不少插件,用於在特定的時機執行特定的操做,同時,用戶也能夠在這些鉤子上註冊自定義的插件來進行功能拓展,接下來將圍繞這些鉤子函數來分析webpack的主流程。javascript

1. compiler實例化

compiler對象的生成過程大體能夠簡化爲以下過程,首先對咱們傳入的配置進行格式驗證,接着調用Compiler構造函數生成compiler實例,自定義的plugins註冊,最後調用new WebpackOptionsApply().process(options, compiler)進行默認插件的註冊,comailer初始化等。css

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)的鉤子上:java

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鉤子。node

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模式時,能夠進行屢次編譯。webpack

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方法開始編譯過程。git

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對源文件的處理,遞歸的進行依賴處理等等.github

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獲得入口文件的具體路徑。web

4.1 resolve

webpack 中每涉及到一個文件,就會通過 resolve 的過程。webpack 使用 enhanced-resolve 來提供絕對路徑、相對路徑、模塊路徑的多樣解析方式。express

圖片描述

moduleFactory.create()resolve過程從經過調用normalModuleFactory中factory函數開始。json

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...

相關文章
相關標籤/搜索