# babel使用及分析
参考资料
babel是一个js编译器,是一个工具链,用于将es2015+版本的代码转换为向后兼容的js语法,可以做:
- 语法转换
- 添加目标环境中缺少的polyfill功能
- 源代码转换
提供了插件功能,一切功能都可以以插件来实现,方便使用和弃用。
# 1、工具包
- @babel/parser : 转化为 AST 抽象语法树;
- @babel/traverse 对 AST 节点进行递归遍历;
- @babel/generator : AST抽象语法树生成为新的代码
- @babel/core:内部核心的编译和生成代码的方法,上面三个集合,一般使用这个包
- @babel/types:判断ast节点类型以及创建新节点的工具类
- @babel/cli:babel命令行工具内部解析相关方法
- @babel/preset-env:babel编译结果预设值,使用can i use网站作为基设
- @babel/polyfill:es6语法的补丁,安装了所有符合规范的 polyfifill 之后,我们需要在组件引用这个模块,就能正常的使用规范中定义的方法了。
# 2、使用
- 安装@babel/core和@babel/cli即可使用命令行解析工具
- 输出编译代码compile: babel index.js -o output.js
- 使用preset预设,配置.babelrc中presets属性,适用于语法层面范畴
- 使用polyfill,需要在代码中引入polyfill模块,给所有方法打补丁,保证运行正常,适用于方法层面,polyfill通常需要--save,其他使用--save-dev即可
- babel执行顺序:plugins先执行、再执行预设presets
几个重要概念:
- preset:预设,是一组用于支持特定语言功能的插件,主要用于对语法进行转换
- polyfill:给方法打补丁,保证运行正常,适用于方法层面
- transform-runtime:将api进行私有化,防止引入外部库冲突,eg:_promise
//presets预设使用
//index.js
const func = () => console.log("hello es6");
const { a, b = 1 } = { a: "this is a" };
//.babelrc配置,presets预设1
{
"presets": [
"@babel/preset-env"
]
}
//输出
"use strict";
var func = function func() {
return console.log("hello es6");
};
var _a = {
a: "this is a"
},
a = _a.a,
_a$b = _a.b,
b = _a$b === void 0 ? 1 : _a$b;
//.babelrc配置,,presets预设2
{
"presets": [
["@babel/preset-env",{
"targets": ">1.5%"
}]
]
}
//输出:箭头函数和解构未转换
"use strict";
const func = () => console.log("hello es6");
const {
a,
b = 1
} = {
a: "this is a"
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//polyfill使用
import "@babel/polyfill";
const array = [1, 2, 3];
console.log(array.includes(2));
//输出
"use strict";
require("@babel/polyfill"); //加载了全部polyfill
const array = [1, 2, 3];
console.log(array.includes(2));
//按需加载
//.babelrc配置
{
"presets": [
["@babel/preset-env",{
"targets": ">1.5%",
"useBuiltIns": "usage", //按需加载
"corejs": 3 //指定corejs版本
}]
]
}
//index.js,去除import
const array = [1, 2, 3];
console.log(array.includes(2));
//输出
"use strict";
require("core-js/modules/es.array.includes.js");
var array = [1, 2, 3];
console.log(array.includes(2));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
解析:@babel/preset-env中useBuiltIns 说明
- false:此时不对
polyfill
做操作。如果引入@babel/polyfill
,则无视配置的浏览器兼容,引入所有的polyfill
,默认选项 - entry:根据配置的浏览器兼容,引入浏览器不兼容的
polyfill
。需要在入口文件手动添加import '@babel/polyfill'
,会自动根据browserslist
替换成浏览器不兼容的所有polyfill
,这里需要指定core-js
的版本 - usage:会根据配置的浏览器兼容,以及你代码中用到的 API 来进行
polyfill
,实现了按需添加
# 3、babel处理步骤
- 解析:接收代码并输出AST(抽象语法树)
- 词法分析:把字符串形式的代码转换为令牌(tokens)流,令牌看作是一个扁平的语法片段数组
- 语法分析:把一个令牌流转换为AST,使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作
- 转换:接收AST并对其遍历,在此过程中对节点进行添加、更新和移除等操作。这是Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分
- 生成:把最终的AST转换成字符串形式的代码,同时创建源码映射(source maps)。代码生成过程:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串
# 4、手写babel原理
(add 2 (subtract 40 2)) 编译成 add(2, subtract(40, 2))
静态编译:字符串 -> 字符串
思路:正则匹配、状态机、编译器处理流程(解析、转换、生成)
- 分词:将表达式分词,水平状态
/*
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '40' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' }
]
*/
function generateToken(str){
let current = 0 //下标
let tokens = [] //记录分词列表
while(current < str.length){
let char = str[current]
//括号分词:记录为词语
if(char === '('){
tokens.push({ //末尾添加对象返回长度,pop删除数组最后一项,返回元素,栈方法FILO
type: 'paren',
value:'('
})
current++
continue;
}
//括号分词:记录为词语
if(char === ')'){
tokens.push({
type: 'paren',
value: ')'
})
current++
continue;
}
//空格分词:直接跳过
if(/\s/.test(char)){
current++
continue
}
//数字分词:二次遍历
if(/[0-9]/.test(char)){
let numberValue = ''
while(/[0-9]/.test(char)){
numberValue += char
char = str[++current]
}
tokens.push({
type: 'number',
value: numberValue
})
continue
}
//字符串分词
if(/[a-z]/.test(char)){
let strValue = ''
while(/[a-z]/.test(char)){
strValue += char
char = str[++current]
}
tokens.push({
type: 'name',
value: strValue
})
continue
}
throw new TypeError('type error')
}
return tokens
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
- 生成ast:垂直结构,estree规范
/* json.cn可查看json信息
{
"type":"Program",
"body":[
{
"type":"CallExpression",
"name":"add",
"params":[
{
"type":"NumberLiteral",
"value":"2"
},
{
"type":"CallExpression",
"name":"subtract",
"params":[
{
"type":"NumberLiteral",
"value":"40"
},
{
"type":"NumberLiteral",
"value":"2"
}
]
}
]
}
]
}
*/
function generateAST(tokens){
let current = 0
let ast = {
type: 'Program',
body: []
}
//闭包处理
function walk(){
let token = tokens[current];
if(token.type === 'number'){
current++
return {
type: 'NumberLiteral',
value: token.value
}
}
//左括号为层级开始,为执行语句
if(token.type === 'paren' && token.value === '('){
token = tokens[++current]
let node = {
type: 'CallExpression',
name: token.value,
params: []
}
token = tokens[++current]
while(
(token.type !== 'paren')||(token.type === 'paren' && token.value !== ')')
){
node.params.push(walk()) //递归调用
token = tokens[current] //取当前值即可,walk()里完成指针移动
}
current++
return node
}
}
while(current < tokens.length){
ast.body.push(walk())
}
return ast
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
- 遍历ast,转化为新的ast
/*
{
"type":"Program",
"body":[
{
"type":"ExpressionStatement",
"expression":{
"type":"CallExpression",
"callee":{
"type":"Identifier",
"name":"add"
},
"arguments":[
{
"type":"NumberLiteral",
"value":"2"
},
{
"type":"CallExpression",
"callee":{
"type":"Identifier",
"name":"subtract"
},
"arguments":[
{
"type":"NumberLiteral",
"value":"40"
},
{
"type":"NumberLiteral",
"value":"2"
}
]
}
]
}
}
]
}*/
function transformer(ast){
let newAst = {
type: 'Program',
body: []
}
ast._context = newAst.body //ast子元素挂载
//类似于babel插件功能
DFS(ast, {//生命周期,enter、exit
NumberLiteral: {
enter(node, parent){
//父元素记录子元素值,为父元素CallExpression做准备
parent._context.push({
type: "NumberLiteral",
value: node.value
})
}
},
//NumberLiteral的父元素为CallExpression
CallExpression: {
enter(node, parent){
let expression = {
type: 'CallExpression',
callee: {
type: "Identifier",
name: node.name
},
arguments:[]
}
//a.子元素值赋值到父亲元素的arguments去
node._context = expression.arguments
//b.二次操作
if(parent.type !== "CallExpression"){
expression = {
type: "ExpressionStatement",
expression: expression
}
}
parent._context.push(expression)
}
}
})
return newAst
}
function DFS(ast, visitor){
//遍历子元素数组
function traverseArray(children, parent){
children.forEach(child => tranverseNode(child, parent))
}
function tranverseNode(node, parent){
let methods = visitor[node.type]
if(methods && methods.enter){
methods.enter(node, parent)
}
switch(node.type){
case "Program"://子元素body,父元素node
traverseArray(node.body, node)
break
case "CallExpression"://子元素params,父元素node
traverseArray(node.params, node)
break;
case "NumberLiteral":
break;
default:
break;
}
if(methods && methods.exit){
methods.exit(node, parent)
}
}
return tranverseNode(ast, null)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
- 基于ast,生成代码
//add(2, subtract(40, 2));
function generate(ast){
switch(ast.type){
case "Identifier": return ast.name;
case "NumberLiteral": return ast.value;
//每个子元素一行展示
case "Program": return ast.body.map(subAst => generate(subAst)).join('\n')
case "ExpressionStatement": return generate(ast.expression) + ";"
//函数调用形式 add (参数, 参数, 参数)
case "CallExpression": return generate(ast.callee) + "(" + ast.arguments.map(arg => generate(arg)).join(', ') + ")"
default: break
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 5、插件添加及使用
# 1、类型
babel插件分为语法插件和转换插件:
- 语法插件:syntax plugin,在@bable/parser中加载,在parser过程中执行的插件,例如:@babel/plugin-syntax-jsx
- 转换插件:transform plugin,在@babel/transform中加载,在transform过程中执行的插件
# 2、插件思路
- 做什么插件:自己做什么事情以及受益
- 分析ast:比对原始数据与最终转换为的数据两个ast的不同,来找到所需操作的transform方法
- 参考手册进行开发:babel官网插件开发手册、@babel/types手册、estree规范手册
# 3、相关概念
babel插件为一个函数或者对象,若为函数:入参使用types对象,出参一个对象,输出对象中有visitor属性。
types对象:拥有每个单一类型节点的定义,包括节点的属性、遍历等信息。
visitor:插件的主要访问者,visitor是一个对象,包含各种类型节点的访问函数,接收state和path参数
path:表示两个节点之间连接的对象,这个对象包含当前节点和父节点的信息以及添加、修改、删除节点有关的方法
- 属性
- node:当前节点
- parent:父节点
- parentPath:父path
- scope:作用域
- context:上下文
- 方法
- get:获取当前节点
- getSibling:获取兄弟节点
- findParent:向父节点搜寻节点
- replaceWith:用ast节点替换该节点
- replaceWithMultiple:用多个ast节点替换该节点
- insertBefore:在节点前插入节点
- insertAfter:在节点后插入节点
- remove:删除节点
- 属性
state:visitor对象中每次访问节点方法时传入的第二个参数,包含当前plugin的信息、scope作用域信息、plugin传入的配置参数信息,当前节点的path信息。可以把babel插件处理过程中的自定义状态存储到state对象中。
Scopes:与js中作用域类似,如函数内外的同名变量需要区分开来。
Bindings:所有引用属于特定的作用域,引用和作用域的这种关系称作为绑定。
# 4、实战
- 将字符串中的+转换为-操作符号:
//input.js
1 + 1;
//output.js
1 - 1;
//plugin.js
export default function({types: t}){
return{
visitor:{
BinaryExpression(path){
path.node.operator = "-";
}
}
}
}
//.babelrc
{
"plugins": [
["./plugin"]
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 去除代码中的console函数调用
const { transform } = require("@babel/core");
const test = "const a = 1; console.log('woshi');let b = 2; console.log('haha');let c=3"
const myPlugins={
name:'myPlugins',
visitor:{
CallExpression(path){
if(path.get('callee').isMemberExpression()){
if(path.get('callee').get('object').isIdentifier()){
if(path.get('callee').get('object').get('name').node == 'console'){
path.remove()
}
}
}
}
}
}
var newCode = transform(test,{
plugins:[myPlugins]
})
console.log(newCode.code)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
3、扩展场景:组件按需引用,提升LCP
import {button, nav} from "elementUi"
// 转换为:import button from 具体路径
1
2
2