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

文章梳理了webapck Parser解析模塊的流程,根據解析模塊過程中Parser針對不同語句即表達式解析拋出的事件注册送28体验金的游戏平台,我們可以自定義地為模塊添加依賴,從而通過依賴來完成相應的功能。本文將繼續從Parser表達式的解析和計算來理解Parser,并配合webpack DefinePlugin定義全局變量的插件來加以分析。

現象

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

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

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

if (process.env.NODE_ENV === 'development') {...}

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

if ('development' === 'development') {...}

不僅如此,像process.env.NODE_ENV這種對象成員調用形式的全局變量,根據js的規則,那么他們的對象也是可以訪問的注册送28体验金的游戏平台,webpack通過DefinePlugin插件對這種形式也進行了處理。

var obj = process.env 
// 它會被轉換為 var obj = Object({NODE_ENV: "development"})
var _type = typ process.env 
// 它會轉換為 var _type = "object"

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

表達式的解析與計算

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

  • 表達式的遍歷解析

  • 計算表達式的值

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

表達式的遍歷解析

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

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鉤子,正是計算條件表達式的值來作為返回結果。

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

在給出例子的這一解析遍歷ConditionalExpression表達式的過程中,對外拋出了如下鉤子函數:

  • walkConditionalExpression:該方法拋出expressionConditionalOperator鉤子

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

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

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

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

利用這些鉤子,用戶可以自定義表達式結果,例如上面的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方法將各種類型的表達式值轉換為字符串等等。

還以上面的ConditionalExpression表達式的例子加以說明,webpack Parser會對三元運算符的條件表達式進行計算求值,上面說到webpack的ConstPlugin插件內部注冊了expressionConditionalOperator鉤子,它會針對三元運算符的條件表達式進行計算,來看看它的實現:

parser.hooks.expressionConditionalOperator.tap("ConstPlugin",expression => {
    // 對條件表達式計算值
    const param = parser.evaluateExpression(expression.test);
    const bool = param.asBool(); // 表達式值轉換為布爾值
    if (typ 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鉤子,有關Parser evaluate部分并沒有對外說明這種情況。

this.hooks.evaluate.for("Literal").tap("Parser", expr => {
    switch (typ 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 instanc 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鉤子來對其進行自定義解析計算。例如,小程序增強框架在處理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 && typ code === "object" &&
            !(code instanc RuntimeValue) &&
            !(code instanc 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都會應用表達式解析,例如上面的例子注册送28体验金的游戏平台,它會為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)
        );
        // 對對象執行typ 會將表達式的計算值設置為字符串‘object’
        // 與typ鉤子不同的是,該鉤子可以對表達式進行計算,自定義返回表達式的值,typ不涉及到表達式的計算
    parser.hooks.evaluateTyp.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);
        }
    });
    // typ process.env時會配合依賴直接替換表達式的內容為'object'
    parser.hooks.typ.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 isTyp = /^typ\s+/.test(key);
    if (isTyp) key = key.replace(/^typ\s+/, "");
    let recurse = false;
    let recurseTyp = false;
    if (!isTyp) {
        // 為 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.evaluateTyp.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({
        // "typ a": "typ b",
        // "typ b": "typ a"
        // });
            if (recurseTyp) return;
            recurseTyp = true;
            //值先轉換字符串
            const typCode = isTyp
            ? toCode(code, parser)
            : "typ (" + toCode(code, parser) + ")"; 
            // 解析字符串的ast并對其進行表達式計算
            const res = parser.evaluate(typCode); 
            recurseTyp = false;
            res.setRange(expr.range);
            return res;
    });
    // 對process.env.NODE_ENV進行typ時,添加依賴使用其對應值的類型來替換該表達式
    parser.hooks.typ.for(key).tap("DefinePlugin", expr => {
        const typCode = isTyp
                ? toCode(code, parser)
                : "typ (" + toCode(code, parser) + ")";
        const res = parser.evaluate(typCode);
        if (!res.isString()) return;
        return ParserHelpers.toConstantDependency(parser,  JSON.stringify(res.string)).bind(parser)(expr);
    });
};

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

原文鏈接:

上一篇:Angular1.x+Webpack4+Layui+IE8兼容項目
下一篇:setTimeout和setImmediate到底誰先執行,本文讓你徹底理解Event Loop

相關推薦

  • 🚀webpack 4 beta — try it today!🚀

    Now that webpack is a 0CJS (Zero Configuration) outofthebox bundler, we will lay groundwork in 4.x a...

    2 年前
  • 😀一個原生js彈幕庫

    danmujs 😀一個原生js彈幕庫,基于 CSS3 Animation 地址、核心代碼 本項目基于 rcbullets,項目約70%的代碼基于rcbullets,首先要感謝這個項目的作者,如...

    2 個月前
  • 🔥一步一步的帶你走進Webpack4的世界

    前言 webpack是當下最熱門的前端資源模塊化管理和打包工具,它可以將許多松散的模塊按照依賴和規則打包成符號生產環境部署的前端資源,還可以將按需加載的模塊進行代碼分割。

    1 個月前
  • 🔥《吊打面試官》系列 Node.js 必知必會必問!

    (/public/upload/f204a3b224d986128f1b4d9b8d06cd17) 前言 codeing 應當是一生的事業,而不僅僅是 30 歲的青春🍚 本文已收錄 Git...

    11 天前
  • 🔥Webpack 插件開發如此簡單!

    本文使用的WebpackQuicklyStarter快速搭建 Webpack4 本地學習環境。 建議多閱讀 Webpack 文檔《Writing a Plugin》章節,學習開發簡單插件。

    1 個月前
  • (獨家!)webpack 5 changelog 全文翻譯

    ★ webpack 團隊于北京時間 10 月 12 日凌晨發布了 版本,本文譯自 。此部分主要面向非插件開發的 webpack 使用者。 ” 簡要說明 此版本重點關注以下內容: ...

    6 個月前
  • (vuejs學習)2、使用ElementUI(*)

    1.element安裝 開發環境是win10,一到node官網下載node的.msi包(https://npm.taobao.org/mirrors/node/v10.16.0/nodev10.16....

    8 個月前
  • (vuejs學習)1、Vue初上手(*)

    參考《官方(https://cli.vuejs.org/zh/guide/installation.html)》官方: Node 版本要求: Vue CLI 需要 Node.js 8.9 或更高...

    8 個月前
  • 黃金搭檔 -- JS 裝飾器(Decorator)與Node.js路由

    很多面對象語言中都有裝飾器(Decorator)函數的概念,Javascript語言的ES7標準中也提及了Decorator,個人認為裝飾器是和一樣讓人興奮的的變化。

    10 個月前
  • 麻煩把JS的事件環給我安排一下!!!

    上次大家跟我吃飽喝足又擼了一遍PromiseA,想必大家肯定滿腦子想的都是西瓜可樂...... 什么西瓜可樂!明明是Promise! 呃,清醒一下,今天大家搬個小板凳,聽我說說JS中比較有意思的事...

    2 年前

官方社區

掃碼加入 JavaScript 社區