1. 程式人生 > >.7-淺析webpack源碼之WebpackOptionsDefaulter模塊

.7-淺析webpack源碼之WebpackOptionsDefaulter模塊

template 靈活 全部 str ati 避免 替換 not 流程圖

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配置對象添加了大量的默認參數。

完事~

.7-淺析webpack源碼之WebpackOptionsDefaulter模塊