zoukankan      html  css  js  c++  java
  • .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 => "
    //# source" + "MappingURL=[url]"
        comment = legacy && modern ? "
    /*
    //@ source" + "MappingURL=[url]
    //# source" + "MappingURL=[url]
    */" :
            legacy ? "
    /*
    //@ source" + "MappingURL=[url]
    */" :
            modern ? "
    //# 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中。

      总结如图:

      完结!

  • 相关阅读:
    Modbus软件开发实战指南 之 开发自己的Modbus Poll工具
    Divide Two Integers-不用'/' '*' '%'操作实现整数的除法
    用最少的砝码称出1到100克的物品
    Binary Tree Inorder Traversal-非递归实现中序遍历二叉树
    leetcode Word Break-单词划分
    位运算题目
    leetcode Single Number II
    leetcode 4Sum
    leetcode 3Sum Closest
    Unique Binary Search Trees-计算表示相同序列的不同BST个数
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8081069.html
Copyright © 2011-2022 走看看