# babel使用及分析

参考资料

1、ast查看链接 (opens new window)

2、bable官网 (opens new window)

3、AST详解与运用 (opens new window)

4、babel插件说明 (opens new window)

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
//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

解析:@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
  • 生成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
  • 遍历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
  • 基于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

# 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、实战

  1. 将字符串中的+转换为-操作符号:
//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
  1. 去除代码中的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

3、扩展场景:组件按需引用,提升LCP

import {button, nav} from "elementUi"
// 转换为:import button from 具体路径
1
2
Last Updated: 9/20/2021, 6:38:58 PM