以下是普通函数function ast(){},转换为ast后的格式:
webpack、Lint等这些工具的原理都是通过 JavaScript Parser 把源代码转化为一颗抽象语法树(AST),通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。
那什么是JavaScript解析器呢? JavaScript解析器的作用,把JavaScript源码转化为抽象语法树。 浏览器会把js源码通过解析器转换为ast,再进一步转换为字节码或者直接生成机器码,进行渲染和执行。 一般来说,每个js引擎都有自己的抽象语法树格式,Chrome的v8引擎, firfox的Spider Monkey引擎等。
JavaScript解析器通常可以包含四个组成部分。
首先词法分析器会扫描(scanning)代码,将一行行源代码,通过switch case 把源码?“/为一个个小单元,jS代码有哪些语法单元呢?大致有以下这些:
就是一个字符一个字符地遍历,然后通过switch case分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。以@babel/parser源码为例:
getTokenFromCode(code) {
switch (code) {
case 46:
this.readToken_dot();
return;
case 40:
++this.state.pos;
this.finishToken(10);
return;
case 41:
++this.state.pos;
this.finishToken(11);
return;
case 59:
++this.state.pos;
this.finishToken(13);
return;
// 此处省略...
case 92:
this.readWord();
return;
default:
if (isIdentifierStart(code)) {
this.readWord(code);
return;
}
}
}
readToken_dot() {
// charCodeAt一个一个向后移
const next = this.input.charCodeAt(this.state.pos + 1);
if (next >= 48 && next <= 57) {
this.readNumber(true);
return;
}
if (next === 46 && this.input.charCodeAt(this.state.pos + 2) === 46) {
this.state.pos += 3;
this.finishToken(21);
} else {
++this.state.pos;
this.finishToken(16);
}
}
将上一步生成的数组,根据语法规则,转为抽象语法树(Abstract Syntax Tree,简称AST)。 以 const a = 1 为例,词法分析中获得了 const 这样一个 token,并判断这是一个关键字,根据这个 token 的类型,判断这是一个变量声明语句。以@babel/parser源码为例,执行 parseVarStatement 方法。
parseVarStatement(node, kind, allowMissingInitializer = false) {
const {
isAmbientContext
} = this.state;
const declaration = super.parseVarStatement(node, kind, allowMissingInitializer || isAmbientContext);
if (!isAmbientContext) return declaration;
for (const {
id,
init
} of declaration.declarations) {
if (!init) continue;
if (kind !== "const" || !!id.typeAnnotation) {
this.raise(TSErrors.InitializerNotAllowedInAmbientContext, {
at: init
});
} else if (init.type !== "StringLiteral" && init.type !== "BooleanLiteral" && init.type !== "NumericLiteral" && init.type !== "BigIntLiteral" && (init.type !== "TemplateLiteral" || init.expressions.length > 0) && !isPossiblyLiteralEnum(init)) {
this.raise(TSErrors.ConstInitiailizerMustBeStringOrNumericLiteralOrLiteralEnumReference, {
at: init
});
}
}
return declaration;
}
经过这一步的处理,最终 const a = 1 会变成如下ast结构:
字节码生成器的作用,是将抽象语法树转为JavaScript引擎可以执行的二进制代码。目前,还没有统一的JavaScript字节码的格式标准,每种JavaScript引擎都有自己的字节码格式。最简单的做法,就是将语义单位翻成对应的二进制命令。
字节码解释器的作用是读取并执行字节码。
这是第一个用JavaScript编写的符合EsTree规范的JavaScript的解析器,后续多个编译器都是受它的影响
acorn 和 Esprima 很类似,输出的ast都是符合 EsTree 规范的,目前webpack的AST解析器用的就是acorn
babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大
用于混淆和压缩代码,因为一些原因,uglify-js自己[内部实现了一套AST规范,也正是因为它的AST是自创的,不是标准的ESTree,es6以后新语法的AST,都不支持,所以没有办法压缩最新的es6的代码,如果需要压缩,可以用类似babel这样的工具先转换成ES5。
esbuild是用go编写的下一代web打包工具,它拥有目前最快的打包记录和压缩记录,snowpack和vite的也是使用它来做打包工具,为了追求卓越的性能,目前没有将AST进行暴露,也无法修改AST,无法用作解析对应的JavaScript。
下面先介绍下babel相关工具库,以及一些API,了解完这些基础概念后,我们会利用这些工具操作AST来编写一个babel插件。
• scope.bindings 当前作用域内声明所有变量 • scope.path 生成作用域的节点对应的路径 • scope.references 所有的变量引用的路径 • getAllBindings() 获取从当前作用域一直到根作用域的集合 • getBinding(name) 从当前作用域到根使用域查找变量 • getOwnBinding(name) 在当前作用域查找变量 • parentHasBinding(name, noGlobals) 从当前父作用域到根使用域查找变量 • removeBinding(name) 删除变量 • hasBinding(name, noGlobals) 判断是否包含变量 • moveBindingTo(name, scope) 把当前作用域的变量移动到其它作用域中 • generateUid(name) 生成作用域中的唯一变量名,如果变量名被占用就在前面加下划线 • scope.dump() 打印自底向上的 作用域与变量信息到控制台
上面这些概念在我们编写babel插件时会用到,接下来我们来实现一个eslint移除console.log()插件吧! 先看下 console.log('a') 的AST长什么样子?
可以看到 console.log('a') 是一个type为 “ExpressionStatement”的节点,所以我们只需要遍历AST,当遇到type=ExpressionStatement的节点删除即可!来一起实现吧~
1 . 引入基础包
var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
2. 遍历AST节点,babel插件的语法都是固定的,里面包含visitor,我们只需在visitor里面处理ExpressionStatement即可,
//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
// babel插件的语法都是固定的,里面包含visitor
return {
pre(file) {
file.set('errors', []);
},
// 访问器
visitor: {
CallExpression(path, state) {
const errors = state.file.get('errors');
const { node } = path
if (node.callee.object && node.callee.object.name === 'console') {
errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
if (fix) {
path.parentPath.remove();
}
}
}
},
post(file) {
// console.log(...file.get('errors'));
}
}
};
3 . 完整实现:
var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
// babel插件的语法都是固定的,里面包含visitor
return {
pre(file) {
file.set('errors', []);
},
// 访问器
visitor: {
CallExpression(path, state) {
const errors = state.file.get('errors');
const { node } = path
if (node.callee.object && node.callee.object.name === 'console') {
errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
if (fix) {
path.parentPath.remove();
}
}
}
},
post(file) {
// console.log(...file.get('errors'));
}
}
};
core.transformFile("eslint/source.js",{
plugins: [eslintPlugin({ fix: false })]
}, function(err, result) {
result; // => { code, map, ast }
console.log(result.code);
})
- END -
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8