[TOC]

写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址

babel概述

babel可以看作是一个转换器,把一些代码转成浏览器可以运行的代码。其转换过程可以分为3个步骤:

  1. 源码解析生成AST:将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon实现的。在解析过程中有两个阶段:词法分析和语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构
  2. AST转换成新的AST:在这个阶段,Babel接受得到的AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分。
  3. 用新的AST生成新的代码:将经过转换的AST通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。

首先我们来学习AST相关的知识:

AST(Abstract Syntax Tree)

抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树状表现形式。webpack、eslint 等很多工具库的核心都是通过抽象语法书这个概念来实现对代码的检查、分析等操作。这里介绍一下JavaScript这类解释型语言的抽象语法树的概念。

image-20200628111500626

如上图中变量声明语句,转换为 AST 之后就是右图中显示的样式

左图中对应的:

  • var 是一个关键字
  • AST 是一个标识符
  • = 是 Equal 等号的叫法有很多形式,在后面我们还会看到
  • is tree 是一个字符串
  • ; 就是 Semicoion

首先一段代码转换成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program;第二个属性是 body 是一个数组。

body 数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息

type:         描述该语句的类型  --> 变量声明的语句
kind:         变量声明的关键字  --> var
declaration:  声明内容的数组,里面每一项也是一个对象
            type: 描述该语句的类型
            id:   描述变量名称的对象
                type: 定义
                name: 变量的名字
            init: 初始化变量值的对象
                type:   类型
                value:  值 "is tree" 不带引号
                row:    "\"is tree"\" 带引号
1
2
3
4
5
6
7
8
9
10
11

词法分析和语法分析

JavaScript是解释型语言,一般通过词法分析 -> 语法分析 -> 语法树,就可以开始解释执行了

词法分析:也叫扫描,是将字符流转换为记号流(tokens),它会读取我们的代码然后按照一定的规则合成一个个的标识

比如说:var a = 2,这段代码通常会被分解成var、a、=、2

[
  { type: 'Keyword', value: 'var' },
  { type: 'Identifier', value: 'a' },
  { type: 'Punctuator', value: '=' },
  { type: 'Numeric', value: '2' },
]
1
2
3
4
5
6

当词法分析源代码的时候,它会一个一个字符的读取代码,所以很形象地称之为扫描 - scans。当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。

语法分析:也称解析器,将词法分析出来的数组转换成树的形式,同时验证语法。语法如果有错的话,抛出语法错误。

{
  ...
  "type": "VariableDeclarator",
  "id": {
    "type": "Identifier",
    "name": "a"
  },
  ...
}
1
2
3
4
5
6
7
8
9

语法分析成AST,我们可以在这里在线看到效果http://esprima.org

AST能做什么

  • 语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等
  • 代码混淆压缩
  • 优化变更代码,改变代码结构等

比如说,有个函数 function a() {} 我想把它变成 function b() {}

比如说,在 webpack 中代码编译完成后require('a') --> __webapck__require__("*/**/a.js")

简单应用-修改函数名字

const parser = require('@babel/parser');
const type = require('@babel/types');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;

const code = 'function fn(a) {}';
// 解析代码转为ast
const ast = parser.parse(code);
const myVisitor = {
    FunctionDeclaration(path) {
        // path.node.id.name = 'test';
        // 替换id
        path.node.id = type.identifier('test');
        // console.log(path.node.params);
        // 追加函数参数
        // path.node.params.push(type.identifier('b'), type.identifier('c'));
        // 更改函数参数
        path.node.params = [type.identifier('b'), type.identifier('c')];
    }
};

traverse(ast, myVisitor);
const res = generator(ast);
// console.log(res);
console.log(res.code);
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

我们常用的浏览器就是通过将 js 代码转化为抽象语法树来进行下一步的分析等其他操作。所以将 js 转化为抽象语法树更利于程序的分析。

我们知道,babel的作用其实就是一个转换器,把我们的代码转成浏览器可以运行的代码。编译代码都是一个文件一个文件的处理,把代码读出来,然后经过处理,再输出,在处理的过程中每个文件的代码其实就是个大的字符串。但是我们要把有些语法修改,比如let定义变量改成var定义,很明显用字符串替换是不现实的,这里babel是把代码转成ast语法树,然后经过一系列操作之后再转成字符串输出。

访问者模式

访问者模式是一种将算法与对象结构分离的软件设计模式。

这个模式的基本想法如下:首先我们拥有一个由许多对象构成的对象结构,这些对象的类都拥有一个accept方法用来接受访问者对象;访问者是一个接口,它拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素作出不同的反应;在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施accept方法,在每一个元素的accept方法中回调访问者的visit方法,从而使访问者得以处理对象结构的每一个元素。我们可以针对对象结构设计不同的访问者类来完成不同的操作。———— 维基百科

具体来说,我们的AST的每一个Node有一个accept方法,当我们用一个visitor来遍历我们的AST时,每遍历到一个 Node 就会调用这个 Node 的 accept 方法来接待这个visitor,而在 accept 方法内,我们会回调 visitor 的 visit 方法。我们来用访问者模式来实现一个旅行者访问城市景点的逻辑。

实际上Node是有两个方法,enter和exit,指遍历进入和离开 Node 的时候。通常访问者的 visit 方法会在 enter 内被调用

Visitors(访问者)

当我们谈及“进入”一个节点,实际上是说我们在访问它们,之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};
1
2
3
4
5

注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的简写形式。

这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。

所以在下面的代码中 Identifier() 方法会被调用四次(包括square在内,总共有四个Identifier)。)

function square(n) {
  return n * n;
}
1
2
3
Called!
Called!
Called!
Called!
1
2
3
4

这些调用都发生在进入节点时,不过有时候我们也可以在退出时调用访问者方法。

假设我们有一个树状结构:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)
1
2
3
4
5
6
7
8

当我们向下遍历这颗树的每一个分支时我们最终会走到尽头,于是我们需要往上遍历回去从而获取到下一个节点。 向下遍历这棵树我们进入每个节点,向上遍历回去时我们退出每个节点。

让我们以上面那棵树为例子走一遍这个过程(深度优先遍历)。

  • 进入 FunctionDeclaration
    • 进入 Identifier (id)
    • 走到尽头
    • 退出 Identifier (id)
    • 进入 Identifier (params[0])
    • 走到尽头
    • 退出 Identifier (params[0])
    • 进入 BlockStatement (body)
      • 进入 ReturnStatement (body)
      • 进入 BinaryExpression (argument)
      • 进入 Identifier (left)
        • 走到尽头
      • 退出 Identifier (left)
      • 进入 Identifier (right)
        • 走到尽头
      • 退出 Identifier (right)
      • 退出 BinaryExpression (argument)
    • 退出 ReturnStatement (body)
    • 退出 BlockStatement (body)
  • 退出 FunctionDeclaration

所以当创建访问者时你实际上有两次机会来访问一个节点。

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};
1
2
3
4
5
6
7
8
9
10
const esprima = require('esprima');
const estraverse = require('estraverse');
const code = 'function ast(a) {}';
// 将代码转为AST
const ast = esprima.parse(code);
// 转换 AST,只会遍历 type 属性
// traverse 方法中有进入和离开两个钩子函数
estraverse.traverse(ast, {
    enter(node) {
        console.log('enter', node.type);
    },
    leave(node) {
        console.log('leave', node.type);
    }
});
// console.log(ast);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enter Program
enter FunctionDeclaration
enter Identifier
leave Identifier
enter Identifier
leave Identifier
enter BlockStatement
leave BlockStatement
leave FunctionDeclaration
leave Program
1
2
3
4
5
6
7
8
9
10

由此可以得到 AST 遍历的流程是深度优先遍历。

遍历

想要转换AST你需要进行递归的树形遍历。

比如:我们有一个FunctionDeclaration类型。它有几个属性:id,params和body,每一个都有一些内嵌节点。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}
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

Babel 工作原理

提到 AST 我们肯定会想到 babel,自从 Es6 开始大规模使用以来,babel 就出现了,它主要解决了就是一些浏览器不兼容 ES6 新特性的问题,其实就把 ES6 代码转换为 ES5 的代码,兼容所有浏览器,babel 转换代码其实就是用了 AST,babel 与 AST 就有着很一种特别的关系。

那么我们就在 babel 的中来使用 AST,看看 babel 是如何编译代码的,需要用到两个工具包@babel/core、@babel/preset-env

当我们配置 babel 的时候,不管是在.babelrc 或者 babel.config.js文件里面配置的都有 presets 和 plugins 两个配置项。

插件和预设的区别

// .babelrc
{
  "presets": ["@babel/preset-env"],
  "plugins": []
}
1
2
3
4
5

当我们配置了 presets 中有 @babel/preset-env,那么 @babel/core 就会去找 preset-env 预设的插件包,它是一套插件的集合。

需要注意:babel核心包并不会去转换代码,核心包只提供一些核心 API,真正的代码转换工作由插件或者预设来完成。比如要转换箭头函数,会用到这个plugin,@babel/plugin-transform-arrow-functions,当需要转换的要求增加时,我们不可能去一一配置相应的 plugin,这个时候就可以用到预设了,也就是 presets。presets 是 plugins 的集合,一个 presets 内部包含了很多 plugin。

babel插件的使用

现在我们有一个箭头函数,要想把它转成普通函数,我们就可以直接这么写:

const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b`
// babel 有 transform 方法会帮我们自动遍历,使用相应的预设或者插件转换相应的代码
const r = babel.transform(code, {
  presets: ['@babel/preset-env'],
})
console.log(r.code)
// 打印结果如下
// "use strict";
// var fn = function fn() { return a + b; };
1
2
3
4
5
6
7
8
9
10

此时我们可以看到最终代码会被转成普通函数,但是我们,只需要箭头函数转通函数的功能,不需要用这么大一套包,只需要一个箭头函数转普通函数的包,我们其实是可以在 node_modules 下面找到有个叫做plugin-transform-arrow-functions的插件,这个插件是专门用来处理 箭头函数的,我们就可以这么写:

const r = babel.transform(code, {
  plugins: ['@babel/plugin-transform-arrow-functions'],
})
console.log(r.code)
// 打印结果如下
// const fn = function () { return a + b; };
1
2
3
4
5
6

我们可以从打印结果发现此时并没有转换我们变量的声明方式还是 const 声明,只是转换了箭头函数

编写自己的插件

实现箭头函数转换插件

  • @Babel/type:类似lodash那样的工具集,主要用来操作AST节点,比如创建、校验、转变等。举例:判断某个节点是不是标识符(identifier)。
  • path:AST中有很多节点,每个节点可能有不同的属性,并且节点之间可能存在关联。path是个对象,它代表了两个节点之间的关联。你可以在path上访问到节点的属性,也可以通过path来访问到关联的节点(比如父节点、兄弟节点等)
  • state:代表了插件的状态,你可以通过state来访问插件的配置项。
  • visitor:Babel采取递归的方式访问AST的每个节点,之所以叫做visitor,只是因为有个类似的设计模式叫做访问者模式,不用在意背后的细节。
  • Identifier:AST的每个节点,都有对应的节点类型,比如标识符(Identifier)、函数声明(FunctionDeclaration)等,可以在visitor上声明同名的属性,当Babel遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是path、state。

插件功能:

  1. 把箭头函数转换成普通function;
  2. const转换成var。

分析AST结构

// 源代码为:
const fn = (a, b) => a + b
// 转换后的结果如下:
var fn = function fn(a, b) {
  return a + b;
};
1
2
3
4
5
6

首先我们通过astexplorer将源代码转为AST,分析const fn = (a, b) => a + bconst fn = function(a, b) { return a + b }看两者语法树的区别,如下图所示:

从上图中可以发现:每个节点都有一个type字段,代表当前节点的类型,(如:"FunctionDeclaration","Identifier",或 "BinaryExpression")。AST的节点类型有很多,更多的type可以到这里节点类型查看。

实现插件核心方法和工具

visitor

visitor对象简单理解就是一些监听函数的集合,当babel在处理AST的每个节点时,如果在visitor中存在声明某个节点类型的方法,那么当babel处理AST此类型节点时就会执行对应的方法,举个🌰:

visitor: {
    Identifier(path, state) { // 当节点类型为identifier时,就会执行该方法
        console.log('Called!');
    }
}
1
2
3
4
5

如何添加visitor对象的节点监听方法?

我们可以将需要转换的源码放到astexplorer中转换成AST,然后找到对应节点的type字段,type字段的值就是就我们要在visitor对象里添加的方法名称。

Path

Path是一个对象,它表示两个节点之间的连接。在visitor对象声明的方法中,第一个参数是path,是捕获到的节点对应的信息,包含了当前节点的信息以及对节点的添加、更新、移动和删除等方法。更多详细信息见path源码

我们可以通过path.node获得这个节点的AST,在这个基础上进行修改就能完成了我们的目标。

── 属性
  - node   当前节点
  - parent  父节点
  - parentPath 父path
  - scope   作用域
  - context  上下文
  - ...
── 方法
  - get   当前节点
  - findParent  向父节点搜寻节点
  - getSibling 获取兄弟节点
  - replaceWith  用AST节点替换该节点
  - replaceWithMultiple 用多个AST节点替换该节点
  - insertBefore  在节点前插入节点
  - insertAfter 在节点后插入节点
  - remove   删除节点
  - ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@babel/types

@babel/types是用于处理AST节点的工具库,包含了构造、验证AST节点等方法。具体请参考@babel/types相关API

实现箭头函数转换为function函数

  1. 变成普通函数之后就不叫箭头函数了 ArrowFunctionExpression,而是函数表达式了 FunctionExpression
  2. 所以首先我们要把 箭头函数表达式(ArrowFunctionExpression) 转换为 函数表达式(FunctionExpression)
  3. 要把 二进制表达式(BinaryExpression) 放到一个 代码块中(BlockStatement)
  4. 其实我们要做就是把一棵树变成另外一颗树,说白了其实就是拼成另一颗树的结构,然后生成新的代码,就可以完成代码的转换

第一步:给visitor对象添加对应节点的监听方法。可以从AST中找到箭头函数类型的监听方法的写法,如下图所示:

从上图中可以看出箭头函数类型是ArrowFunctionExpression,所以visitor里的写法如下:

visitor: {
  ArrowFunctionExpression: (path, state) => {}
}
1
2
3

第二步:转换AST。要将箭头函数转换为function普通函数,需要使用@babel/types的function函数构造方法来构造一个function函数节点,然后将原来的箭头函数节点替换掉即可,具体使用方法见@babel/types

t.functionExpression(id, params, body, generator, async)
1
  • id: Identifier (default: null) id 可传递 null
  • params: Array (required) 函数参数,可以把之前的参数拿过来
  • body: BlockStatement (required) 函数体,接受一个 BlockStatement,这里需要生成一个
  • generator: boolean (default: false) 是否为 generator 函数,当然不是了
  • async: boolean (default: false) 是否为 async 函数,肯定不是了

实现const转换成var

第一步:给visitor对象添加对应节点的监听方法。可以从AST中找到变量声明的type为VariableDeclaration,即visitor对象对应的方法名。

visitor: {
  VariableDeclaration: (path, state) => {}
}
1
2
3

第二步:转换AST

如上图所示:VariableDeclaration类型的节点有一个变量kind标识着声明变量的方式,所以要实现将const转换为var,那可以直接在visitor里监听的VariableDeclaration方法中将该节点的kind属性赋值为var即可:

visitor: {
  VariableDeclaration: (path, state) => {
      const {node} = path;
      if (node.kind === 'const' || node.kind === 'let') {
          node.kind = 'var'; // 方式1:直接替换
      }
  }
}
1
2
3
4
5
6
7
8

除了上述直接替换的方式,我们也可以通过替换当前节点的方式来达到同样的效果。上文说过@babel/types用于处理AST节点的工具库,包含了构造、验证AST节点等方法。所以我们可以直接用@babel/types定义的构造方法来构造一个variableDeclaration节点,代码实现如下:

visitor: {
  VariableDeclaration: (path, state) => {
      const {node} = path;
      if (node.kind === 'const' || node.kind === 'let') {
          // 基于@babel/types的VariableDeclaration方法构造一个variableDeclaration节点
          const variableDeclaration = type.VariableDeclaration('var', node.declarations)
          // replaceWith为替换节点的方法
          path.replaceWith(variableDeclaration);
      }
  }
}
1
2
3
4
5
6
7
8
9
10
11

最终实现如下:

const babel = require('@babel/core');
const type = require('@babel/types');

const transformArrowFunction = {
    // 该visitor包含两个节点监听方法VariableDeclaration和ArrowFunctionExpression
    visitor: {
        // 将const和let转换为var
        // 每个节点都有一个type字段,type值为VariableDeclaration会被匹配成功
        VariableDeclaration: (path, state) => {
            const {node} = path;
            if (node.kind === 'const' || node.kind === 'let') {
                // node.kind = 'var'; // 方式1:直接替换
                // 方式2:采用replaceWith
                // 基于@babel/types的VariableDeclaration方法构造一个variableDeclaration节点
                const variableDeclaration = type.VariableDeclaration('var', node.declarations)
                // replaceWith为替换节点的方法
                path.replaceWith(variableDeclaration);
            }
        },
        // Visitor中的每个函数接收2个参数:path和state
        // path是表示两个节点之间连接的对象
        ArrowFunctionExpression: (path, state) => {
            // console.log(path);
            // node就是ArrowFunctionExpression匹配到的当前节点
            // parent是当前节点的父节点
            const {node, parent} = path;
            const id = parent.id;
            const params = node.params; // 获取参数
            // 判断是不是 blockStatement,不是的话让他变成 blockStatement
            if (!t.isBlockStatement(node.body)) {
                // 将BinaryExpression转换为BlockStatement
                const body = type.blockStatement([
                    // node.body原来是a+b,即BinaryExpression
                    type.returnStatement(node.body)
                ]);
            }
            // 生成对应的functionExpression
            const functionExpression = type.functionExpression(id, params, body, false, false);
            // 节点替换,将匹配到的ArrowFunctionExpression替换为新生成的FunctionExpression
            path.replaceWith(functionExpression);
        }
    }
};

const code = 'const fn = (a, b) => a + b';
const result = babel.transform(code, {
    plugins: [
        transformArrowFunction
    ]
});

console.log(result.code);
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

预计算插件

const babel = require('@babel/core');
const type = require('@babel/types');
const code = 'const num = 2 * 3 * 4 * 5';

const preCalculatePlugin = {
    visitor: {
        BinaryExpression: (path, state) => {
            const node = path.node;
            const {left, right, operator} = node;
            if (!isNaN(left.value) && !isNaN(right.value)) {
                let result = eval(left.value + operator + right.value);
                result = type.numericLiteral(result);
                path.replaceWith(result);
                // 如果当前节点的父节点也是表达式的话,需要递归计算
                if (path.parentPath.node.type === 'BinaryExpression') {
                    preCalculatePlugin.visitor.BinaryExpression.call(null, path.parentPath);
                }
            }
        }
    }
}

const res = babel.transform(code, {
    plugins: [preCalculatePlugin]
});

console.log(res.code); // const num = 120;
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

实现按需加载插件

默认导入不做处理

上图导入的方式为默认导入,不做处理。

// 判断是否是默认导入
types.isImportDefaultSpecifier(specifiers[0]))
1
2

从上面两个图中,可以看出两种导入方式的区别在于specifiers的类型不同。因此,只要重新生成newImportSpecifiers即可,最终代码实现如下:

const types = require('@babel/types');

const visitor = {
    // 这里的ref是ImportDeclaration的第二个参数,值是.babelrc中的{"library": "lodash"}
    ImportDeclaration(path, ref = {opts: {}}) {
        const {opts} = ref;
        const node = path.node; // 拿到当前节点
        const specifiers = node.specifiers;
        // isImportDefaultSpecifier判断是否是默认导入,是的话不做处理
        if (opts.library === node.source.value && !types.isImportDefaultSpecifier(specifiers[0])) {
            const newImportSpecifiers = specifiers.map(specifier => (
                    types.importDeclaration([types.ImportDefaultSpecifier(specifier.local)],
                    types.stringLiteral(`${node.source.value}/${specifier.local.name}`
                ))
            ));
            path.replaceWithMultiple(newImportSpecifiers);
        }
    }
}

module.exports = () => {
    return {visitor};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// .babelrc配置:
{
    "presets": ["@babel/preset-env"],
    "plugins": [
        [
            "dynamic-import",
            {
                "library": "lodash" // 引用哪个库的时候使用我们写的这个插件
            }
        ]
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12

最后将编写好的插件放入到/node_modules/babel-plugin-dynamic-import/文件夹下,并在package.json文件中指明入口。

没有按需加载前的打包结果如下:

yarn run v1.16.0
$ webpack --mode development
Hash: 8e4da5b2edbe6c47dd1c
Version: webpack 4.41.2
Time: 325ms
Built at: 2019-11-13 19:45:03
    Asset     Size  Chunks             Chunk Names
bundle.js  552 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {main} [built]
[./src/index.js] 37 bytes {main} [built]
    + 1 hidden module
✨  Done in 1.53s.
1
2
3
4
5
6
7
8
9
10
11
12
13
14

采用按需加载前的打包结果如下:

yarn run v1.16.0
$ webpack --mode development
Hash: 58ece130df8aef56b95d
Version: webpack 4.41.2
Time: 105ms
Built at: 2019-11-13 19:46:28
    Asset      Size  Chunks             Chunk Names
bundle.js  20.9 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/index.js] 110 bytes {main} [built]
    + 15 hidden modules
✨  Done in 0.67s.
1
2
3
4
5
6
7
8
9
10
11
12
13

注意事项

  1. babel插件的文件夹命名,必须以babel-plugin-xxx命名,否则引入不成功;
  2. babel插件返回的是一个对象,里面有一个visitor对象。

demo

// 这里手动指定环境变量为development,在命令行执行如下命令:
NODE_ENV=development npx babel index.js
// 转换结果如下:
if ("development" === 'development') {
  console.log('开发环境');
}
1
2
3
4
5
6

@babel/parser(Babylon的升级)

能传递选项给 parse():

babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});
1
2
3
4

sourceType可以是"module" 或者 "script",它表示 Babylon 应该用哪种模式来解析。"module" 将会在严格模式下解析并且允许模块定义,"script" 则不会。

注意:sourceType 的默认值是 "script" 并且在发现 import或 export 时产生错误,使用 scourceType: "module" 来避免这些错误。

总结

插件其实就是基于源代码的AST,利用一些工具生成目标代码的AST的过程。

参考文档

  1. Babel 插件开发指南
  2. Babel 插件开发入门指南
  3. 自己写一个Babel插件
  4. 理解 Babel 插件
  5. Babel插件开发入门指南
  6. 原来babel插件是这样写的
  7. AST 与前端工程化实战
  8. Parser_API
  9. 如何编写一个 babel 插件
  10. 重拳出击:打造 Vue3.0 + Typescript + TSX 开(乞)发(丐)模式
  11. 深入Babel,这一篇就够了
  12. 13 个示例快速入门 JS 抽象语法树
  13. 入门babel--实现一个es6的class转换器
  14. AST 团队分享