Most of you may be unfamiliar with Babel and familiar with it. Most of you may be familiar with Babel using a Babel-loader in webpack. But when you really understand his mastery of it, he actually has some stronger uses…

The basic concept

What’s a Babel?

Babel is a compiler (input source => output compiled code). Just like any other compiler, the compilation process is divided into three stages: parsing, conversion, and printout. (Explanation on the official website).

What are Babel Plugin and Babel Preset?

There are many concepts in Babel, such as plugins, preset, and more basic tools (such as @babel/ Parser,@babel/traverse, etc.). Their relationship can be understood as Babel’s plugin is built on the base tool, and Babel’s preset is a packed set of multiple Babel plugins, such as @babel/preset-env,@babel/preset- React.

Babel in-depth

This article won’t talk too much about the official Babel Plugin, Preset library, because this is an in-depth tutorial. We ask a more fundamental question: How does Babel translate code?

We break down the process of translating code into three general steps:

  • Step 1 (parse) : Code =>ast
  • Transform: Ast => Modified AST
  • Generate: Modified AST => compiled code

These steps correspond to the three basic Babel tools, the first for @babel/ Parser, the second for @babel/traverse, and the third for @babel/ Generator. Here’s a closer look at these three processes.

parse(@babel/parser)

This is where Babel converts code into an AST. Ast is short for Abstract syntax tree, which may be difficult to understand by itself, but we can take a look at a concrete example. You can use astexplorer.net/ to help you run @babel/parser:

function mirror(something) {
  return something
}
Copy the code

Translated as AST:

{
  "type": "File"."start": 0."end": 49."loc": {
    "start": {
      "line": 1."column": 0
    },
    "end": {
      "line": 3."column": 1}},"program": {
    "type": "Program"."start": 0."end": 49."loc": {
      "start": {
        "line": 1."column": 0
      },
      "end": {
        "line": 3."column": 1}},"sourceType": "module"."interpreter": null."body": [{"type": "FunctionDeclaration"."start": 0."end": 49."loc": {
          "start": {
            "line": 1."column": 0
          },
          "end": {
            "line": 3."column": 1}},"id": {
          "type": "Identifier"."start": 9."end": 15."loc": {
            "start": {
              "line": 1."column": 9
            },
            "end": {
              "line": 1."column": 15
            },
            "identifierName": "mirror"
          },
          "name": "mirror"
        },
        "generator": false."async": false."params": [{"type": "Identifier"."start": 16."end": 25."loc": {
              "start": {
                "line": 1."column": 16
              },
              "end": {
                "line": 1."column": 25
              },
              "identifierName": "something"
            },
            "name": "something"}]."body": {
          "type": "BlockStatement"."start": 27."end": 49."loc": {
            "start": {
              "line": 1."column": 27
            },
            "end": {
              "line": 3."column": 1}},"body": [{"type": "ReturnStatement"."start": 31."end": 47."loc": {
                "start": {
                  "line": 2."column": 2
                },
                "end": {
                  "line": 2."column": 18}},"argument": {
                "type": "Identifier"."start": 38."end": 47."loc": {
                  "start": {
                    "line": 2."column": 9
                  },
                  "end": {
                    "line": 2."column": 18
                  },
                  "identifierName": "something"
                },
                "name": "something"}}]."directives": []}}],"directives": []},"comments": []}Copy the code

At first glance it seems complicated, but what you need to do is to find the key information in it. We removed the fields that affect the reading (loc,start,end, and nesting of the outer layer of the function) :

{
  "type": "FunctionDeclaration"."id": {
    "type": "Identifier"."name": "mirror"
  },
  "generator": false."async": false."params": [{"type": "Identifier"."name": "something"}]."body": {
    "type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
          "type": "Identifier"."name": "something"}}]."directives": []}}Copy the code

Isn’t that much easier? The outer layer is a function declaration called mirror, and its pass parameter is something. Inside the function body, the return variable is something. We compare this description with the js code above, and it happens to be the same (actually from this point also can see code<=>ast this process is reversible). For beginners, the abstract syntax tree above may be difficult to understand because of the verbose node types. Here is a quick list of common node names in JS (you can skip them carefully, but knowing these node names will help you understand Babel and even the JS language itself). See babeljs. IO/docs/en/bab…

type The literal meaning describe
FunctionDeclaration Function declaration function a() {}
FunctionExpression Functional expression var a = function() {}
ArrowFunctionExpression Arrow function expression () = > {}(Consider: why there is no arrow function Declaration and the difference between Declaration and Expression)
AwaitExpression Await the expression async function a () { await b() }
CallExpression Call expression a()
MemberExpression Member expression a.b
VariableDeclarator Variable declarations var,const,let(Var,const,let is distinguished by kind in Node)
Identifier Variable identifier var a(where a is an Identifier)
NumericLiteral Numeric literal var a = 1
StringLiteral String literals var a = 'a'
BooleanLiteral Boolean value literals var a = true
NullLiteral Null literal var a = null(Here you can think: why not undefined literal)
BlockStatement block {}
ArrayExpression Array expression []
ObjectExpression Object expression var a = {}
SpreadElement Extended operator {... a},[...a]
ObjectProperty Object properties {a:1}(where a:1 is an ObjectProperty)
ObjectMethod Function attributes {a(){}}
ExpressionStatement Expression statement a()
IfStatement if if () {}
ForStatement for for (;;) {}
ForInStatement for in for (a in b) {}
ForOfStatement for of for (a of b) {}
ImportDeclaration The import declaration import 'a'
ImportDefaultSpecifier Import default specifier import a from 'a'
ImportSpecifier The import specifier import {a} from 'a'
NewExpression New expressions new A()
ClassDeclaration The class declaration class A {}
ClassBody class body class A {}(Inside the class)

The list of common enough… I’ll leave you there.

generate(@babel/generator)

Generate was supposed to be the third step, so why put it there? Because it’s relatively simple, and we’re going to need it when we’re using traverse. Here we simply convert a code to ast and then to code:

Install the dependencies first. This point will not be repeated again

yarn add @babel/parser @babel/generator
Copy the code
const parser = require('@babel/parser')
const generate = require('@babel/generator').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const transformedCode = generate(ast).code
console.log(transformedCode)
Copy the code
  • Results:
function mirror(something) {
  return something;
}
Copy the code

This is the basic usage of generator, see babeljs. IO /docs/en/bab…

transform(@babel/traverse,@babel/types,@babel/template)

It’s time for the most critical transform step, with @babel/traverse, @babel/types and @babel/template as auxiliary tools. Let’s start by talking about the concept of visitor.

visitor

  1. What is the visitor

Visitor is a cross-language pattern for AST traversal. They are simply an object that defines a method for obtaining a specific node in a tree structure.

Let’s say you write a visitor that passes to Babel like this:

const visitor = {
  Identifier () {
    enter () {
      console.log('Hello Identifier! ')
    },
    exit () {
      console.log('Bye Identifier! ')}}}Copy the code

Babel then uses his recursive traverser to traverse the ast, executing our defined function on entering and exiting the Identifier node.

2. Exit is rarely used in general, so it can be abbreviated as:

const visitor = {
  Identifier () {
    console.log('Hello Identifier! ')}}Copy the code

3. If necessary, you can also use the method name with | | divided into a node type b in the form of the node type string, use the same function in a variety of access nodes.

const visitor = {
  'FunctionExpression|ArrowFunctionExpression' () {
    console.log('A function expression or a arrow function expression! ')}}Copy the code

Ok, now write a simple traverse example using the mirror function above:

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const visitor = {
  Identifier (path) {
    console.log(path.node.name)
  }
}
traverse(ast, visitor)
Copy the code
  • Results: mirror, something, something

Is it consistent with your estimate? If we agree, then we can move on. Here you might ask: What is this path?

path

You can simply think of path as a wrapper around the currently accessed Node. For example, using path.node to access the current node and using path.parent to access the parent node, the contents of path are listed here (some methods contained in Path are not listed yet).

{
  "parent": {... },"node": {... },"hub": {... },"contexts": []."data": {},
  "shouldSkip": false."shouldStop": false."removed": false."state": null."opts": null."skipKeys": null."parentPath": null."context": null."container": null."listKey": null."inList": false."parentKey": null."key": null."scope": null."type": null."typeAnnotation": null
}
Copy the code

When you have a visitor to the Identifier() member method, you are actually accessing the path rather than the node. In this way, you are dealing with the responsive representation of the node rather than the node itself. babel handbook

Path also provides a series of utility functions, such as traverse(performing recursion under the current path),remove(deleting the current node),replaceWith(replacing the current node), and so on.

With path explained, let’s try to actually convert the code, using @babel/ Generator to convert the AST to code

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const visitor = {
  Identifier (path) {
    path.node.name = path.node.name.split(' ').reverse().join(' ')
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
Copy the code
  • Results:
function rorrim(gnihtemos) {
  return gnihtemos;
}
Copy the code

This code should be easy to understand, just do a string flip of all the variables. Are things getting interesting already?

@babel/types

The Babel Types module is a Lodash library for AST nodes that contains methods for constructing, validating, and transforming AST nodes. This tool library contains thoughtful tool methods that are useful for writing logic that processes AST. (Handbook)

Show the most common usage to determine the type of node

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const visitor = {
  enter(path) {
    if (t.isIdentifier(path.node)) {
      console.log('Identifier! ')
    }
  }
}
traverse(ast, visitor)
Copy the code
  • Results: the Identifier. Identifier! Identifier!

@babel/types can also be used to generate nodes. With that in mind, we tried changing the return value of the mirror function

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const strNode = t.stringLiteral('mirror')
const visitor = {
  ReturnStatement (path) {
    path.traverse({
      Identifier(cpath){
        cpath.replaceWith(strNode)
      }
    })
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
Copy the code
  • Results:
function mirror(something) {
  return "mirror";
}
Copy the code

Here we use t.stingLiteral (‘mirror’) to create a string literal node and recursively iterate through the Identifier under the ReturnStatement, And replace it with the string literal node we created (note that we’ve already started using some public methods under PATH here).

@babel/template

It’s easy to create simple nodes with @babel/type, but difficult to create large pieces of code. In this case, we can use @babel/template. Here is a simple example that writes some logical judgments inside the mirror function.

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const template = require('@babel/template').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',})const visitor = {
  FunctionDeclaration(path) {
    // Declare a template here, which is much easier to generate than @babel/types
    const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
    const returnNode = path.node.body.body[0]
    const tempAst = temp({
      NORMAL_RETURN: returnNode
    })
    path.node.body.body[0] = tempAst
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
Copy the code
  • Results:
function mirror(something) {
  if (something) {
    return something;
  } else {
    return 'nothing'; }}Copy the code

Perfect! Now that we’ve covered the basic tools for using Babel, let’s get down to business: Try writing a Babel plug-in.

Write a Babel plug-in

At this point, writing a Babel plug-in is pretty straightforward, so let’s try porting the above code directly to a Babel plug-in

module.exports = function (babel) {
  const {
    types: t,
    template
  } = babel
  const visitor = {
    FunctionDeclaration(path) {
      const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
      const returnNode = path.node.body.body[0]
      const tempAst = temp({
        NORMAL_RETURN: returnNode
      })
      path.node.body.body[0] = tempAst
    }
  }
  return {
    name: 'my-plugin',
    visitor
  }
}
Copy the code

The Babel plugin exposes a function that takes Babel as an argument. You can use destruct assignment to get tools like types and template. The return value of the function contains a name and a visitor. The name is the name of the plug-in, and the visitor is the visitor we wrote several times above.

You may have noticed that some Babel plugins can pass parameters, so how do we receive parameters in Babel plugins

module.exports = function (babel) {
  const {
    types: t,
    template
  } = babel
  const visitor = {
    FunctionDeclaration(path, state) {
      const temp = template(`
        if(something) {
          NORMAL_RETURN
        } else {
          return '${state.opts.whenFalsy}'} `)
      const returnNode = path.node.body.body[0]
      const tempAst = temp({
        NORMAL_RETURN: returnNode
      })
      path.node.body.body[0] = tempAst
    }
  }
  return {
    name: 'my-plugin',
    visitor
  }
}
Copy the code

In the above example we saw that the second parameter state could be passed in the visitor, where state.opts[configuration name] is used to access the value of the configuration name passed by the user

How do you test that your Babel plug-in works? Reference your plugin and test it:

const babel = require("@babel/core")

const code = `function mirror(something) { return something }`
const res = babel.transformSync(code, {
  plugins: [[require('Address of plug-in you wrote'), {
      whenFalsy: 'Nothing really.'}}]])console.log(res.code)
Copy the code
  • Results:
function mirror(something) {
  if (something) {
    return something;
  } else {
    return 'Nothing really.'; }}Copy the code

Now that we have a basic understanding of how Babel works, we can write our own Babel plug-in. As for how to use Babel’s power in everyday work? It’s up to you to find out for yourself.


Shuidi front-end team recruiting partners, welcome to send resume to email: [email protected]