preface
According to the bible, there was a very high tower, is composed of a group of speak the same language, and unite the people building and hardworking, they can hope to heaven, god restrained the plans of the people, for love and protection, to rely on the knowledge of god, god so to disrupt their language, and makes them can’t understand each other, And scattered them all over the world. Hence the name of Babel, also known as the Tower of Chaos.
The wind will destroy the trees, and JavaScript has not escaped this fate. Since its birth, with the potential of lightning, with its own flexibility and ease of use, in the browser end shine, widely used in different standards of each browser. However, the good times don’t last long, an evil organization called ECMA in the dark continues to experiment with JavaScript, will develop it into a terrible biological weapons. Scientists sought to gain control over language specifications by integrating features into ES4 to satisfy their own desires, but ES4 failed to resist the pressure and eventually died in childbirth. To keep the experiment going, scientists called DC and M$came up with a more conservative, incremental proposal, which was widely accepted and published two years later, called ES5. For a long time, the laboratory named TC39 has secretly developed the TC39 process pipeline, which consists of 5 stages:
Stage 0
(Strawman
Phase) – This phase is an open submission phase, any inTC39
Registered contributions are orTC39
Members can submitStage 1
(Proposal
Phase) – This phase is a formal proposal for a new feature to be submittedStage 2
(Draft
Phase) – this phase is the first version that will appear in the standardStage 3
(Canidate
Phase) – The proposal for this phase is nearing completionStage 4
(Finished
Phase) – this phase will be included in the standard
Since 2015, JavaScript has entered a new ERA of ES6, which represents the launch of ES2015, which is the culmination of a new generation of JavaScript that not only has its own ES Module specification, It has also unlocked Proxy, Async, Class, Generator and other features. It has gradually grown into a robust language, and started to occupy the server market with the high-performance Node framework. In recent years, it has joined forces with React Native to compete for mobile development, Shouting freedom and democracy. Gradually capture the heart of a teenager and a girl.
Any language depends on an execution environment. For scripting languages like JavaScript, it always depends on a JavaScript engine, which is usually shipped with the browser. Different versions and implementations of the engine vary from browser to browser. It’s easy to get into the problem of parsing the JavaScript language differently from browser to browser. For developers, we need to abandon language features and write compatible code to support different browser users; It is unreasonable and unrealistic for users to force them to change to the latest browser.
This situation was not resolved until Babel, a JavaScript compiler designed to convert ES2015+ syntax standard code into backwardly compatible versions to accommodate older versions of the operating environment. Babel is more than just a compiler. It is a bridge to the unification and standardization of JavaScript, allowing software developers to write source code in their preferred programming language or style and translate it into a unified JavaScript form using Babel.
Babel is where chaos begins and ends, and for the peace of the world, we all need to try and learn the basics of the Babel plugin in case we need it.
Abstract syntax tree
In computer science, Abstract Syntax and Abstract Syntax trees are Tree representations of Abstract Syntax structures of source code, also known as AST (Abstract Syntax Tree). AST is commonly used for syntax check, code style check, code format, code highlighting, code error prompt, code auto-completion, etc. It is widely used. In JavaScript, AST follows ESTree specifications.
To illustrate, let’s define a function:
function square(n) {
return n * n;
}
Copy the code
Its AST conversion results in the following (some empty and positional fields are omitted) :
{
"type": "File"."program": {
"type": "Program"."body": [{"type": "FunctionDeclaration",},"id": {
"type": "Identifier"."identifierName": "square"
},
"name": "square"
},
"params": [{"type": "Identifier"."name": "n"}]."body": {
"type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
"type": "BinaryExpression"."left": {
"type": "Identifier"."name": "n"
},
"operator": "*"."right": {
"type": "Identifier"."name": "n"}}}],}}],},}Copy the code
Since the AST is a tree structure, we can think of it as a Node, each of which implements the following specification:
interface Node {
type: string;
loc: SourceLocation | null;
}
Copy the code
Type indicates different syntax types. The AST contains FunctionDeclaration, BlockStatement, and ReturnStatement. All types are visible.
The working process
By configuring Babel’s presets, plugins, and other information, Babel transforms the source code specifically and outputs more general object code, the three main parts of which are: compile, transform, and generate.
compile
Compiling Babel is mainly done by @babel/ Parser. Its final goal is to convert it into an AST abstract syntax tree. There are two steps in this process:
- Lexical Analysis, which converts the source code to a flattened array of syntax fragments, also known as token flows
- Syntactic Analysis, which converts the token flow obtained in the previous stage into AST form
To get the compiled result, we import the @babel/parser package, compile a common function, and view the printed result:
import * as parser from '@babel/parser';
function square(n) {
return n * n;
}
const ast = parser.parse(square.toString());
console.log(ast);
Copy the code
conversion
The transformation step traverses the AST node and performs CRUD operations on the node. In Babel this is done with @babel/traverse, and we continue with the compilation process of the previous piece of code, we want to convert n * n to math.pow (n, 2) :
import traverse from '@babel/traverse';
// ...
const ast = parser.parse(square.toString());
traverse(ast, {
enter(path) {
if (t.isReturnStatement(path.parent) && t.isBinaryExpression(path.node)) {
path.replaceWith(t.callExpression(
t.memberExpression(t.identifier('Math'), t.identifier('pow')),
[t.stringLiteral('n'), t.numericLiteral(2)])}}});console.log(JSON.stringify(ast));
Copy the code
In this process, we use @babel/types to make type judgments and generate nodes of the specified type.
generate
In Babel it is mainly generated with @babel/ Generator, which regenerates the transformed AST as a code string. Based on the above Demo, rewrite the code:
import generator from '@babel/generator';
/ /... Same as above
console.log(generator(ast));
Copy the code
We end up with the transformed code:
{
code: 'function square(n) {\n return Math.pow("n", 2);\n}'.map: null.rawMappings: null
}
Copy the code
The plug-in structure
Let’s start by defining the basic structure of a plug-in:
// plugins/hello.js
export default function(babel) {
return {
visitor: {}}; }Copy the code
We can then make a simple reference in the configuration file as follows:
// babel.config.js
module.exports = { plugins: ['./plugins/hello.js']};Copy the code
visitor
In the plugin, there is a visitor object that represents the visitor pattern. Inside the Babel are nodes traversed by @babel/traverse mentioned above. We can access the AST by specifying the node type:
module.exports = function(babel) {
return {
visitor: {
Identifier(path) {
console.log('visiting:', path.node.name)
}
}
};
};
Copy the code
So when you compile n by n, you see the output twice. Visitor also provides enter and exit access for nodes. Let’s rewrite the program:
visitor: {
Identifier: {
enter(path) {
console.log('enter:', path.node.name);
},
exit(path) {
console.log('exit:', path.node.name); }}}Copy the code
So, when you recompile the program, you have four prints, and the visitor does a depth-first walk from top to bottom of the AST, visits the node once when you enter it, and visits it once when you exit it. Let’s write a code to test the order of the traverse:
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
function square(n) {
return n * n;
}
const ast = parser.parse(square.toString());
traverse(ast, {
enter(path) {
console.log('enter:', path.node.type, path.node.name || ' ');
},
exit(path) {
console.log('exit:', path.node.type, path.node.name || ' '); }});Copy the code
Print result:
enter: Program
enter: FunctionDeclaration
enter: Identifier square
exit: Identifier square
enter: Identifier n
exit: Identifier n
enter: BlockStatement
enter: ReturnStatement
enter: BinaryExpression
enter: Identifier n
exit: Identifier n
enter: Identifier n
exit: Identifier n
exit: BinaryExpression
exit: ReturnStatement
exit: BlockStatement
exit: FunctionDeclaration
exit: Program
Copy the code
path
Path as the first parameter of the node access, it represents the node access path, the infrastructure looks like this:
{
"parent": {
"type": "FunctionDeclaration"."id": {... },... },"node": {
"type": "Identifier"."name": "..."}}Copy the code
Where node represents the current node, parent represents the parent node, and path contains some node meta information and some methods for manipulating nodes:
- FindParent searches for nodes from parent nodes
- GetSibling gets the sibling node
- ReplaceWith replaces the node with an AST node
- ReplaceWithMultiple replaces this node with multiple AST nodes
- InsertBefore Inserts a node before the node
- InsertAfter inserts a node after a node
- Remove Delete a node.
A path is a Reactive representation of a node’s position in the tree and information about that node. When you call a tree modification method, the path information is also updated. Babel manages all this for you, making it as simple and stateless as possible.
opts
When using the Babel plug-in, the user can pass in the configuration information of the Babel plug-in, and the plug-in can process the code according to the configuration. First, when importing the plug-in, change the way of importing the array, where the first object is the path and the second element is the configuration item opts:
module.exports = {
presets,
plugins: [['./src/plugins/xxx.js',
{
op1: true}}]].Copy the code
In the plug-in, it is accessible through state:
module.exports = function(babel) {
return {
visitor: {
Identifier: {
enter(_, state) {
console.log(state.opts)
// { op1: true }}}}}; };Copy the code
nodes
When writing the Babel plug-in, we often need to insert or modify AST nodes. In this case, we can use the built-in functions provided by @babel/types to construct nodes. The two methods are equivalent:
import * as t from '@babel/types';
module.exports = function({ types: t }) {}
Copy the code
The function names that build Node usually match type, except for the first lowercase letter, such as t.memberExpression(…) when building a MemberExpression object. Method, where the construct parameter depends on the definition of the node.
Babel plug-in practices
Here are some of the basic uses of the Babel plug-in, but the most important is to practice in code engineering, imagine scenarios where we could write a Babel plug-in to solve a real problem, and then Just Do It.
A simple plug-in instance
Let’s take the simplest example to get the ball rolling. In the process of code debugging, we often use the Debugger statement to facilitate the debugging of functions at run time. We want to use the Babel plug-in to print the location of the current Debugger node in the development environment to remind us, and delete the node directly in the production environment.
To implement such a plug-in, first use ASTExplorer to find the Node type of the Debugger as DebuggerStatement. We need to use this Node accessor, and then use NODE_ENV to determine the running environment. If it is production, the path.remove method is called; otherwise, the stack is printed.
First, create a plug-in called babel-plugin-drop-debugger.js and write the code:
module.exports = function() {
return {
name: 'drop-debugger'.visitor: {
DebuggerStatement(path, state) {
if (process.env.NODE_ENV === 'production') {
path.remove();
return;
}
const {
start: { line, column }
} = path.node.loc;
console.log(
`Debugger exists in file: ${ state.filename }, at line ${line}, column: ${column}`); }}}; };Copy the code
Then reference the plug-in in babel.config.js:
module.exports = {
plugins: ['./babel-plugin-drop-debugger.js']};Copy the code
Create a test file called test-plugin.js:
function square(n) {
debugger;
return (a)= > 2 * n;
}
Copy the code
Print when we execute: NPX Babel test-plugin.js:
Debugger exists in file: /Users/xxx/test-plugin.js, at line 2.column: 2
Copy the code
NODE_ENV=production NPX Babel test-plugin.js
function square(n) {
return (a)= > 2 * n;
}
Copy the code
conclusion
I have not yet encountered a scenario in the project that requires Babel to solve a problem, so I will not go into further details, but hope to fill in later. In this article we have a basic idea of how to use the Babel plug-in. To learn the basics of how to use the Babel plug-in, visit the user manual.
Babel consists of three main parts: compile, transform and generate. The plug-in mechanism does not open the following core libraries:
@babel/parser
The Babel AST parser, formerly known as Babylon, was created byacornAnd to@babel/traverse
To traverse and update the AST Node@babel/generator
Rebuild the code according to the AST and related options@babel/types
, determine AST node types and construct new nodes
Here are some useful development AIDS:
- Fast debugging of AST and plug-ins online: ASTExplorer
- View the definition of all AST node types: AST Spec
- AST specification: ESTree
It is worth mentioning that the API documentation and Doc of Babel’s official Github library are not very sound, and sometimes you can only learn from the source code. Hopefully, the next time you need to implement a complete Babel plug-in, you’ll want to explore further.