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.target 和 import.meta 属性,下面我们对其进行简短介绍。
new.target
该属性常用来检测函数是否是通过 new 运行符被调用的,在通过 new 运算符初始化的函数中,new.target 返回一个指向该函数的引用,在普通的函数调用中,new.target 为 undefined。
通过该属性,我们在使用函数模拟类实现时,可强行要求调用方使用 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 语句的判断条件)时触发:
-
ArrowFunctionExpressionconst doSomething = () => {}; // 触发钩子 parser.hooks.evaluate.for('ArrowFunctionExpression').tap('DemoPlugin', (expression) => {}); -
AssignmentExpressionlet a; const b = a = 12; // 触发钩子 // OR if (a = 12) { // 触发钩子 } parser.hooks.evaluate.for('AssignmentExpression').tap('DemoPlugin', (expression) => {}); -
AwaitExpressionasync 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) => {}); -
CallExpressionfunction doSomething() { } const result = doSomething(); // 触发钩子 // OR if (doSomething()) { // 触发钩子 } parser.hooks.evaluate.for('CallExpression').tap('DemoPlugin', (expression) => {}); -
ClassExpressionconst A = class { // 触发钩子 constructor() {} } parser.hooks.evaluate.for('ClassExpression').tap('DemoPlugin', (expression) => {}); -
FunctionExpressionconst doSomething = function () { // 触发钩子 } parser.hooks.evaluate.for('FunctionExpression').tap('DemoPlugin', (expression) => {}); -
Identifierconst 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) => {}); -
MemberExpressionclass A { doSomething() { } } const a = new A(); a.doSomething(); // 触发钩子 parser.hooks.evaluate.for('MemberExpression').tap('DemoPlugin', (expression) => {}); -
NewExpressionclass A { constructor() {} } const a = new A(); // 触发钩子 parser.hooks.evaluate.for('NewExpression').tap('DemoPlugin', (expression) => {}); -
ObjectExpressionconst a = {}; // 触发钩子 parser.hooks.evaluate.for('ObjectExpression').tap('DemoPlugin', (expression) => {}); -
SequenceExpression序列表达式(也叫逗号操作符),对操作数进行从左到右求值,并返回最后一个操作数的值,详情参见:逗号操作符。
let x = 1; x = (x++, x); // 触发钩子 parser.hooks.evaluate.for('SequenceExpression').tap('DemoPlugin', (expression) => {}); -
TaggedTemplateExpressionconst div = html`<div>hello world</div>`; // 触发钩子 parser.hooks.evaluate.for('TaggedTemplateExpression').tap('DemoPlugin', (expression) => {}); -
ThisExpressionconst 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 的有以下几种类型:
BlockStatementVariableDeclarationFunctionDeclarationReturnStatementClassDeclarationExpressionStatementImportDeclarationExportAllDeclarationExportDefaultDeclarationExportNamedDeclarationIfStatementSwitchStatementForInStatementForOfStatementForStatementWhileStatementDoWhileStatementThrowStatementTryStatementLabeledStatementWithStatement
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
全局函数(比如:setTimeout、eval 等)调用时被触发:
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(在ConditionalExpression、TemplateLiteral及SpreadElement类型下)和evaluateDefinedIdentifier,笔者结合源码调试了各种情况,均无法构建出能够成功触发其钩子的表达式,如有哪位读者知道,还请一定告知,某不胜感激。
总结
本文我们先对自由变量、MetaProperty 基本概念进行了说明,然后介绍了 JavaScriptParser 的获取方式,最后对 JavaScriptParser 的钩子函数的使用进行了说明,希望本文能够弥补 Webpack 官方的缺陷,解决大家在使用过程中所遇到的疑惑。如有疏漏之处还望诸位海涵,祝大家快乐编码每一天。
参考链接
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。