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