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提供的不一样时机的钩子,配合着依赖,来对值固定的表达式内容进行替换。

相关文章
相关标签/搜索