.15-淺析webpack源碼之WebpackOptionsApply模塊-plugin事件流總覽


  總體過了一下后面的流程,發現Compiler模塊確實不適合單獨講解,這里繼續講解后面的代碼:

compiler.options = new WebpackOptionsApply().process(options, compiler);

  這行代碼與之前設置options默認值非常相似,但是復雜程度根本不是一個次元的。

  這一節只能簡單的看一眼內部到底有多少東西,整理后源碼如下:

"use strict";

const OptionsApply = require("./OptionsApply");
// ...巨量插件引入

class WebpackOptionsApply extends OptionsApply {
    constructor() {
        super();
    }
    process(options, compiler) {
        let ExternalsPlugin;
        compiler.outputPath = options.output.path;
        compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
        compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath;
        compiler.name = options.name;
        compiler.dependencies = options.dependencies;
        // 在默認參數配置中被設置為web
        if (typeof options.target === "string") {
            let JsonpTemplatePlugin;
            let NodeSourcePlugin;
            let NodeTargetPlugin;
            let NodeTemplatePlugin;
            switch (options.target) {
                case "web":
                    JsonpTemplatePlugin = require("./JsonpTemplatePlugin");
                    NodeSourcePlugin = require("./node/NodeSourcePlugin");
                    compiler.apply(
                        new JsonpTemplatePlugin(options.output),
                        new FunctionModulePlugin(options.output),
                        new NodeSourcePlugin(options.node),
                        new LoaderTargetPlugin(options.target)
                    );
                    break;
                    // other case...
                default:
                    throw new Error("Unsupported target '" + options.target + "'.");
            }
        } else if (options.target !== false) {
            options.target(compiler);
        } else {
            throw new Error("Unsupported target '" + options.target + "'.");
        }
        // options.output.library參數處理
        if (options.output.library || options.output.libraryTarget !== "var") { /**/ }
        // options.output.externals參數處理
        if (options.externals) { /**/ }
        let noSources;
        let legacy;
        let modern;
        let comment;
        // options.devtool => sourcemap || source-map
        if (options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) { /**/ }
        // options.devtool => eval
        else if (options.devtool && options.devtool.indexOf("eval") >= 0) { /**/ }
        // 加載模塊並觸發entry-option事件流
        compiler.apply(new EntryOptionPlugin());
        compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
        // 瘋狂加載插件
        compiler.apply( /**/ );
        // options.performance參數處理
        if (options.performance) { /**/ }

        // 繼續加載插件
        compiler.apply(new TemplatedPathPlugin());
        compiler.apply(new RecordIdsPlugin());
        compiler.apply(new WarnCaseSensitiveModulesPlugin());
        // options.performance參數處理
        if (options.cache) { /**/ }
        // 觸發after-plugins
        compiler.applyPlugins("after-plugins", compiler);
        if (!compiler.inputFileSystem) throw new Error("No input filesystem provided");
        // 給compiler.resolvers設置值
        compiler.resolvers.normal = ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem
        }, options.resolve));
        compiler.resolvers.context = ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem,
            resolveToContext: true
        }, options.resolve));
        compiler.resolvers.loader = ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem
        }, options.resolveLoader));
        // 觸發after-resolvers事件流
        compiler.applyPlugins("after-resolvers", compiler);
        return options;
    }
}

module.exports = WebpackOptionsApply;

  這個模塊除去父類引入,其余插件光頂部引入就有34個,簡直就是插件之王。

  略去具體插件內容,先看流程,父類其實是個接口,啥都沒有:

"use strict";

class OptionsApply {
    process(options, compiler) {}
}
module.exports = OptionsApply;

  接下來是一個唯一的主方法process,總結下流程依次為:

1、根據options.target加載對應的插件,如果配置文件沒有配置該參數,則在WebpackOptionsDefaulter模塊會被自動初始化為web。

2、處理options.output.library、options.output.externals參數

3、處理options.devtool參數

4、加載EntryOptionPlugin插件並觸發entry-option的事件流

5、加載大量插件

6、處理options.performance參數

7、加載TemplatePathPlugin、RecordIdPlugin、WarnCaseSensitiveModulesPlugin插件

8、觸發after-plugins事件流

9、設置compiler.resolvers的值

10、觸發after-resolvers事件流

  如果按類型分,其實只有兩種:加載插件,觸發事件流。

  事件流的觸發類似於vue源碼里的鈎子函數,到特定的階段觸發對應的方法,這個思想在Java的數據結構源碼中也被普通應用。

  模塊中的參數處理如果該參數比較常用,那么就進行分析,其余不太常用的就先跳過,按順序依次講解。

  這里的options經過默認參數模塊的加工,豐富后如下:

{
    "entry": "./input.js",
    "output": {
        "filename": "output.js",
        "chunkFilename": "[id].output.js",
        "library": "",
        "hotUpdateFunction": "webpackHotUpdate",
        "jsonpFunction": "webpackJsonp",
        "libraryTarget": "var",
        "path": "D:\\workspace\\doc",
        "sourceMapFilename": "[file].map[query]",
        "hotUpdateChunkFilename": "[id].[hash].hot-update.js",
        "hotUpdateMainFilename": "[hash].hot-update.json",
        "crossOriginLoading": false,
        "chunkLoadTimeout": 120000,
        "hashFunction": "md5",
        "hashDigest": "hex",
        "hashDigestLength": 20,
        "devtoolLineToLine": false,
        "strictModuleExceptionHandling": false
    },
    "context": "D:\\workspace\\doc",
    "devtool": false,
    "cache": true,
    "target": "web",
    "module": {
        "unknownContextRequest": ".",
        "unknownContextRegExp": false,
        "unknownContextRecursive": true,
        "unknownContextCritical": true,
        "exprContextRequest": ".",
        "exprContextRegExp": false,
        "exprContextRecursive": true,
        "exprContextCritical": true,
        "wrappedContextRegExp": {},
        "wrappedContextRecursive": true,
        "wrappedContextCritical": false,
        "strictExportPresence": false,
        "strictThisContextOnImports": false,
        "unsafeCache": true
    },
    "node": {
        "console": false,
        "process": true,
        "global": true,
        "Buffer": true,
        "setImmediate": true,
        "__filename": "mock",
        "__dirname": "mock"
    },
    "performance": {
        "maxAssetSize": 250000,
        "maxEntrypointSize": 250000,
        "hints": false
    },
    "resolve": {
        "unsafeCache": true,
        "modules": ["node_modules"],
        "extensions": [".js", ".json"],
        "mainFiles": ["index"],
        "aliasFields": ["browser"],
        "mainFields": ["browser", "module", "main"],
        "cacheWithContext": false
    },
    "resolveLoader": {
        "unsafeCache": true,
        "mainFields": ["loader", "main"],
        "extensions": [".js", ".json"],
        "mainFiles": ["index"],
        "cacheWithContext": false
    }
}

  除去entry與output.filename,其余的參數全部是填充上去的,因為后面的流程會檢測參數,所以這里先列出來。

 

  這一節先這樣,具體內容后面進行詳細講解。

  發現這節沒啥營養,填充下內容,將事件流的plugin總覽一下,先不做深入分析,在編譯運行階段在做講解。

  依次看每一段代碼注入了哪些事件流,絕不深究。

 

options.target參數

  這個一般不會去設置,默認會被置為web,源碼中進入下列case:

switch (options.target) {
    case "web":
        JsonpTemplatePlugin = require("./JsonpTemplatePlugin");
        NodeSourcePlugin = require("./node/NodeSourcePlugin");
        compiler.apply(
            new JsonpTemplatePlugin(options.output),
            new FunctionModulePlugin(options.output),
            new NodeSourcePlugin(options.node),
            new LoaderTargetPlugin(options.target)
        );
        break;
}

  這里的apply我在剛開始有個誤區,以為是每個函數必帶的apply,后來其實調用的是父類tapable的apply,而該函數源碼為:

Tapable.prototype.apply = function apply(...fns) {
    // 遍歷所有參數並執行
    for (var i = 0; i < fns.length; i++) {
        fns[i].apply(this);
    }
};

  又是個apply,這樣意思就明白了,compiler.apply是調用每一個函數的apply方法。

JsonpTemplatePlugin插件 => this-compilation
class JsonpTemplatePlugin {
    apply(compiler) {
        compiler.plugin("this-compilation", (compilation) => { /**/ });
    }
}

  若自定義插件未plugin該事件流,此次為第一次this-compilation。

FunctionModulePlugin插件 => compilation
class FunctionModulePlugin {
    constructor(options, requestShortener) { /**/ }
    apply(compiler) {
        compiler.plugin("compilation", (compilation) => { /**/ });
    }
}

  第一次compilation。

NodeSourcePlugin插件 => compilation、after-resolvers

LoaderTargetPlugin插件 => compilation

 

options.output.library參數、options.externals參數

  這兩個參數這里不做講解。

 

options.devtool參數

  在vue-cli構建的腳手架中,開發者模式該參數為'eval-source-map',在生產模式下則為'#source-map'。

  這里需要看一下源碼是如何解析這個字符串:

if (options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) {
    // 用indexof判斷參數的真假值
    // inline,hidden,cheap,moduleMaps,nosources
    const evalWrapped = options.devtool.indexOf("eval") >= 0;
    legacy = options.devtool.indexOf("@") >= 0;
    modern = options.devtool.indexOf("#") >= 0;
    // eval-source-map => null
    // #source-map => "\n//# source" + "MappingURL=[url]"
    comment = legacy && modern ? "\n/*\n//@ source" + "MappingURL=[url]\n//# source" + "MappingURL=[url]\n*/" :
        legacy ? "\n/*\n//@ source" + "MappingURL=[url]\n*/" :
        modern ? "\n//# source" + "MappingURL=[url]" :
        null;
    let Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin;
    compiler.apply(new Plugin({
        filename: inline ? null : options.output.sourceMapFilename,
        moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
        fallbackModuleFilenameTemplate: options.output.devtoolFallbackModuleFilenameTemplate,
        append: hidden ? false : comment,
        module: moduleMaps ? true : cheap ? false : true,
        columns: cheap ? false : true,
        lineToLine: options.output.devtoolLineToLine,
        noSources: noSources,
    }))
};

  完全沒有格式可言,全部通過indexOf判斷標記參數的真假值,所以說eval-source-map跟eeeeeeevallllll-source-map是一樣的。

  兩種情況下,加載的插件一樣,但是append參數不一樣,如下:

EvalSourceMapDevToolPlugin插件 => compilation

class EvalSourceMapDevToolPlugin {
    constructor(options) { /**/ }
    apply(compiler) {
        const options = this.options;
        compiler.plugin("compilation", (compilation) => { /**/ });
    }
}

 

EntryOptionPlugin插件 => entry-option

module.exports = class EntryOptionPlugin {
    apply(compiler) {
        compiler.plugin("entry-option", (context, entry) => { /**/ });
    }
};

  在注入事件流后,這里會立即進行調用,代碼如下:

compiler.applyPluginsBailResult("entry-option", options.context, options.entry);

 

  接下來是海量插件引入,不寫垃圾代碼了,直接看看源碼就行:

compiler.apply(
    new CompatibilityPlugin(),
    new HarmonyModulesPlugin(options.module),
    new AMDPlugin(options.module, options.amd || {}),
    new CommonJsPlugin(options.module),
    new LoaderPlugin(),
    new NodeStuffPlugin(options.node),
    new RequireJsStuffPlugin(),
    new APIPlugin(),
    new ConstPlugin(),
    new UseStrictPlugin(),
    new RequireIncludePlugin(),
    new RequireEnsurePlugin(),
    new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles),
    new ImportPlugin(options.module),
    new SystemPlugin(options.module)
);

compiler.apply(
    new EnsureChunkConditionsPlugin(),
    new RemoveParentModulesPlugin(),
    new RemoveEmptyChunksPlugin(),
    new MergeDuplicateChunksPlugin(),
    new FlagIncludedChunksPlugin(),
    new OccurrenceOrderPlugin(true),
    new FlagDependencyExportsPlugin(),
    new FlagDependencyUsagePlugin()
);

  簡直可怕。

 

options.performance參數

  暫不講解。

 

  然后是三個插件的加載:

TemplatedPathPlugin、RecordIdsPlugin、WarnCaseSensitiveModulesPlugin => compilation

 

optison.cache參數

  該參數會被默認設置為true,所以該插件是被默認加載。

class CachePlugin {
    constructor(cache) {
        this.cache = cache || {};
        this.FS_ACCURENCY = 2000;
    }
    apply(compiler) {
        if (Array.isArray(compiler.compilers)) {
            compiler.compilers.forEach((c, idx) => {
                c.apply(new CachePlugin(this.cache[idx] = this.cache[idx] || {}));
            });
        } else {
            const registerCacheToCompiler = (compiler, cache) => {
                compiler.plugin("this-compilation", compilation => { /**/ });
            };
            registerCacheToCompiler(compiler, this.cache);
            compiler.plugin("watch-run", (compiler, callback) => { /**/ });
            compiler.plugin("run", (compiler, callback) => { /**/ });
            compiler.plugin("after-compile", function(compilation, callback) { /**/ });
        }
    }
}

  這個插件比較麻煩,依次注入了this-compilation、watch-run、run、after-compile事件流。

 

  在所有插件就加載完畢后,會執行after-plugins事件流並給compiler.resolvers賦值,然后執行after-resolvers事件流。

  這樣,所有的插件都打包到了compiler當中,其中大部分的事件流都集中在了compilation中。

  總結如圖:

 

  完結!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM