webpack Parser對JS表達式語句的解析計算

上一篇文章梳理了webapck Parser解析模塊的流程,根據解析模塊過程當中Parser針對不一樣語句即表達式解析拋出的事件,咱們能夠自定義地爲模塊添加依賴,從而經過依賴來完成相應的功能。本文將繼續從Parser表達式的解析和計算來理解Parser,並配合webpack DefinePlugin定義全局變量的插件來加以分析。javascript

現象

不知道你們是否注意到這樣的狀況,在某些狀況下webpack會直接將咱們代碼中值固定的表達式直接使用最終的結果值來替換。例如webpack中 定義全局變量的DefinePlugin插件,會直接將咱們定義的全局變量給替換爲配置的值,例以下面方式定義的全局變量:java

new webpack.DefinePlugin({
    "process.env": {
        "NODE_ENV": JSON.stringify('development')
    }
})
複製代碼

那麼咱們在代碼中使用到process.env.NODE_ENV表達式時,例如:webpack

if (process.env.NODE_ENV === 'development') {...}
複製代碼

細心的同窗可能會發現,查看webpack最終構建生成的代碼,會發現該表達式被其對應的字符串值development所替換:git

if ('development' === 'development') {...}
複製代碼

不只如此,像process.env.NODE_ENV這種對象成員調用形式的全局變量,根據js的規則,那麼他們的對象也是能夠訪問的,webpack經過DefinePlugin插件對這種形式也進行了處理。github

var obj = process.env 
// 它會被轉換爲 var obj = Object({NODE_ENV: "development"})
var _type = typeof process.env 
// 它會轉換爲 var _type = "object"
複製代碼

DefinePlugin是怎麼實現的呢,這就是要涉及到Parser對模塊的遍歷計算,配合依賴最終完成替換的。下面就來一塊兒看看Parser是若是對JS表達式進行計算的web

表達式的解析與計算

上一篇文章提到,Parser是經過ast來遍歷解析模塊的,分爲 當前做用域定義變量標識符的收集 以及 ast語句的解析;其中語句解析是經過walkStatement方法完成的,這一過程包括兩個部分:express

  • 表達式的遍歷解析小程序

  • 計算表達式的值api

下面來詳細介紹下這個過程數組

表達式的遍歷解析

ast語句主要包括聲明語句或者表達式組成,例如在astexplorer.net官網在線寫了一段代碼轉換後的狀況以下圖所示:

Parser利用walkStatement對語句進行解析,對語句遍歷解析的過程最終會轉移到對錶達式的解析,這在Parser中最終體如今walkExpression方法上,它負責對組成語句的不一樣表達式進行遍歷,這些表達式以下圖:

能夠看出,幾乎ast的絕大部分表達式都已覆蓋。

那麼Parser在針對每種表達式是如何解析的呢,所謂解析也就是表達式的遍歷,即:

Parser會對錶達式的每一部分分別進行遍歷,直至標識符Identifier或者字面量Literal的程度。

遍歷的結果是針對不一樣的表達式內容對外拋出相應的鉤子函數,用戶能夠註冊這些鉤子從而完成自定義的解析過程。咱們如下面的例子來講明Parser是如何對三元運算符表達式ConditionalExpression進行解析的。

current.env === 'development' ? a() : b
複製代碼

該三元運算符對應的ast表達式內容以下圖:

首先,walkExpression針對賦值表達式ConditionalExpression的解析是經過walkConditionalExpression方法來完成的,來看看看該方法的實現:

walkConditionalExpression(expression) {
    const result = this.hooks.expressionConditionalOperator.call(expression);
    // 對三元運算符進行優化,根據expression.test的結果來決定是否texpression.alternate和expression.alternate的解析
    if (result === undefined) { // 什麼都沒有返回,須要三個部分都遍歷
        this.walkExpression(expression.test);
        this.walkExpression(expression.consequent);
        if (expression.alternate) {
            this.walkExpression(expression.alternate);
        }
    } else {
        if (result) { // 條件爲true,只解析expression.consequent
            this.walkExpression(expression.consequent);
        } else if (expression.alternate) { // 條件爲false,只解析expression.alternate
            this.walkExpression(expression.alternate);
        }
    }
}
複製代碼

其中,解析三元運算符表達式時,Parser會向執行expressionConditionalOperator鉤子,其返回值做爲三元運算符條件表達式的計算結果值,例如上面例子中的current.env === 'development',若其返回true,則解析表達式的expression.consequent部分,若返回false則解析表達式的expression.alternate部分。默認狀況下,webpack內部的ConstPlugin插件註冊了expressionConditionalOperator鉤子,它會計算條件表達式的值來做爲返回結果。

下面以expressionConditionalOperator鉤子回調返回結果爲undefined爲假設,那麼須要對三元運算符的三個部分分別加以遍歷解析,從上圖中能夠看到它們分別是一元運算符表達式BinaryExpression、調用表達式CallExpression和標識符Identifier,那麼Parser會依次對這三個部分調用walkExpression繼續解析,它會分別解析對應的表達式,作了一個流程圖便於理解:

上面例子中的這一解析遍歷ConditionalExpression表達式的過程當中,對外拋出了以下鉤子函數:

  • walkConditionalExpression:該方法拋出expressionConditionalOperator鉤子

  • walkMemberExpression:該方法可觸發expressionexpressionAnyMember鉤子

  • walkCallExpression:該方法可觸發callcallAnyMember鉤子

  • walkIdentifier:該方法可觸發expression鉤子

  • 計算Identifier值:其可觸發evaluateIdentifierevaluateDefinedIdentifier鉤子

具體的鉤子是幹什麼用的,能夠參考webpack Parser官網。用戶對本身感興趣的解析能夠自定義解析行爲,例如上面的expressionConditionalOperator鉤子。

計算表達式的值

上面介紹的是從ast語句開始遍歷解析,該條語句解析結束也就意味着組成該語句的表達式或者標識符已遍歷解析完畢。上面提到,Parser解析模塊時,除了遍歷表達式以外,在這一過程可能還須要對計算表達式進行計算,求其值,例如相似這種值固定的BinaryExpression表達式'1' + '2'會被計算爲'12',固然這一過程的實現涉及到添加依賴來替換源碼字符串內容的。

webpack Parser在對錶達式進行計算時,它會爲該表達式實例化一個BasicEvaluatedExpression實例,該實例記錄了表達式的計算相關信息:

class BasicEvaluatedExpression {
    constructor() {
        this.type = TypeUnknown; // 當前表達式是類型,每種類型對應一個值
        this.range = null; // 表達式在ast的範圍
        this.falsy = false; // 表達式的值是否爲爲布爾值false
        this.truthy = false; // 表達式的值是否爲布爾值true
        this.bool = null; // 用來記錄表達式的值爲布爾值
        this.number = null; // 用來表達式的值爲數字
        this.regExp = null; // 用來記錄表達式的值爲正則
        this.string = null; // 用來記錄表達式的值爲字符串
        this.quasis = null; // 它與this.parts共同記錄這模板字符串表達式的內容
        this.parts = null;
        this.array = null; // 表達式值爲數組時記錄的字段
        this.items = null; // 記錄數組每項的表達式計算值
        this.options = null; // 記錄三元運算符除表達式以外的另外兩個表達式,這中狀況三元運算表達式沒法計算一個固定的靜態值
        this.prefix = null; 
        this.postfix = null;
        this.wrappedInnerExpressions = null;
        this.expression = null; // 當前表達式,內容爲表達式的ast
    }
    ...
}
複製代碼

固然,其中也包含了一些取值等方法,例如isIdentifier判斷是不是標識符表達式,asString方法將各類類型的表達式值轉換爲字符串等等。

上面說到webpack的ConstPlugin插件內部註冊了expressionConditionalOperator鉤子,它會針對三元運算符的條件表達式進行計算,來看看它的實現:

parser.hooks.expressionConditionalOperator.tap("ConstPlugin",expression => {
    // 對條件表達式計算值
    const param = parser.evaluateExpression(expression.test);
    const bool = param.asBool(); // 表達式值轉換爲布爾值
    if (typeof bool === "boolean") {
        if (expression.test.type !== "Literal") {
            // 優化:添加常量依賴,做用是直接使用Literal的值來替換條件表達式
            const dep = new ConstDependency(` ${bool}`, param.range);
            dep.loc = expression.loc;
            parser.state.current.addDependency(dep);
        }
        // Expressions do not hoist.
        // It is safe to remove the dead branch.
        //
        // Given the following code:
        //
        // false ? someExpression() : otherExpression();
        //
        // the generated code is:
        //
        // false ? undefined : otherExpression();
        //
        const branchToRemove = bool ? expression.alternate: expression.consequent;
        // 優化:對應刪除的分支,直接用undefined代替
        const dep = new ConstDependency(
            "undefined",
            branchToRemove.range
        );
        dep.loc = branchToRemove.loc;
        parser.state.current.addDependency(dep);
        return bool;
    }
});
複製代碼

能夠看出,根據表達式的計算結果,配合着依賴來動態的修改webpack最終輸出的構建代碼,正如代碼註釋的,代碼中的死分支能夠經過webapck的優化進行去除。

webpack Parser是怎麼對錶達式進行計算的呢?答案是經過調用evaluateExpression方法來實現的,該方法會返回一個BasicEvaluatedExpression實例來表示當前表達式計算結果,來看看該方法的實現:

evaluateExpression(expression) {
    try {
      // 查看是否註冊對應表達式的鉤子,若是註冊並返回非空值就用返回值做爲計算值
      const hook = this.hooks.evaluate.get(expression.type);
      if (hook !== undefined) {
        const result = hook.call(expression);
        if (result !== undefined) {
            if (result) {
                result.setExpression(expression);
            }
            return result;
        }
      }
    } catch (e) {
        console.warn(e);
    }
    // 不然實例一個
    return new BasicEvaluatedExpression()
        .setRange(expression.range)
        .setExpression(expression);
}
複製代碼

能夠看出該方法會執行外部註冊的針對不一樣表達式類型(如CallExpression)的evaluate鉤子,從而自定義表達式的計算結果,例如咱們能夠返回一個值爲字符串的計算值:

parser.hooks.evaluate.for('CallExpression').for('MyPlugin', expr => {
   return new BasicEvaluatedExpression().setString('hello world').setRange(expr.range)
})
複製代碼

這樣牽涉到CallExpression表達式值的計算時會將其轉爲會字符串結果的計算值。

須要提醒一下,能夠註冊evaluate鉤子的表達式限制爲walkExpression方法中設定的表達式類型,具體能夠參考上面第二幅圖。

補充一點,Parser內部會對ast中的Literal也進行計算,將其轉換爲BasicEvaluatedExpression實例,將字面量的值設置爲表達式計算值,可是不推薦用戶對其註冊evaluate鉤子,webpack官網有關Parser evaluate部分並無對外說明這種狀況。

this.hooks.evaluate.for("Literal").tap("Parser", expr => {
    switch (typeof expr.value) {
        case "number": // 設置實例的數字值
            return new BasicEvaluatedExpression()
                .setNumber(expr.value)
                .setRange(expr.range);
        case "string": // 設置實例的字符串值
            return new BasicEvaluatedExpression()
                .setString(expr.value)
                .setRange(expr.range);
        case "boolean": // 設置實例的布爾值
            return new BasicEvaluatedExpression()
                .setBoolean(expr.value)
                .setRange(expr.range);
	}
	if (expr.value === null) { // 設置實例的null值
            return new BasicEvaluatedExpression().setNull().setRange(expr.range);
	}
	if (expr.value instanceof RegExp) { // 設置實例的正則值
	    return new BasicEvaluatedExpression()
            .setRegExp(expr.value)
            .setRange(expr.range);
	}
});
複製代碼

何時須要表達式的計算

簡單來講,只要是調用了evaluateExpression方法,都會涉及到對錶達式的計算,它會返回一個表示表達式計算結果的BasicEvaluatedExpression實例,具體能夠看上面有關BasicEvaluatedExpression的源碼;同時該方法會執行爲當前表達式註冊的evaluate鉤子,其根據鉤子返回的結果決定是否須要由Parser初始化一個BasicEvaluatedExpression實例,因此從另外一個角度來講,evaluate鉤子是表達式計算的鉤子。

那麼具體來講,什麼狀況下須要對錶達式進行計算呢?

這取決用戶怎麼對錶達式進行優化處理

先來看看Parser內部爲這幾種表達式LiteralLogicalExpressionBinaryExpressionUnaryExpressionIdentifierCallExpressionMemberExpressionTemplateLiteralTaggedTemplateExpressionConditionalExpressionArrayExpression註冊了evaluate鉤子函數,之因此Parser內部爲這些表達式註冊鉤子,一個重要的緣由:這些狀況下的表達式都是須要進行表達式計算,從而完成代碼層面的優化處理,舉幾個例子:

  • BinaryExpression

    ("prefix" + inner + "postfix") + 123 => ("prefix" + inner + "postfix123")
    複製代碼
  • LogicalExpression

    truthyExpression() || someExpression() => truthyExpression() || false
    複製代碼

除此以外,用戶也能夠爲其餘類型的表達式註冊的evaluate鉤子來對其進行自定義解析計算。例如,小程序加強框架mpx在處理babel對Promise進行polyfill過程當中,由於小程序沒法訪問window致使雖然當前環境支持Promise,babel依然要對Promise進行polyfill,因此它對babel內部的./_global模塊中的Function('return this')()進行處理,具體以下:

parser.hooks.evaluate.for('CallExpression').tap('MpxWebpackPlugin', (expr) => {
    const current = parser.state.current
    const arg0 = expr.arguments[0]
    const callee = expr.callee
    // 對./_global模塊中調用者是 Fuction,參數爲 return this經過添加依賴了修改
    if (arg0 && arg0.value === 'return this' && callee.name === 'Function' && current.rawRequest === './_global') {
        current.addDependency(new InjectDependency({
            content: '(function() { return this })() || ',
            index: expr.range[0]
        }))
    }
})
複製代碼

最終生成的結果是在攔截的目標前注入能夠拿到window對象的代碼,即(function() { return this })()

能夠看到mpx爲CallExpression註冊的鉤子,並無返回任何BasicEvaluatedExpression實例,它只是利用攔截了這一時機,那麼webpack就會用Parser內部默認對該表達式的計算處理,具體的處理邏輯以下:

this.hooks.evaluate.for("CallExpression").tap("Parser", expr => {
    if (expr.callee.type !== "MemberExpression") return;
	if (expr.callee.property.type !==(expr.callee.computed ? "Literal" : "Identifier"))
	    return;
	const param = this.evaluateExpression(expr.callee.object);
	if (!param) return;
	const property = expr.callee.property.name || expr.callee.property.value;
	const hook = this.hooks.evaluateCallExpressionMember.get(property);

	if (hook !== undefined) {
		return hook.call(expr, param);
	}
});
複製代碼

DefinePlugin自定義解析表達式

下面咱們以文章開始提到的例子來看看DefinePlugin如何進行自定義表達式的解析計算。DefinePlugin插件在webpack compiler的compilations鉤子註冊了針對js模塊的解析:

compiler.hooks.compilation.tap('DefinePlugin', (compilation, {normalModuleFactory}) => {
    ...
// 爲不一樣的js模塊註冊模塊解析 
    normalModuleFactory.hooks.parser
        .for("javascript/auto").tap("DefinePlugin", handler);
    normalModuleFactory.hooks.parser
         .for("javascript/dynamic").tap("DefinePlugin", handler);
    normalModuleFactory.hooks.parser
        .for("javascript/esm").tap("DefinePlugin", handler);
})
複製代碼

下面來看看handler的處理,入口方法是walkDefinitions,用來遍歷該插件配置的對象參數definitions中的key,從而對由該key組成的表達式計算值。對於文章開始的例子該值是:

{
 "process.env": {
     "NODE_ENV": JSON.stringify('development')
  }
}
複製代碼

walkDefinitions怎麼對對象參數的key進行遍歷呢?show code:

const walkDefinitions = (definitions, prefix) => {
    Object.keys(definitions).forEach(key => {
        const code = definitions[key];
        // key對應的值爲純對象形式,如process.env對應key的值爲對象
        if (code && typeof code === "object" &&
            !(code instanceof RuntimeValue) &&
            !(code instanceof RegExp)
        ) {
            walkDefinitions(code, prefix + key + ".");
            // 對象形式調用的key的自定義解放方式,如process
            applyObjectDefine(prefix + key, code);
            return;
        }
        // 對如嵌套對象形式的key,非最後一層的key的自定義解析方式,如process.env
        applyDefineKey(prefix, key); 
        // 最後一層路徑key的自定義解析方式, 如process.env.NODE_DEV
        applyDefine(prefix + key, code); 
    });
};
複製代碼

能夠看出,該方法最終會對對象每一層key都會應用表達式解析,例如上面的例子,它會爲processprocess.envprocess.env.NODE_ENV分別註冊對應的Parser鉤子來解析。

先來看看applyObjectDefine方法對process.env的解析,

const applyObjectDefine = (key, obj) => {
    // 運行對process.env進行重命名,便可以將其賦值給其餘變量
    parser.hooks.canRename.for(key)
        .tap("DefinePlugin", ParserHelpers.approve);
	// 爲process.env爲註冊evaluateIdentifier鉤子
	// 該鉤子會返回標識符process.env的自定義的表達式計算值給Parser調用者
    parser.hooks.evaluateIdentifier.for(key)
        .tap("DefinePlugin", expr =>
            new BasicEvaluatedExpression().setTruthy().setRange(expr.range)
        );
        // 對對象執行typeof 會將表達式的計算值設置爲字符串‘object’
        // 與typeof鉤子不一樣的是,該鉤子能夠對錶達式進行計算,自定義返回表達式的值,typeof不涉及到表達式的計算
    parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr =>{
        return ParserHelpers.evaluateToString("object")(expr);
    });

	// 優化process.env的值,由於值固定,配合依賴來將表達式內容替換爲:Object({NODE_ENV:'development'})
    parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
        const strCode = stringifyObj(obj, parser);
        if (/__webpack_require__/.test(strCode)) {
            return ParserHelpers.toConstantDependencyWithWebpackRequire(parser, strCode)(expr);
        } else {
            return ParserHelpers.toConstantDependency(parser, strCode)(expr);
        }
    });
	// typeof process.env時會配合依賴直接替換表達式的內容爲'object'
    parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
        return ParserHelpers.toConstantDependency(parser, JSON.stringify("object"))(expr);
    });
};
複製代碼

接着看applyDefineKey方法如何對組成process.env每一層進行處理的

// 對於上面的例子,key爲NODE_ENV,不會走到forEach語句
const applyDefineKey = (prefix, key) => {
	const splittedKey = key.split(".");
	splittedKey.slice(1).forEach((_, i) => {
		const fullKey = prefix + splittedKey.slice(0, i + 1).join(".");
		parser.hooks.canRename
			.for(fullKey)
			.tap("DefinePlugin", ParserHelpers.approve);
	});
};
複製代碼

該方法會對definitions相似{'a.b.c': 1}或者{proces.env: {'a.b.c': 1}}這種定義的形式,分別對aa.b設置能夠重命名。

最後看看applyDefine如何對全局變量表達式進行解析的,上碼:

const applyDefine = (key, code) => {
    const isTypeof = /^typeof\s+/.test(key);
    if (isTypeof) key = key.replace(/^typeof\s+/, "");
    let recurse = false;
    let recurseTypeof = false;
    if (!isTypeof) {
        // 爲 process.env.NODE_ENV設置可命名
        parser.hooks.canRename.for(key).tap("DefinePlugin", ParserHelpers.approve);
        // 爲process.env.NODE_ENV設置該鉤子,防止循環依賴
        parser.hooks.evaluateIdentifier.for(key).tap("DefinePlugin", expr => {
            // this is needed in case there is a recursion ithe DefinePlugin
            // to prevent an endless recursion
            // e.g.: new DefinePlugin({
                // "a": "b",
                // "b": "a"
	            //})
            if (recurse) return;
            recurse = true;
            //對最終key對應的值轉換爲字符串,並獲得其ast,而後對ast表達式計算值
            const res = parser.evaluate(toCode(code, parser));
            recurse = false;
            res.setRange(expr.range);
            return res;
        });
        // 解析process.env.NODE_ENV表達式時,添加依賴使用其具體值來替換該表達式
        parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
            // 將key對應的值先轉換爲對應的字符串形式,經過依賴對字符串的操做來替換表達式
            const strCode = toCode(code, parser); 
            if (/__webpack_require__/.test(strCode)) {
                return ParserHelpers.toConstantDependencyWithWebpackRequire(parser, strCode)(expr);
            } else {
                return ParserHelpers.toConstantDependency(parser, strCode)(expr);
            }
        });
    }
    parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {
        // this is needed in case there is a recursion in the DefinePlugin
        // to prevent an endless recursion
        // e.g.: new DefinePlugin({
        // "typeof a": "typeof b",
        // "typeof b": "typeof a"
        // });
            if (recurseTypeof) return;
            recurseTypeof = true;
            //值先轉換字符串
            const typeofCode = isTypeof
			? toCode(code, parser)
			: "typeof (" + toCode(code, parser) + ")"; 
            // 解析字符串的ast並對其進行表達式計算
            const res = parser.evaluate(typeofCode); 
            recurseTypeof = false;
            res.setRange(expr.range);
            return res;
	});
	// 對process.env.NODE_ENV進行typeof時,添加依賴使用其對應值的類型來替換該表達式
    parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
        const typeofCode = isTypeof
                ? toCode(code, parser)
                : "typeof (" + toCode(code, parser) + ")";
        const res = parser.evaluate(typeofCode);
        if (!res.isString()) return;
        return ParserHelpers.toConstantDependency(parser,  JSON.stringify(res.string)).bind(parser)(expr);
    });
};
複製代碼

能夠看到,webpack經過DefinePlugin插件定義的全局變量,變量對應的表達式的值是靜態固定的,如訪問process.env.NODE_ENV時,其值就是指定的配置值;因此該插件最終是經過模塊解析階段,webpack Parser提供的不一樣時機的鉤子,配合着依賴,來對值固定的表達式內容進行替換。

相關文章
相關標籤/搜索