Using Babel family barrel to modularize the ancient noodle code

Time:2020-10-27

Using Babel family barrel to modularize the ancient noodle code

In my recent work, I took over an old project. The JS code is a lump of noodle code. About 3000 lines of code are all written in one file, which is really a headache to maintain.

Using Babel family barrel to modularize the ancient noodle code

I don’t know why the students who maintain the project before can tolerate such hard to maintain code Now that I’ve taken this pot down, I can’t tolerate the existence of such ugly code. I have to optimize it.

After looking at it for a long time, the logic has been mixed in a file, and you can see it dazzled. The most urgent task is to split it into modules and extract them into files, so as to facilitate the continuous optimization in the future.

1、 Structural analysis

Say to do, since we want to split into modules, we must first analyze the structure of the source code. Although the content of the source code is very long and complex, fortunately, it still has a clear structure, which can be simplified as follows:

Using Babel family barrel to modularize the ancient noodle code

It is easy to see that this is a classic code organization mode in the Es5 era. In an Iife, a constructor is placed in the constructorprotorypeMount different methods on to achieve different functions. Since the code structure is clear, the idea of modularization is also very clear, that is, to find a way to bind all of them to the constructorprototypeMethods on the extraction, placed in the form of module files, while the source code uses ES6importStatement introduces the module to complete the modularization of the code

Using Babel family barrel to modularize the ancient noodle code

In order to achieve this effect, we can use the@babelThe whole family bucket is used to construct our transformation script.

2、 Code analysis with AST

There are a lot of relevant information about ast, so I won’t repeat it here. In this paper, we will analyze the source code with the help of AST, and select the parts in the source code that need to be separated and modified. Therefore, AST can be said to be the core of this paper. In https://astexplorer.net/ On this website, we can post sample code to see what its ast looks like online

Using Babel family barrel to modularize the ancient noodle code

You can see it clearly from the ast tree on the right,Demo.prototype.func = function () {}belong toAssignmentExpressionNode, which is called “assignment statement”, has two different nodes on the left and right(leftright)。

Since there may be multiple assignment statements in a JS code, we only want to deal with the followingDemo.prototype.func = function () {}So we need to continue to analyze the nodes on the left and right sides.

First of all, look at the node on the left. It belongs to a “member expression”. Its characteristics are shown in the arrow below:

Using Babel family barrel to modularize the ancient noodle code

For the node on the left, as long as itsobject.property.nameThe value of isprototypeThat is, the corresponding function name is the node’sproperty.name

Then look at the node on the right, which belongs to a “function expression”:
Using Babel family barrel to modularize the ancient noodle code

What we have to do is extract it as a separate file.

After analyzing the ast, we have known what features the code to be processed has. Next, we will operate on these features. At this time, we need our@babelThe whole family is out!

3、 Processing code

First, we need to install four tools. They are:

  • @babel/parser: used to convert JS source code into ast;
  • @babel/traverse: it is used to traverse the ast tree to obtain the node content;
  • @babel/generator: convert ast node into corresponding JS code;
  • @babel/types: create a new ast node.

Next, create a new oneindex.jsFile, introduce the above four tools, and try to load our source code (source code isdemo/es5code.js):

const fs = require('fs')
const { resolve } = require('path')

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

const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')

const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')

Then use@babel/parserAST to obtain the source code:

const ast = parser.parse(code)

After you get the ast, you can use it@babel/traverseTo traverse its nodes. From the ast analysis in the previous section, we only need to focus on the “assignment expression” node

traverse(ast, {
  AssignmentExpression ({ node }) {
    /* ... */
  }
})

The current node is the parameternodeWe need to analyze the nodes on its left and right sides. Only when the type of the left node is “memberexpression” and the type of the node on the right is “functionexpression”, we need to go to the next stepa = 1Such nodes are also of type assignmentexpression and are not in our scope).

Because there may be different memberexpression nodes in JS, such asa.b.c = function () {}But we just need to deal with it nowa.prototype.funcIt means to focus on keywordsprototype。 By analyzing the ast node, we know that this keyword is located in theobject.property.nameAttribute:

Using Babel family barrel to modularize the ancient noodle code

At the same time, the corresponding function name is hidden in theproperty.nameAttribute:

Using Babel family barrel to modularize the ancient noodle code

Therefore, it can be easily extractedMethod name

traverse(ast, {
  AssignmentExpression ({ node }) {
    const { left, right } = node
    if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {
      const { object, property } = left
      if (object.property.name === 'prototype') {
        const funcName =  property.name  //Extract the method name
        console.log(funcName)
      }
    }
  }
})

The method name can be easily printed out to check:

Using Babel family barrel to modularize the ancient noodle code

Now that we have analyzed the code of the left node, we have extracted the method name. The next step is to deal with the right node. Since the code on the right is directly a function expression node, all we have to do is to pass the@babel/generatorConvert the node to JS code and write it to a file.

In addition, we also need to take the original code fromDemo.prototype.func = function () {}IntoDemo.prototype.func = funcTherefore, the node on the right needs to be converted from the “functionexpression” type to the “identifier” type. We can use the@babel/typesTo deal with it.

Another thing to remember is that we have extracted the code of the node on the right into JS files, so we should also introduce them into the source files after the final transformation, such asimport func1 from './func1' This form can therefore continue to be used@babel/typesOfimportDeclaration()Function to generate the corresponding code. This function parameter is complex and can be encapsulated into a function

function createImportDeclaration (funcName) {
  return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))
}

Just pass in onefuncNameTo generate a paragraphimport funcName from './funcName'code.

The final overall code is as follows:

const fs = require('fs')
const { resolve } = require('path')

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

const INPUT_CODE = resolve(__dirname, '../demo/es5code.js')
const OUTPUT_FOLDER = resolve(__dirname, '../output')

const code = fs.readFileSync(`${INPUT_CODE}`, 'utf-8')
const ast = parser.parse(code)

function createFile (filename, code) {
  fs.writeFileSync(`${OUTPUT_FOLDER}/${filename}.js`, code, 'utf-8')
}

function createImportDeclaration (funcName) {
  return t.importDeclaration([t.importDefaultSpecifier(t.identifier(funcName))], t.stringLiteral(`./${funcName}`))
}

traverse(ast, {
  AssignmentExpression ({ node }) {
    const { left, right } = node
    if (left.type === 'MemberExpression' && right.type === 'FunctionExpression') {
      const { object, property } = left
      if (object.property.name === 'prototype') {    
        //Gets the method name of the left node
        const funcName = property.name
        //Get the JS code corresponding to the right node
        const { code: funcCode } = generator(right)
        //The right node is changed to identifier
        const replacedNode = t.identifier(funcName)
        node.right = replacedNode
       
        //With the help of` fs.writeFileSync () ` write the JS code of the right node to an external file
        createFile(funcName, 'export default ' + funcCode)

        //Introduce detached files in the file header
        ast.program.body.unshift(createImportDeclaration(funcName))
      }
    }
  }
})

//Output new file
createFile('es6code', generate(ast).code)

4、 Run script

In our project directory, the structure is as follows:

.
├── demo
│   └── es5code.js
├── output
├── package.json
└── src
    └── index.js

Run the script,demo/es5code.jsThe code will be processed and then output tooutputcatalog:

.
├── demo
│   └── es5code.js
├── output
│   ├── es6code.js
│   ├── func1.js
│   ├── func2.js
│   └── func3.js
├── package.json
└── src
    └── index.js

Take a look at our code:
Using Babel family barrel to modularize the ancient noodle code

Using Babel family barrel to modularize the ancient noodle code

be accomplished! When we apply the script to our project, we can even find that the original 3000 lines of code have been sorted into more than 300 lines:

Using Babel family barrel to modularize the ancient noodle code

Put the real environment to run this code, the original function is not affected!

Summary

Just took over this project, my heart is ten thousand beasts galloping past, my heart is very collapse. But now that it’s taken over, it’s worth taking care of it. With the help of AST and@babelFamily bucket, we have the means to fully transform the source code. Is it possible to spend an hour and a half to clean up the code in the haze? Will it take more time to clean up the old code? It’s worth watching!

Recommended Today

What black technology does the real-time big data platform use behind the glory of the king?

Hello everyone, I’m Xu Zhenwen. Today’s topic is “Tencent game big data service application practice based on Flink + servicemesh”, which is mainly divided into the following four parts: Introduction to background and Solution Framework Real time big data computing onedata Data interface service onefun Microservice & servicemesh 1、 Introduction to the solution framework and […]