背景
Javascript
属于web编程语言,本身语法设计存在缺陷,随着ES5、ES6、ES7等各个版本的推出,语法不断完善,但浏览器的兼容问题成为了目前最大的痛点,typescript的相继推出在一定程度上弥补了js语法本身的不足,但不能被浏览器识别,那么如何将ts
、jsx
、es6+
这些代这些代码转换成浏览器可识别的代码呢?Babel
的出现解决了这一问题。
什么是Babel
Babel 是一个 JavaScript 编译器
Babel 是一个JavaScript编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel能够实现的功能如下:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
- 源码转换 (codemods)
- 代码压缩混淆
- ....
执行原理
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gq9x67ypxuj314205kjs5.jpg" alt="image-20210507155904069" style="zoom:50%;" />
parser解析
Babel会将拿到的源码提取出来,以一种抽象的树状语法结构表示,该结构被称为抽象语法树AST,全称为: Abstract Syntax Tree。当前解析阶段主要分为两个步骤:**词法分析 **和 语法分析,
词法分析
将代码字符串转换成Token流(即将源码分割成最小单元),可以将Token流理解成一种扁平的语法片段数组,数组中的每一项都有一组属性来描述该Token。
代码中的语法单元主要包括以下几类:
关键字(let、const等)、标识符、运算符、数字、空格、注释;
根据语法类型,将源码字符串按指定的规则转换成数组片段,如下:
[
{
"type" : "Identifier" ,
"value" : "console"
},
{
"type" : "Punctuator" ,
"value" : "."
},
{
"type" : "Identifier" ,
"value" : "log"
},
{
"type" : "Punctuator" ,
"value" : "("
},
{
"type" : "String" ,
"value" : "'zcy'"
},
{
"type" : "Punctuator" ,
"value" : ")"
},
{
"type" : "Punctuator" ,
"value" : ";"
}
]
语法分析
该阶段会将Token流转换为AST形式,将Token中的描述属性添加到AST语法结构中,方便后期操作语法树。
在babel中,将源代码转换成AST语法树的插件是@babel/parser
转换前:
const name = 'Jack';
const list = ['Jack','Joe'];
list.map(val=>{
if( val == name ){
alert('找到了')
}
})
转换后:
当前AST描述了每一部分代码以及它们之间的关系:其中VariableDeclaration
表示声明变量,kind
表示声明类型如:const、let、var,function函数都属于ExpressionStatement
类别;
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gq9xzxxw78j30uk0u0n2w.jpg" alt="image-20210507162740302" style="zoom:50%;" />
transform语法转换
该步骤实现了对AST语法树的遍历,对AST节点的操作,包括增加、删除、修改等,此时就需要一系列具备相应功能的插件来进行转换操作,例如ES6转ES5、jsx语法转js等;
当源代码通过@babel/parser 转换成AST后,通过配置一系列的plugins
和presets
转换成新的AST语法,
plugins
@babel/traverse
其中plugins
包括:@babel/traverse
,该插件是一个对AST语法遍历的工具:
以下是AST语法树中import导入方法的节点描述信息
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gqtd4gdjoyj30tk0rw41s.jpg" alt="image-20210524113640574" style="zoom:50%;" />
使用traverse工具遍历查找
const traverse = require('babel-traverse').default
// 遍历查找指定节点
traverse(astContext, {
// 查找import导入
ImportDeclaration: ({ node }) => {
if (node.source.value.match(regRules)) {
node.specifiers && node.specifiers.map((item,index)=>{
if( item.imported&&item.imported.name ){
}
})
}
}
});
traverse(ast, {
enter(path){
// isExpressionStatement查询符合条件的表达式节点
if(path.isExpressionStatement(){
console.log(path.node.start)
}
// isIdentifier查询符合条件的节点
if(path.isIdentifier({name: 'test'})){
console.dir(path.node)
}
if(){
}
}
})
presets
其中presets
主要包括:
es2015 / es2016 / es2017 / env / stage-0 / stage-4,其中esXX表示转换成指定年份标准,env是最新标准,stage 系列集合了一些对 es7 的草案支持的插件,由于是草案,所以作为插件的形式提供。
以上plugins
和presets
主要是.babelrc配置文件中的参数;
ES各版本语法间的转换
babel
将ES6及以上版本分为语法层
和api层:
语法层:let、const、class、=>等需要在语法层面上进行转义,Babel针对每一个新语法都提供了相应的转换插件,命名格式为:@babel/plugin-xxx
,为避免一个一个的配置,通常使用 @babel/preset-env
插件集,将新语法转换为var,function等;
api层:Promise、includes、map等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重
新定义,需要使用@babel/polyfill
(babel7.4后废弃,由 core-js 替代)进行单独转译;
转换前:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x},${this.y})`;
}
}
转换后:
"use strict";
var Point = /*#__PURE__*/function () {
function Point(x, y) {
_classCallCheck(this, Point);
this.x = x;
this.y = y;
}
_createClass(Point, [{
key: "toString",
value: function toString() {
return "(".concat(this.x, ",").concat(this.y, ")");
}
}]);
return Point;
}();
generat生成器
在transform阶段实现的是对ast代码节点的替换和重组,接下来进入generat阶段,@babel/generator模块是代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
import { parse } from "@babel/parser";
import generate from "@babel/generator";
const code = "class Example {}";
const ast = parse(code);
const output = generate(
ast,
{
/* options */
},
code
);
如何编写Babel插件
Babel插件基础结构:
// 该function传入babel对象或者babel.types
export default function({ types: t }) {
return {
visitor: {
Identifier(path, state) {},
ImportDeclaration(path, state) {}
}
};
};
参数详解:
- types:传入的是babel.types,该工具集主要负责AST节点操作
- visitor:babel通过递归的方式访问节点,该属性为插件的主要访问者
- path:是代表AST节点之间关联关系的一个对象,可以通过path访问节点属性
- state:插件状态,可访问插件配置项
- Identifier,ImportDeclaration:AST的节点类型,主要包括标识符(Identifier)、函数声明(FunctionDeclaration)、表达式(VariableDeclaration)等
了解了Bable插件的基本结构,下面我们来写一个简单的变量名替换插件:将代码中变量名为a的统一替换成b;
我们先来看下转化后的语法树结构:
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gqtlwq2kuyj30qy0jqabn.jpg" alt="image-20210524164039276" style="zoom:50%;" />
变量方法如下:
//plugin.js
export default function({ types: t }) {
return {
visitor: {
VariableDeclarator(path, state) {
if (path.node.id.name == 'a') {
path.node.id = t.identifier('b')
}
}
}
}
}
调用代码:
// index.js
var a = "Hello!";
转换命令:
npx babel --plugins ./plugin.js index.js
Babel常用包及功能
@babel/core 核心编译器,包含了@babel/parser
、@babel/traverse
、@babel/generator
。
@babel/parser 将源码转换成AST语法树
@babel/traverse 遍历AST节点,实现对节点的增删改查
@babel/generator 将修改后的AST语法树编译成代码字符串
@babel/cli 终端命令集
@babel/preset-env 环境预设,包含了es2015,es2016, es2017等最新的语法转化插件
@babel/polyfill 兼容JS高本版全局或者Object、Array等的原型部分语法
总结
通过对Babel插件的学习,我们了解了Babel的编译原理,以及如何手动实现一个Babel插件,希望能够帮助我们更深入的去学习这一系列JS工具,在今后的项目开发过程中能够结合市面上已有工具来定制化开发指定功能的插件。