Webpack JavaScriptParser Hooks 使用小结
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
语句的判断条件)时触发:
-
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
全局函数(比如: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 官方的缺陷,解决大家在使用过程中所遇到的疑惑。如有疏漏之处还望诸位海涵,祝大家快乐编码每一天。
参考链接
转载自:https://juejin.cn/post/7117837829165547556