preface

Do you often write try/catch logic in async functions during development for robustness or to catch asynchronous errors?

[JavaScript]

Plain text view
Copy the code

?
1
2
3
4
5
6
7
async
function
func() {

try
{

let res = await asyncFunc()

}
catch
(e) {

/ /...

}
}


I mentioned an elegant way to handle async/await in 28 JavaScript Tips to Be a Qualified Intermediate Front-end Engineer





This way we can wrap the async function with a helper function for error catching

[JavaScript]

Plain text view
Copy the code

?
1
2
3
4
5
6
7
async
function
func() {

let [err, res] = await errorCaptured(asyncFunc)

if
(err) {

/ /... Error trapping

}

/ /...
}

One drawback is that you have to introduce errorCaptured every time you use it. Is there a way to get lazy? The answer is definitely there, and I came up with a new idea after that blog post that would automatically inject try/catch code with a Webpack loader, and hopefully the end result would be something like this

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// development
async
function
func() {

let res = await asyncFunc()

/ /... Other logic
}
// release
async
function
func() {

try
{

let res = await asyncFunc()

}
catch
(e) {

/ /...

}

/ /... Other logic
}

Isn’t it great? There is no need for any extra code in the development environment. Let Webpack automatically inject error capture logic into the code in the production environment. Next we will step by step implement the loader principle before implementing the Webpack Loader. Each loader we define in Webpack is essentially a function. When defining loader, we will also define a test attribute. Webpack will traverse all module names, and when matching the re defined by the test attribute, This module is passed to the Loader for execution as the source parameter

[JavaScript]

Plain text view
Copy the code

?
1
2
3
4
{

test: /\.vue$/,

use:
"vue-loader"
.
}

When a file name ending in.vue is matched, the file is passed to vue-loader as the source parameter. The use attribute can be followed by a string or a path. If the file is a string, the nodeJS module will go to node_modules and these files are actually strings (images and videos are Buffer objects). In the case of vue-loader, when loader receives the file, The template string will be compiled into the render function by vue-loader, script part will be handed to babel-loader, style part will be handed to CSS-loader, and loader follows a single principle. That is, a loader only does one thing, so that multiple Loaders can be flexibly combined without interfering with each other. Because loader can read the matched files and turn them into the desired output after processing, we can implement a Loader by ourselves and accept JS files. ‘await’ keyword: wrap a try/catch layer around the code. How can we wrap a try/catch layer around ‘await’ keyword exactly? We need to know about abstract syntax trees (AST)

An abstract syntax tree is an abstract representation of the syntax structure of source code. It represents the syntactic structure of a programming language as a tree, with each node in the tree representing a structure in the source code


AST can be used to implement many very useful functions, such as converting ES6 code to ES5, ESLint checking, code beautification, and even JS engines rely on AST. At the same time, because the code is simply a string, it is not limited to converting BETWEEN JS, SCSS, CSS preprocessors such as LESS are also CSS code recognized by browsers through AST conversion. Let’s take an example

[JavaScript]

Plain text view
Copy the code

?
1
2
let a = 1
let b = a + 5

This is what happens when you convert it to an abstract syntax tree

“>

Converting a string into an AST tree requires both lexical analysis and syntax analysis

For example, the first line will convert let, a, =, 1 into tokens. Token is an object that describes the position of the code fragment in the whole code and records some information about the current value

The syntax analysis will convert the token into Node in combination with the syntax of the current language (JS), and Node contains a type attribute to record the current type. For example, let in JS stands for a variable declaration keyword. So its type is VariableDeclaration, and a = 1 will be the declaration description of let. Its type is VariableDeclarator, and the declaration description is dependent on the VariableDeclaration, so there is a hierarchy

In addition, it can be found that there is not a token for a Node. The equal sign must have both values to form a declaration statement, otherwise a warning will be issued. This is the basic principle of ESLint. Finally, all nodes are grouped together to form an AST syntax tree

Recommend a very useful AST viewing tool, AST Explorer, a more intuitive view of how code is translated into abstract syntax tree

Going back to the implementation of the code, we just need to find the await expression through the AST tree and wrap the try/catch Node around the await expression

[JavaScript]

Plain text view
Copy the code

?
1
2
3
async
function
func() {

await asyncFunc()
}

Corresponding AST tree:



[JavaScript]

Plain text view
Copy the code

?
1
2
3
4
5
6
7
async
function
func() {

try
{

await asyncFunc()

}
catch
(e) {

console.log(e)

}
}

Corresponding AST tree:



Loader developmentWith that in mind, we’ll start writing the loader. When our loader receives the source file, it passes @babel/ Parser

This package converts files into AST abstract syntax trees, so how do I find the corresponding await expression?

This requires another Babel package, @babel/traverse, which passes in an AST tree and hook functions, and then traverses the AST tree in depth, executing the corresponding callback when the nodes and hook functions have the same name

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
module.exports =
function
(source) {

let ast = parser.parse(source)

traverse(ast, {

AwaitExpression(path) {

/ /...

}

})

/ /...
}

With @babel/traverse we can easily find the Node corresponding to the await expression, and then create a Node with type TryStatement and await it. There is another package, @babel/types, which is the loadsh library for Babel. It provides a number of helper functions related to AST nodes. We need to use the tryStatement method. Create a TryStatement Node

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
const t = require(
"@babel/types"
)
module.exports =
function
(source) {

let ast = parser.parse(source)

traverse(ast, {

AwaitExpression(path) {

let tryCatchAst = t.tryStatement(

/ /...

)

/ /...

}

})
}

The tryStatement takes three arguments. The first is a try clause, the second is a catch clause, and the third is a finally clause. A complete try/catch statement corresponds to a Node Node that looks like this

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
const t = require(
"@babel/types"
)
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
const t = require(
"@babel/types"
)
module.exports =
function
(source) {

let ast = parser.parse(source)

traverse(ast, {

AwaitExpression(path) {

let tryCatchAst = t.tryStatement(

// try clause (required)

t.blockStatement([

t.expressionStatement(path.node)

]),

/ / catch clause

t.catchClause(

/ /...

)

)

path.replaceWithMultiple([

tryCatchAst

])

}

})

/ /...
}

Use the blockStatement, expressionStatement methods to create a block-level scope and a Node with await expressions. The @babel/traverse will pass each hook function a path argument. Contains some information about the current traversal, such as the current Node, the path object of the last traversal, and the corresponding Node. Most importantly, it contains some methods for manipulating Node nodes. We need to use the replaceWithMultiple method to replace the current Node Node with the Node Node of the try/catch statement and we need to consider the await expression as a declaration statement

[JavaScript]

Plain text view
Copy the code

?
1
let res = await asyncFunc()

Copy code may also be an assignment statement

[JavaScript]

Plain text view
Copy the code

?
1
res = await asyncFunc()

Copying code may also be a pure expression

[JavaScript]

Plain text view
Copy the code

?
1
await asyncFunc()

The AST corresponding to these three cases is also different, so we need to handle them separately. @bable/types provides rich judgment functions. In the AwaitExpression hook function, we only need to determine what type of Node the parent Node is. In addition, you can use the AST Explorer to view the AST tree structure to be generated

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
const t = require(
"@babel/types"
)
module.exports =
function
(source) {

let ast = parser.parse(source)

traverse(ast, {

AwaitExpression(path) {

if
(t.isVariableDeclarator(path.parent)) {
// Variable declaration

let variableDeclarationPath = path.parentPath.parentPath

let tryCatchAst = t.tryStatement(

t.blockStatement([

variableDeclarationPath.node
// Ast

]),

t.catchClause(

/ /...

)

)

variableDeclarationPath.replaceWithMultiple([

tryCatchAst

])

}
else
if
(t.isAssignmentExpression(path.parent)) {
// Assign expression

let expressionStatementPath = path.parentPath.parentPath

let tryCatchAst = t.tryStatement(

t.blockStatement([

expressionStatementPath.node

]),

t.catchClause(

/ /...

)

)

expressionStatementPath.replaceWithMultiple([

tryCatchAst

])

}
else
{
// await expression

let tryCatchAst = t.tryStatement(

t.blockStatement([

t.expressionStatement(path.node)

]),

t.catchClause(

/ /...

)

)

path.replaceWithMultiple([

tryCatchAst

])

}

}

})

/ /...
}

After obtaining the replacement AST tree, use the @babel/core package transformFromAstSync method to convert the AST tree back into the corresponding code string and return it

[JavaScript]

Plain text view
Copy the code

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
const parser = require(
"@babel/parser"
)
const traverse = require(
"@babel/traverse"
).
default
const t = require(
"@babel/types"
)
const core = require(
"@babel/core"
)
module.exports =
function
(source) {

let ast = parser.parse(source)

traverse(ast, {

AwaitExpression(path) {

/ / same as above

}

})

return
core.transformFromAstSync(ast).code
}

The copy code also exposes some loader configuration items to improve ease of use, such as an await statement not being injected again if an await statement is already wrapped in a try/catch. This principle is also based on AST, using the findParent method of the path parameter to traverse all parent nodes. Traverse (ast, {AwaitExpression(path) {if (path.findparent ((path) => T.i This allows all errors to be handled using uniform logic. The principle is to convert the user-configured code fragment into an AST and pass it as a parameter to the Catch node when the TryStatement node is created. For example, the technology stack is an old project of jQuery, it can match the Node Node of $. Ajax, inject the error processing logic uniformly, and even customize some new syntax that ECMA does not have. Sorry, I understand the compilation principle. Really can do whatever you want the article reprinted to: https://juejin.cn/post/6844903886898069511