preface
For front-end development, Babel is certainly familiar enough to be used in your work. In addition to being used as a tool to transform ES6 and JSX, I feel that Babel’s plug-in mechanism, based on abstract syntax trees, offers us more possibilities. There is a lot of good documentation online about Babel concepts and plug-ins. Detailed analysis is recommended in the official Babel plug-in manual. There are some things you should know before you start developing plug-ins that you can skip if you’re already familiar with them.
Abstract Syntax Tree (AST)
Babel using a ESTree based and modified the AST, its kernel documentation can be in [here] (https://github. com/Babel/Babel/blob/master/doc/AST/spec. Md) found. It should be clearer to look directly at the example:
function square(n) {
return n * n;
}
Copy the code
Corresponding AST object (object format provided by Babel)
{
// Code block category, function declaration
type: "FunctionDeclaration".// Variable identifier
id: {
type: "Identifier".// Variable name
name: "square"
},
/ / parameters
params: [{
type: "Identifier".name: "n"}]./ / the function body
body: {
/ / block statements
type: "BlockStatement".body: [{
/ / return statement
type: "ReturnStatement".argument: {
// A binary expression
type: "BinaryExpression"./ / operators
operator: "*".left: {
type: "Identifier".name: "n"
},
right: {
type: "Identifier".name: "n"}}}]}}Copy the code
Each layer is called a Node, and the JS object corresponding to a complete AST may have many nodes, depending on the situation. Babel returns each node as an interface. These include properties as shown in the code above, such as type, start, end, LOc and other general properties and private properties corresponding to specific types. The processing of our later plug-ins is also handled according to different types.
Don’t be bothered by the sheer size of the JS object. It’s not realistic to parse the AST and remember the different types as defined by Babel every time. This should be left to the computer. We can use the AST Explorer to convert object code into syntax tree objects and combine the AST Node Types to view specific properties.
Processing steps for Babel
Babel’s three main processing steps are: parse, transform and generate. We don’t want to describe the process in detail, but read the official manual. It is important to note that the Babel plug-in comes into play during the transformation process, where the parsed syntax tree object is processed for its own purposes before the code generation step. So learn more about transformations.
The code generation step converts the final AST (after a series of transformations) into string code, and also creates source maps for easy debugging.
Code generation works by going depth-first through the AST and then building a string that represents the transformed code. When you convert, you do recursive tree traversal.
conversion
Visitor
When it comes to transformation, that’s when the plug-in comes into play, but how to get into that process, Babel provides us with a specification of that Visitor. We can define our access logic through the Visitor. It’s going to look something like this
const MyVisitor = {
// This corresponds to the node type above. All nodes whose type is Identifier will enter this method
Identifier() {
console.log("Called!"); }};// Take this method as an example
function square(n) {
return n * n;
}
// will be called four times, because
// The function name is square
/ / parameter n
// The two n's in the function body are identifiers
path.traverse(MyVisitor);
// So output four
Called!
Called!
Called!
Called!
Copy the code
Because of the depth-first traversal calculation method, after arriving at a leaf node, it is found that there are no descendant nodes, so it is necessary to trace up to go back to the upper level to continue traversing the next child node, so each node will be visited twice. If not specified, the calls occur when the node is entered, although the visitor method can also be called on exit.
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!"); }}};Copy the code
Here are some other tips:
Can use | in the method name to match a variety of different type, use the same processing function.
const MyVisitor = {
"ExportNamedDeclaration|Flow"(path) {}
};
Copy the code
You can also use aliases in visitors (as defined by babel-types) such as Function is FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, Alias of ObjectMethod and ObjectMethod, which can be used to match all of the above types of Type
const MyVisitor = {
Function(path) {}
};
Copy the code
Paths
An AST usually has many nodes, so how do the nodes relate directly to each other? We can represent relationships between nodes with a large mutable object that can be manipulated and accessed, or we can simplify things with Paths. Path is an object that represents the connection between two nodes. It’s a little bit clearer to look at examples.
{
type: "FunctionDeclaration".id: {
type: "Identifier".name: "square"},... }Copy the code
The child Identifier, expressed as a Path, looks like this:
{
"parent": {
"type": "FunctionDeclaration"."id": {... },... },"node": {
"type": "Identifier"."name": "square"}}Copy the code
When you pass a visitor to an Identifier() member method, you are actually accessing a path, not a node. In this way, you are dealing with the responsive representation of the node rather than the node itself.
Writing a plug-in
The front are some essential knowledge points, this article is just some relatively important points mentioned. Detailed or want to see the development manual. Personally, there are three steps to developing a plug-in:
- Analyze the source file abstract syntax tree AST
- Analyze the object file abstract syntax tree
- Build Visitor 3.1 Determine access conditions 3.2 Determine conversion logic
There are three main steps to a plug-in, but the first two are very important. 3.1 and 3.2 depend on the results of 1 and 2 respectively. Only after a clear understanding of AST structure can we have a clear target and get twice the result with half the effort. For example, the following code:
var func = (a)= >{
console.log(this.b)
};
Copy the code
The goal is to convert the arrow function into a normal function declaration (this is just a detailed conversion of this format, not to mention the rest). As follows:
var _this = this;
var func = function () {
console.log(_this.b);
};
Copy the code
Source file syntax tree
Here’s a look at this simple function declaration, as defined above, but it’s recommended that the AST Explorer see our syntax tree clearly. Only useful information is captured here:
"init": {
"type": "ArrowFunctionExpression"./ *... Other information.... * /
"id": null./ / parameter
"params": []."body": {
// Function body,this part
"arguments": [{"type": "MemberExpression"."object": {
/ / this expression
"type": "ThisExpression",},"property": {
/ / b attributes
"type": "Identifier"."name": "b"}}]}}Copy the code
The only thing we want to transform is the ArrowFunctionExpression that is the arrow function and this expression ThisExpression part, leaving everything else untouched for now. So the function name in our visitor includes ArrowFunctionExpression and ThisExpression.
// The key of the method in the visitor corresponds to the node type we are processing
const visitor = {
// Handle this expression
ThisExpression(path){
// Convert this to _this
},
// Handle the arrow function.
ArrowFunctionExpression(path){
// Convert to normal FunctionExpression}}Copy the code
Object file syntax tree
In the same way, the syntax tree object looks like this: The syntax tree is too long, so let’s look at the changes
// The transformed body consists of an array of two elements. The two variables are declared to be statistical
"body": [
//var _this = this; structure
{
"type": "VariableDeclaration"."kind": "var"."declarations": [{"type": "VariableDeclarator".//left is the identifier of _this
"id": {
"type": "Identifier"."name": "_this"
},
//right is this expression
"init": {
"type": "ThisExpression"
/ * * * * * /}},// var func = function (b) {
// console.log(_this.b);
/ /}; Structure only looks at the key
{
"type": "VariableDeclaration"."kind": "var"."declarations": [{/ * * * * * omit * * * * * * * /
"arguments": [{"type": "MemberExpression".// The converted _this.b
"object": {
"type": "Identifier"."name": "_this"
},
"property": {
"type": "Identifier"."name": "b"}]}}]Copy the code
After comparison, it is determined that our operation should be to replace ArrowFunctionExpression with FunctionExpression, bind this when this expression is encountered, and convert it. The API provided by Path is used to do things like add and replace:
- Replace replaceWith (targetObj)
- FindParent () finds the parent that satisfies the condition
- InsertBefore Inserts siblings for more please consult the documentation, which lists only the methods we use.
Tectonic node
ToFunctionExpression is an API that I never found… Maybe I’m not looking for FunctionExpression in the right place. I have no choice but to look it up in Babel source code:
//@src /babel/packages/babel-types/src/definitions/core.js
defineType("FunctionExpression", {
inherits: "FunctionDeclaration".//....
}
// Find FunctionDeclaration
defineType("FunctionDeclaration", {
Id,params,body..
builder: ["id"."params"."body"."generator"."async"].visitor: ["id"."params"."body"."returnType"."typeParameters"]
//....
}
Copy the code
If there is a clear document, please do not hesitate to comment. Now this is easy.
Later, I looked for it and finally found the corresponding documentportal
To improve the Visitor
const Visitor = {
/ / this expression
ThisExpression(path){
Build var _this = this
let node = t.VariableDeclaration(
'var',
[
t.VariableDeclarator(
t.Identifier('_this'),
t.Identifier('this'))),// Build the _this identifier
str = t.Identifier('_this'),
// Find the parent node of the variable declaration
// This is for example only, but there are a lot of cases to consider for real conversion
parentPath = path.findParent((path) = > path.isVariableDeclaration())
// The condition is met
if(parentPath){
/ / insert
parentPath.insertBefore(node)
path.replaceWith(
str
)
}else{
return}},// Handle the arrow function.
ArrowFunctionExpression(path){
var node = path.node
// create a t. unctionExpression node and replace the original path
path.replaceWith(t.FunctionExpression(
node.id,
node.params,
node.body
))
}
}
Copy the code
That’s the end of the main visitor, of course, if it’s a plug-in
// babel-types is passed as an argument when the plug-in is called by Babel
export default function({ types: t }) {
return {
visitor:Visitor
}
Copy the code
For local debugging, you can introduce babel-core and babel-types respectively
var babel = require('babel-core');
var t = require('babel-types');
var code = `var func = ()=>{ console.log(this.b) }; `
const result = babel.transform(code, {
plugins: [{
// Visitor in front
visitor: Visitor
}]
});
// Output the converted code
/** * var _this = this; * var func = function () { * console.log(_this.b); *}; * /
console.log(result.code);
Copy the code
conclusion
Refer to the article
Babel for ES6? And Beyond!
I thought I understood the principles of Babel and the plugin mechanism, but I didn’t realize it was so hard to write a small demo. I’m still not familiar with the relevant API, and I don’t know how to build nodes. This article is a simple summary of the plug-in manual, the idea of their own implementation is summarized. Throw a brick to attract jade, common progress, in addition, I hope to have a little help to the students in need. See more on my blog