• .7-浅析webpack源码之WebpackOptionsDefaulter模块


    WebpackOptionsDefaulter模块

      通过参数检测后,会根据单/多配置进行处理,本文基于单配置,所以会进行到如下代码:

    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        // TODO webpack 4: process returns options
        // 这里的处理有部分是为了webpack4.0做准备
        new WebpackOptionsDefaulter().process(options);
        // ...
    }

      模块的作用是进行默认值的设置,流程图如下:

      

      进入该模块:

    "use strict";
    
    const OptionsDefaulter = require("./OptionsDefaulter");
    const Template = require("./Template");
    
    class WebpackOptionsDefaulter extends OptionsDefaulter {
        constructor() {
            super();
            this.set("devtool", false);
            // 大量的this.set...
        }
    }
    module.exports = WebpackOptionsDefaulter;

      可以看到,这个模块的内容是用ES6的新语法写的,很好理解,因为这个模块是只是针对webpack的默认设置,所以主要功能内容应该都在原型上面,直接进入OptionsDefaulter模块:

    "use strict";
    
    function getProperty(obj, name) { /**/ }
    
    function setProperty(obj, name, value) { /**/ }
    
    class OptionsDefaulter {
        constructor() {
            this.defaults = {};
            this.config = {};
        }
    
        process(options) {
            // TODO: change this for webpack 4: options = Object.assign({}, options);
            for (let name in this.defaults) {
                switch (this.config[name]) {
                    // case...
                }
            }
        }
    
        set(name, config, def) {
            if (arguments.length === 3) {
                this.defaults[name] = def;
                this.config[name] = config;
            } else {
                this.defaults[name] = config;
                delete this.config[name];
            }
        }
    }
    module.exports = OptionsDefaulter;

      这个模块也是用ES6写的,内容比较简单,主要内容如下:

    1、构造函数定义两个对象defaults,config

    2、两个原型方法process,set

    3、两个工具方法getProperty,setProperty

      构造函数创建了两个对象,defaults对象保存了参数的默认值,而config对象保存了某些参数的特殊处理方式。

      由于原型方法依赖于工具方法,所以从工具方法开始讲解:

    getProperty

    // obj就是options
    function getProperty(obj, name) {
        // 切割name
        name = name.split(".");
        // 注意这里是length-1
        for (let i = 0; i < name.length - 1; i++) {
            // 层层赋值
            obj = obj[name[i]];
            // 若obj非对象返回直接返回undefined
            if (typeof obj !== "object" || !obj) return;
        }
        // 返回最后一层的值
        return obj[name.pop()];
    }

      这个函数有意思,直接看比较懵逼,需要来个案例:

    // obj => {entry:'./inpuit.js',output:{filename:'output.js'}}
    // name => output.filename
    function getProperty(obj, name) {
        // 切割后得到[output,filename]
        name = name.split(".");
        // 第二次跳出循环
        for (let i = 0; i < name.length - 1; i++) {
            // obj => {filename:'output.js'}
            obj = obj[name[i]];
            // 返回了对象这里就不返回了
            if (typeof obj !== "object" || !obj) return;
        }
        // 注意这里obj是options.output
        // 所以返回的是options.output.filename
        return obj[name.pop()];
    }

      可以看出,这个函数是尝试获取对象的某个键,键的递进用点来连接,如果获取失败返回undefined。

      精妙的函数!避免了多次判断 obj[key] 是否为undefined,直接用字符串的方式解决了此问题。

    setProperty

    function setProperty(obj, name, value) {
        name = name.split(".");
        for (let i = 0; i < name.length - 1; i++) {
            // 非对象直接返回undefined
            if (typeof obj[name[i]] !== "object" && typeof obj[name[i]] !== "undefined") return;
            // 设置为对象
            if (!obj[name[i]]) obj[name[i]] = {};
            obj = obj[name[i]];
        }
        // 设置对应的值
        obj[name.pop()] = value;
    }

      有了前面的getProperty,这个就比较好懂了。

      下面就是原型方法:

    set

        set(name, config, def) {
            if (arguments.length === 3) {
                this.defaults[name] = def;
                this.config[name] = config;
            }else {
                this.defaults[name] = config;
                delete this.config[name];
            }
        }

      没什么好讲的,根据传参数量对构造函数生成的对象进行复制。

      至于process方法会在后面调用,所以这里暂时不讲,回到WebpackOptionsDefaulter中的大量set,抽取两个情况的进行讲解:

    this.set("output.chunkFilename", "make", (options) => {
        const filename = options.output.filename;
        return filename.indexOf("[name]") >= 0 ? filename.replace("[name]", "[id]") : "[id]." + filename;
    });
    this.set("resolve.extensions", [".js", ".json"]);

      调用这两个方法后,defaults与config对象如下:

    defaults = {
        "resolve.extensions": [".js", ".json"],
        "output.chunkFilename": (options) => {
            const filename = options.output.filename;
            return filename.indexOf("[name]") >= 0 ? filename.replace("[name]", "[id]") : "[id]." + filename;
        })
    }
    config = {
        "output.chunkFilename": "make"
    }

      函数中,由于output.filename是必传参数,所以能取到值。

      chunkFilename的函数会对字符串中的 [name] 置换成 [id] ,如果没有就加上 [id.] 前缀。

      例如多输出中经常会取 [name].js 作为输出文件名,在这里chunkFilename的默认值就是 [id].js 。

      大量大量的set后,可以来看看process函数了,为方便展示,写成function形式:

    // 传进来的options
    function process(options) {
        // 遍历defaults对象
        for (let name in this.defaults) {
            // 匹配config参数
            switch (this.config[name]) {
                // 默认情况直接进行赋值
                case undefined:
                    if (getProperty(options, name) === undefined)
                        setProperty(options, name, this.defaults[name]);
                    break;
                    // 用来保证根键为对象
                case "call":
                    setProperty(options, name, this.defaults[name].call(this, getProperty(options, name), options), options);
                    break;
                    // 默认值通过调用函数注入
                    // 传入一个options参数
                case "make":
                    if (getProperty(options, name) === undefined)
                        setProperty(options, name, this.defaults[name].call(this, options), options);
                    break;
                    // 将默认值添加进已有的数组中
                case "append":
                    {
                        let oldValue = getProperty(options, name);
                        if (!Array.isArray(oldValue)) oldValue = [];
                        oldValue.push.apply(oldValue, this.defaults[name]);
                        setProperty(options, name, oldValue);
                        break;
                    }
                default:
                    throw new Error("OptionsDefaulter cannot process " + this.config[name]);
            }
        }
    }

      根据config的情况有4种默认值注入方式,其中函数调用方式可以更加灵活的进行配置。

      为求完整,在某些set中有调用 Template.toIdentifier 方法,看一眼其内部实现:

    // 匹配所有非大小写字母$_
    const IDENTIFIER_NAME_REPLACE_REGEX = /^[^a-zA-Z$_]/;
    // 匹配所有非大小写字母数字$_
    const IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX = /[^a-zA-Z0-9$_]/g;
    module.exports = class Template extends Tapable {
        constructor(outputOptions) { /**/ }
    
        //静态方法 直接调用
        static toIdentifier(str) {
            if (typeof str !== "string") return "";
            // 特殊符号全部置换为_
            return str.replace(IDENTIFIER_NAME_REPLACE_REGEX, "_").replace(IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX, "_");
        }
    
        // 其余方法...
    }

      只是一个普通的字符替换函数而已。

      一句话总结:WebpackOptionsDefaulter模块对options配置对象添加了大量的默认参数。

    完事~

  • 相关阅读:
    HTML基础
    一次由任意文件漏洞开始的渗透测试过程
    谈一谈信息泄露这件事
    浅谈任意文件下载漏洞的利用
    [原创] 一次渗透测试过程--从外网进内网
    一次绕过360+诺顿的提权过程
    漏洞挖掘高级方法
    新型勒索软件Magniber正瞄准韩国、亚太地区开展攻击
    Microsoft Edge 浏览器远程代码执行漏洞POC及细节(CVE-2017-8641)
    Office远程代码执行漏洞CVE-2017-0199复现
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8034891.html
Copyright © 2020-2023  润新知