What is babel?

babel is a Javascript compiler, one of the most commonly used tools for front-end development today, primarily for converting ECMAScript 2015+ versions of code to backward-compatible JavaScript syntax so that it can run in current and older versions of browsers or other environments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
a ?? 1;
a?.b;

//After babel conversion
"use strict";

var _a;

(_a = a) !== null && _a !== void 0 ? _a : 1;
(_a2 = a) === null || _a2 === void 0 ? void 0 : _a2.b;

You can test it yourself on the babel website.

Uses of babel

  1. Translate esnext, typescript, flow, etc. to js supported by the target environment.
  2. Some use-specific code conversions. babel exposes a number of api’s that can be used to parse code to AST, convert AST, and generate target code. Developers can use it for specific-purpose conversions such as function stubbing (automatic insertion of code into functions, e.g. buried code), automatic internationalization, default import to named import, etc.
  3. Static analysis of code. After parse, the code can be transformed because the structure of the AST allows the code to be understood. After understanding the code, it can also be used to analyze the information of the code and perform some checks, in addition to converting and generating the target code.
  4. The linter tool is to analyze the structure of the AST and to check the code specification.
  5. api Documentation automatic generation tool that extracts comments from the source code and then generates documentation.
  6. The type checker checks the AST for type consistency based on the type information extracted or derived from the AST, thus reducing type-related errors at runtime.
  7. Compression and obfuscation tool, this also analyzes the code structure and performs various compilation optimizations such as removing dead code, variable name obfuscation, constant folding, etc. to generate smaller and better performing code.

The babel compilation process

The overall babel compilation process is divided into three steps.

  1. parse: convert the source code into an abstract syntax tree (AST) by parser
  2. transform: traverse the AST and call various transform plugins to add, delete and change the AST
  3. generate: print the transformed AST to the target code and generate sourcemap in three steps.

The purpose of the parse phase is to convert the source code string into a machine-understandable AST, which is divided into lexical analysis and syntax analysis.

parse

For example, let name = 'guang'; such a piece of source code, we have to first divide it into words (token) that cannot be subdivided, that is let, name, =, 'guang', this process is lexical analysis, splitting the string into words according to the rules of word formation.

transform

The transform phase is the processing of the AST generated by parse, and it will traverse the AST, and the corresponding visitor function will be called when different AST nodes are processed in the traversal process.

generate

The generate phase prints the AST to the target code string and generates the sourcemap.

1
2
3
while (node condition) {
  // node contet
}

AST Node

Literals, identifiers, expressions, statements, module syntax, and class syntax all have their own ASTs.

For example, literalLiteral.

‘a’ is StringLiteral, 123 is NumberLiteral, variable names, attribute names, and other identifiers are Identifier.

We can go to the AST visualization tool to understand each node.

You can also go to @babel/types to see all AST types.

Public Properties of AST

  • type: the type of the AST node.
  • start, end, loc: start and end represent the start and end subscripts of the source string corresponding to the node, and do not distinguish between rows and columns. The loc attribute is an object with line and column attributes that record the start and end row numbers, respectively.
  • leadingComments, innerComments, trailingComments: indicates the starting comments, middle comments, and ending comments, because there may be comments in each AST node, and they may be in the starting, middle, and ending positions.
  • extra: Record some extra information for handling some special cases.

babel api

@babel/parser

It provides two api’s: parse and parseExpression, both of which convert the source code to AST, but parse returns an AST with File as the root node and parseExpression returns an AST with Expression as the root node. You can specify what to parse and how to parse it. The two most common options are plugins and sourceType.

plugins: Specifies plugins such as jsx, typescript, flow, etc. to parse the corresponding syntax.

sourceType: specifies whether to support parsing module syntax, and has three values: module parses the es module syntax, script does not parse the es module syntax and is executed as a script, and unambiguous is based on whether the content has import and unambiguous is based on whether the content has import and export to determine whether to parse the es module syntax.

1
2
3
4
5
6
7
require("@babel/parser").parse("code", {
  sourceType: "module",
  plugins: [
    "jsx",
    "typescript"
  ]
});

@babel/traverse

The parse AST is traversed and modified by @babel/traverse. The babel traverse package provides the traverse method: function traverse(parent, opts)

parent specifies the AST node to be traversed and opts specifies the visitor function. babel calls the corresponding visitor function when traversing the AST corresponding to parent. enter is called before traversing the children of the current node and exit is called after traversing the children of the current node.

1
2
3
4
5
6
7
visitor: {
    Identifier (path, state) {},
    StringLiteral: {
        enter (path, state) {},
        exit (path, state) {}
    }
}

The path parameter is the path during traversal, which preserves contextual information. The path provides access to node information.

1
2
3
4
5
6
7
8
path.scope gets the scope information of the current node
path.isXxx determines if the current node is of type xx.
path.assertXxx determines whether the current node is of type xx or not, and throws an exception if it is not.
isXxx, assertXxx series methods can be used to determine the AST type
path.insertBefore, path.insertAfter insert node
path.replaceWith, path.replaceWithMultiple, replaceWithSourceString replace nodes
path.remove Remove a node
These methods can add, delete and change ASTs

The parameter state allows passing data between different nodes.

@babel/types

Creates an AST and determines the type of the AST. To create an IfStatement you call t.ifStatement(test, consequent, alternate);

To determine if a node is an IfStatement, call isIfStatement or assertIfStatement t.isIfStatement(node, opts); t.assertIfStatement(node, opts);

@babel/template

Batch node creation

1
2
3
const ast = template(code, [opts])(args);
const ast = template.ast(code, [opts]);
const ast = template.program(code, [opts]);

@babel/generate

Print to target code string

1
2
const { code, map } = generate(ast, { sourceMaps: true })
function (ast: Object, opts: Object, code: string): {code, map} 

@babel/core

To complete the babel compilation process based on the packages above, you can start with the source strings, source files, and ASTs.

Usage

Download

  • Installation
1
npm i @babel/core @babel-cli
  • package.json config
1
2
3
4
5
{
  "scripts": {
    "build": "babel src -d dist"
  }, 
}
  • The babel-loader needs to be configured in webpack
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// webpack.config.js
const path = require('path');
module.exports = {
    entry: './src/app.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }
        ]
    }
};

Configuration

We need to tell babel how to compile and what to compile. There are several ways to configure a file.

  • Set the babel field in package.json.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//package.json
{
   "name":"babel-test",
   "version":"1.0.0",
   "devDependencies": {
       "@babel/core":"^7.4.5",
       "@babel/cli":"^7.4.4",
       "@babel/preset-env":"^7.4.5"
   }
   "babel": {
       "presets": ["@babel/preset-env"]
   }
}
  • .babelrc file or .babelrc.js

    • .babelrc

      1
      2
      3
      
      {
          "presets": ["@babel/preset-env"]
      }
      
    • .babelrc.js

      1
      2
      3
      4
      
      //The webpack configuration file is written in the same way
      module.exports = {
      presets: ['@babel/preset-env']
      };
      
  • babel.config.js file

Same as .babelrc.js, but babel.config.js is for the whole project.

Use

The configuration file tells babel what to compile and then introduces the corresponding plugins, for example, the @babel/plugin-transform-arrow-functions plugin for the arrow function transformation.

1
2
3
4
5
npm i @babel/plugin-transform-arrow-functions 
// babel.config.js
module.exports = {
    plugins: ['@babel/plugin-transform-arrow-functions']
};

Now the arrow function in our code will be compiled as a normal function.

Presets

When there are a lot of plugins or a lot of options for a plugin, it becomes more expensive to use. A preset is a layer of wrapping around babel configuration.

Just install the preset and it will automatically convert the new features in the code into code supported by the target browser, depending on the target browser you set.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
npm i @babel/preset-env
// babel.config.js
module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                targets: {
                    chrome: '58'
                }
            }
        ]
    ]
};

plugin-transform-runtime and runtime plugin

When compiling a class with babel, you need some tool functions to help with the implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class People{
}

// After babel is compiled
'use strict';

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError('Cannot call a class as a function');
    }
}

var People = function People() {
    _classCallCheck(this, People);
};

Each class generates _classCallCheck, which ends up generating a lot of duplicate code. plugin-transform-runtime is designed to solve this problem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
npm i @babel/plugin-transform-runtime
//Production Dependence
npm i @babel/runtime
module.exports = {
    presets: ['@babel/preset-env'],
    plugins: ['@babel/plugin-transform-runtime']
};
"use strict";

// After babel is compiled
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var People = function People() {
  (0, _classCallCheck2["default"])(this, People);
};

babel-polyfill

babel can translate some new features, but for new built-in functions (Promise, Set, Map), static methods (Array.from, Object.assign), instance methods (Array.prototype.includes) that need babel-polyfill to be addressed. babel-polyfill will fully simulate an ES2015+ environment.

Because @babel/polyfill is relatively large, introducing it as a whole both increases the size of the project and pollutes it with too many variables, so it is more recommended to use preset-env to introduce polyfill on demand.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// corejs is a library that provides an interface to lower versions of browsers
npm i core-js@2
// babel.config.js
module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                useBuiltIns: 'usage', // use-introduce on-demand entry-introduce the entry (overall introduction) false-do not introduce polyfill
                corejs: 2  // 2-corejs@2  3-corejs@3
            }
        ]
    ]
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const a = Array.from([1])

//babel after compilation
"use strict";

require("core-js/modules/es6.string.iterator");

require("core-js/modules/es6.array.from");

var a = Array.from([1]); 

A simple case

Function Staking

You want to use babel to automatically insert arguments for filename and row number in api such as console.log, so that you can easily locate the code, which does not affect other logic.

Idea: When traversing the AST to console.log and other api automatically insert some arguments, that is, to do some processing of the function call expression CallExpression specified by the visitor. callExrpession node has two properties, callee and arguments, respectively, corresponding to the name of the function called and So we need to determine when callee is console.xx and insert an AST node in the array of arguments, creating an AST node requires the @babel/types package.

Main Code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous',
    plugins: ['jsx']
});

const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
traverse(ast, {
    CallExpression(path, state) {
        const calleeName = generate(path.node.callee).code;
         if (targetCalleeName.includes(calleeName)) {
            const { line, column } = path.node.loc.start;
            path.node.arguments.unshift(types.stringLiteral(`filename: (${line}, ${column})`))
        }
    }
});
 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
// babel before compilation
const sourceCode = `
    console.log(1);

    function func() {
        console.info(2);
    }

    export default class Clazz {
        say() {
            console.debug(3);
        }
    }
`;

// After babel is compiled
console.log("filename: (2, 4)", 1);

function func() {
  console.info("filename: (5, 8)", 2);
}

export default class Clazz {
  say() {
    console.debug("filename: (10, 12)", 3);
  }
}

Reference https://blog.dteam.top/posts/2021-06/babel-theory.html