likes
comments
collection
share

Webpack JavaScriptParser Hooks 使用小结

作者站长头像
站长
· 阅读数 45

Webpack 在将代码转换为 AST 的同时为我们提供了 JavaScriptParser 钩子函数,以便开发者能够分析模块依赖,并根据自己的需求替换或添加依赖。由于官网对某些钩子的解释含糊不清,故此笔者抽空具体研究了下,今天把相关结论记录在此,以供大家参考。

一些概念

在具体解释 JavaScriptParser 钩子函数之前,我们先解释一下这些钩子函数涉及到的一些概念。

自由变量

在 JavaScript 中,如果某个作用域中使用了变量 a,但变量 a 并未在该作用域中声明,那么变量 a 即为自由变量。比如下面的例子:

const a = 1;

function doSomething() {
  console.log(a);
}

上例中,doSomething 函数体内使用了变量 a,但变量 a 不是在 doSomething 中,而是在其函数体的父作用域中声明的,所以此刻我们可称变量 a 为自由变量。

上文我们介绍了什么是自由变量,这里需要注意的是,在 Webpack JavaScriptParser 钩子函数中,作用域以模块为单位,即如果某个模块使用了变量 a,但变量 a 并未在该模块中声明,那么变量 a 即为自由变量。

再看上面的例子,由于 doSomething 函数体与变量 a 位于同一个模块,故变量 a 不能称之为自由变量。

再看下面的例子:

// src/a.js
export const a = 1;

// src/index.js
import { a } from './a';

function doSomething() {
  console.log(a);
}

上例中,由于 doSomething 函数体与变量 a 位于不同的模块,此时变量 a 便可称为自由变量。

MetaProperty

如果看 ESTree Spec MetaProperty 的解释,相信大家直接会晕掉,通过翻看 acorn 的实现可知,MetaProperty 指的就是 new.targetimport.meta 属性,下面我们对其进行简短介绍。

new.target

该属性常用来检测函数是否是通过 new 运行符被调用的,在通过 new 运算符初始化的函数中,new.target 返回一个指向该函数的引用,在普通的函数调用中,new.targetundefined

通过该属性,我们在使用函数模拟类实现时,可强行要求调用方使用 new 运算符初始化,比如下面的例子:

function Foo() {
  if (!new.target) {
    throw new Error('Foo() must be called with new');
  }
  console.log('Foo instantiated with new');
}

Foo(); // 异常:Foo() must be called with new
new Foo(); // 输出:Foo instantiated with new

在类中,该属性用于指向被初始化类的类定义,即使包含继承关系,比如下面的例子:

class A {
  constructor() {
    console.log(new.target);
  }          
}

class B extends A {}

new A(); // 输出:class A { constructor() { console.log(new.target); } }
new B(); // 输出:class B extends A {}

该属性仅能在函数或类的方法中使用。

import.meta

通过 import.meta 可获得当前模块的元信息,该属性只能在模块内部使用,常用属性有:

  • import.meta.url:在浏览器环境下返回模块所在的 URL 路径,在 Node.js 环境下返回模块所在的本地路径(以 file:/ 开头);
  • import.meta.scriptElement:仅在浏览器下可用,返回加载该模块的 <script> 元素,作用等同于 document.currentScript

获取实例

可通过 NormalModuleFactory 获取 JavaScriptParser 实例,比如下面的例子:

compiler.hooks.compilation.tap('DemoPlugin', (compilation, { normalModuleFactory }) => {
  normalModuleFactory.hooks.parser.for('javascript/auto').tap('DemoPlugin', (parser, options) => {});
});

上例代码清晰明了,不过多阐述,只对 for 中参数的可用值进行简短介绍:

  • javascript/auto:处理 CommonJS、AMD 及 ESModule 格式的 JavaScript 模块及其依赖;
  • javascript/dynamic:处理 CommonJS 及 AMD 格式的 JavaScript 模块及其依赖;
  • javascript/esm:处理 ESModule 格式的 JavaScript 模块及其依赖。

钩子列表

得到 JavaScriptParser 的实例对象 parser 后,我们便可使用以下方式订阅 JavaScriptParser 的钩子函数(有些钩子无需调用 for 方法):

parser.hooks.${hook}.for(${identifier}).tap('DemoPlugin', (expression) => {});

其中:

  • ${hook}:为要监听的钩子函数;
  • ${identifier}:为要监听的表达式(比如变量 a)。

这里需要注意的是,在 tap 的回调函数中,不要试图尝试修改 AST 的内容,而是分析模块依赖,并根据自己的需求替换或添加依赖,这是因为最终代码是通过代码生成器(JavaScriptGenerator)配合依赖模板生成的,而依赖模板是基于 loader 处理后的代码而不是 JavaScriptParser 解析出来的 AST。

evaluateTypeof

将对自由变量(或其属性)、new.target(或其属性)、import.meta(或其属性)执行 typeof 操作赋值给某一变量(或作为 if 语句的判断条件)时触发:

// src/a.js
export const a = 1;

// src/index.js
import { a } from './a';

const typeA = typeof a; // 触发钩子

function Foo() {
  if (typeof new.target) { // 触发钩子
  }
}

if (typeof import.meta) { // 触发钩子
}

// plugins/demo-plugin.js
parser.hooks.evaluateTypeof.for('a').tap('DemoPlugin', (expression) => {});
parser.hooks.evaluateTypeof.for('new.target').tap('DemoPlugin', (expression) => {});
parser.hooks.evaluateTypeof.for('import.meta').tap('DemoPlugin', (expression) => {});

evaluate

将以下类型的表达式赋值给某一变量(或作为 if 语句的判断条件)时触发:

  • ArrowFunctionExpression

    const doSomething = () => {}; // 触发钩子
    
    parser.hooks.evaluate.for('ArrowFunctionExpression').tap('DemoPlugin', (expression) => {});
    
  • AssignmentExpression

    let a;
    
    const b = a = 12; // 触发钩子
    // OR
    if (a = 12) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('AssignmentExpression').tap('DemoPlugin', (expression) => {});
    
  • AwaitExpression

    async function doSomething() {
    }
    
    async function main() {
      const result = await doSomething(); // 触发钩子
    }
    // OR
    async function main() {
      if (await doSomething()) { // 触发钩子
      }
    }
    
    parser.hooks.evaluate.for('AwaitExpression').tap('DemoPlugin', (expression) => {});
    
  • BinaryExpression

    二元运算操作符详见:BinaryOperator

    const a = 12;
    
    const d = a >= 12; // 触发钩子
    // OR
    if (a >= 12) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('BinaryExpression').tap('DemoPlugin', (expression) => {});
    
  • CallExpression

    function doSomething() {
    }
    
    const result = doSomething(); // 触发钩子
    // OR
    if (doSomething()) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('CallExpression').tap('DemoPlugin', (expression) => {});
    
  • ClassExpression

    const A = class { // 触发钩子
      constructor() {}
    }
    
    parser.hooks.evaluate.for('ClassExpression').tap('DemoPlugin', (expression) => {});
    
  • FunctionExpression

    const doSomething = function () { // 触发钩子
    }
    
    parser.hooks.evaluate.for('FunctionExpression').tap('DemoPlugin', (expression) => {});
    
  • Identifier

    const a = 1;
    
    const b = a; // 触发钩子
    // OR
    if (a) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('Identifier').tap('DemoPlugin', (expression) => {});
    
  • LogicalExpression

    逻辑表达式,操作符有 &&||??

    const a = 1;
    const b = 2;
    
    const c = a || b; // 触发钩子
    // OR
    if (a || b) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('LogicalExpression').tap('DemoPlugin', (expression) => {});
    
  • MemberExpression

    class A {
      doSomething() {
      }
    }
    
    const a = new A();
    a.doSomething(); // 触发钩子
    
    parser.hooks.evaluate.for('MemberExpression').tap('DemoPlugin', (expression) => {});
    
  • NewExpression

    class A {
      constructor() {}
    }
    
    const a = new A(); // 触发钩子
    
    parser.hooks.evaluate.for('NewExpression').tap('DemoPlugin', (expression) => {});
    
  • ObjectExpression

    const a = {}; // 触发钩子
    
    parser.hooks.evaluate.for('ObjectExpression').tap('DemoPlugin', (expression) => {});
    
  • SequenceExpression

    序列表达式(也叫逗号操作符),对操作数进行从左到右求值,并返回最后一个操作数的值,详情参见:逗号操作符

    let x = 1;
    
    x = (x++, x); // 触发钩子
    
    parser.hooks.evaluate.for('SequenceExpression').tap('DemoPlugin', (expression) => {});
    
  • TaggedTemplateExpression

    const div = html`<div>hello world</div>`; // 触发钩子
    
    parser.hooks.evaluate.for('TaggedTemplateExpression').tap('DemoPlugin', (expression) => {});
    
  • ThisExpression

    const self = this; // 触发钩子
    // OR
    if (this) { // 触发钩子 
    }
    
    parser.hooks.evaluate.for('ThisExpression').tap('DemoPlugin', (expression) => {});
    
  • UnaryExpression

    一元运算操作符参见:UnaryOperator

    const a = 1;
    
    const b = typeof a; // 触发钩子
    // OR
    if (typeof a) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('UnaryExpression').tap('DemoPlugin', (expression) => {});
    
  • UpdateExpression

    递增或递减表达式,操作符有 ++--

    const a = 1;
    
    const b = a++; // 触发钩子
    // OR
    if (a++) { // 触发钩子
    }
    
    parser.hooks.evaluate.for('UpdateExpression').tap('DemoPlugin', (expression) => {});
    

evaluateIdentifier

将自由变量(或其属性)赋值给某一变量(或作为 if 语句的判断条件)时触发:

// src/a.js
export const a = 12;

// src/index.js
import { a } from './a';

const b = a; // 触发钩子

if (a) { // 触发钩子
}

// plugins/demo-plugin.js
parser.hooks.evaluateIdentifier.for('a').tap('DemoPlugin', (expression) => {});

evaluateCallExpressionMember

将字面值对象或类的成员函数的返回值赋值给某一变量(或作为 if 语句的判断条件)时触发:

class A {
  doSomething() {}
}

const a = new A();

const b = a.doSomething(); // 触发钩子

if (a.doSomething()) { // 触发钩子
}

// OR
const a = {
  doSomething: () => {}
};

const b = a.doSomething(); // 触发钩子

if (a.doSomething()) { // 触发钩子
}

parser.hooks.evaluateCallExpressionMember.for('doSomething').tap('DemoPlugin', (expression) => {});

statement

通用钩子,每解析一个语句调用一次:

parser.hooks.statement.tap('DemoPlugin', (statement) => {});

其中 statement.type 的有以下几种类型:

  • BlockStatement
  • VariableDeclaration
  • FunctionDeclaration
  • ReturnStatement
  • ClassDeclaration
  • ExpressionStatement
  • ImportDeclaration
  • ExportAllDeclaration
  • ExportDefaultDeclaration
  • ExportNamedDeclaration
  • IfStatement
  • SwitchStatement
  • ForInStatement
  • ForOfStatement
  • ForStatement
  • WhileStatement
  • DoWhileStatement
  • ThrowStatement
  • TryStatement
  • LabeledStatement
  • WithStatement

statementIf

在解析 if 语句时触发;作用等同于 statement,但仅在 statement.type == 'IfStatement' 时触发:

if (a) { // 触发钩子
}

parser.hooks.statementIf.tap('DemoPlugin', (statement) => {});

label

在解析带有标签的语句时触发;作用等同于 statement,但仅在 statement.type == 'LabeledStatement' 时触发:

loop:
while (true) {
} // 触发钩子

parser.hooks.label.for('loop').tap('DemoPlugin', (statement) => {});

import

解析 import 语句时触发:

import _ from 'lodash'; // 触发钩子

parser.hooks.import.tap('DemoPlugin', (statement, source) => {});

importSpecifier

解析 import 语句时,碰到一个导入的标识符触发一次:

import _, { has } from 'lodash'; // 触发钩子(触发 2 次)

parser.hooks.importSpecifier.tap('DemoPlugin', (statement, source, exportName, identifierName) => {});

export

解析 export 语句时触发:

export function doSomething() {} // 触发钩子

parser.hooks.export.tap('DemoPlugin', (statement) => {});

exportImport

解析类似 export * from 'otherModule' 语句时触发:

export * from './utils'; // 触发钩子

parser.hooks.exportImport.tap('DemoPlugin', (statement, source) => {});

exportDeclaration

解析导出声明时触发:

export const a = 1; // 触发钩子
export let b = 2; // 触发钩子
export var c = 3; // 触发钩子
export function doSomething() {} // 触发钩子
export class A {} // 触发钩子

parser.hooks.exportDeclaration.tap('DemoPlugin', (statement, declaration) => {});

exportExpression

解析导出表达式(一般为 export default)时触发:

export function doSomething() {}

export default { doSomething } // 触发钩子

parser.hooks.exportExpression.tap('DemoPlugin', (statement, declaration) => {});

exportSpecifier

除了在满足 exportDeclaration 触发条件时触发外,还将在下列情况下触发:

const a = 1
const b = 2;
export { a, b }; // 触发钩子(触发 2 次)

parser.hooks.exportSpecifier.tap('DemoPlugin', (statement, identifierName, exportName, index) => {});

exportImportSpecifier

解析类似 export { a, b } from 'otherModule' 语句时触发,每碰到一个导出标识符执行一次:

export { a, b } from './utils'; // 触发钩子(触发 2 次)

parser.hooks.exportImportSpecifier.tap('DemoPlugin', (statement, source, identifierName, exportName, index) => {});

varDeclaration

变量声明时触发:

const a = 1; // 触发钩子
// OR
let a = 1; // 触发钩子
// OR
var a = 1; // 触发钩子

parser.hooks.varDeclaration.for('a').tap('DemoPlugin', (declaration) => {});

varDeclarationLet

通过 let 声明变量时触发:

let a = 1; // 触发钩子

parser.hooks.varDeclarationLet.for('a').tap('DemoPlugin', (declaration) => {});

varDeclarationConst

通过 const 声明变量时触发:

const a = 1; // 触发钩子

parser.hooks.varDeclarationConst.for('a').tap('DemoPlugin', (declaration) => {});

varDeclarationVar

通过 var 声明变量时触发:

var a = 1; // 触发钩子

parser.hooks.varDeclarationVar.for('a').tap('DemoPlugin', (declaration) => {});

canRename

将某个全局变量(或其属性)赋值给另外一个变量之前触发,以确定是否允许该操作,通常与 rename 钩子一起使用:

const b = a; // 触发钩子

parser.hooks.canRename.for('a').tap('DemoPlugin', (expression) => {
  return true; // returning true allows renaming
});

rename

将某个全局变量(或其属性)赋值给另外一个变量时且 canRename 钩子返回 true 时触发:

const b = a; // 触发钩子

parser.hooks.rename.for('a').tap('DemoPlugin', (expression) => {});

assign

对自由变量赋予新值时触发:

// src/a.js
export let a = 1;

// src/index.js
import { a } from './a';

a = 2; // 触发钩子

parser.hooks.assign.for('a').tap('DemoPlugin', (expression) => {});

typeof

将对自由变量(或其属性)执行 typeof 操作时触发:

// src/a.js
export const a = 1;

// src/index.js
import { a } from './a';

typeof a; // 触发钩子

// plugins/demo-plugin.js
parser.hooks.typeof.for('a').tap('DemoPlugin', (expression) => {});

call

全局函数(比如:setTimeouteval 等)调用时被触发:

eval(/* doSomething */); // 触发钩子

parser.hooks.call.for('eval').tap('DemoPlugin', (expression) => {});

callMemberChain

全局对象(比如:document 等)的成员方法被调用时触发:

document.getElementById('main'); // 触发钩子

parser.hooks.callMemberChain.for('document').tap('DemoPlugin', (expression, properties) => {});

new

实例化不在本模块定义的类,并将其实例赋予某个变量时触发:

// src/a.js
export class A {}

// src/index.js
import { A } from './a';

const a = new A(); // 触发钩子

// plugins/demo-plugin.js
parser.hooks.new.for('A').tap('DemoPlugin', (expression) => {});

expression

解析含有某个全局变量的表达式时触发:

const a = this; // 触发钩子

typeof this; // 触发钩子

parser.hooks.expression.for('this').tap('DemoPlugin', (expression) => {});

expressionConditionalOperator

解析三目运算符时触发:

const a = 1;

a > 1 ? true : false; // 触发钩子

parser.hooks.expressionConditionalOperator.tap('DemoPlugin', (expression) => {});

program

获取代码的抽象语法树(AST)信息:

parser.hooks.program.tap('DemoPlugin', (ast, comments) => {});

说明

  • 钩子 assigned 在 Webpack 5 已废弃,故本文不再阐述;
  • 钩子 evaluate(在 ConditionalExpressionTemplateLiteralSpreadElement 类型下)和 evaluateDefinedIdentifier,笔者结合源码调试了各种情况,均无法构建出能够成功触发其钩子的表达式,如有哪位读者知道,还请一定告知,某不胜感激。

总结

本文我们先对自由变量、MetaProperty 基本概念进行了说明,然后介绍了 JavaScriptParser 的获取方式,最后对 JavaScriptParser 的钩子函数的使用进行了说明,希望本文能够弥补 Webpack 官方的缺陷,解决大家在使用过程中所遇到的疑惑。如有疏漏之处还望诸位海涵,祝大家快乐编码每一天。

参考链接

转载自:https://juejin.cn/post/7117837829165547556
评论
请登录