• Column address: Front-end compilation and engineering
  • Series of articles: Babel those things, a preview of the Vue file CLI tool
  • Jouryjc

The Babel thing shares minimum optimal configurations and packages for Babel7.x, and this article takes our feelings a step further by taking a closer look at the “Babel Plugin”.

workflow

The compiling process for Babel is shown in the figure above. It consists of three steps: parse, Transform, and generate. Parse compiles source code to generate an abstract syntax tree. Transform performs various operations on the AST tree (compile, delete, update, add, etc.); Finally, generate generates new code from the processed AST, with sourcemap attached.

There are a number of toolkits available for each process to help you get the job done:

  • @babel/parser is used to parse source code;

  • @babel/traverse to traverse and modify the nodes of the AST;

  • @babel/types is used to create nodes and determine node types;

  • @babel/template is a quick way to create AST nodes, much better than an AST that generates a splicing of @babel/types one by one;

  • @babel/generator convert AST to object code;

  • @babel/core is a mobile phone that covers all of the above package functionality from compilation and conversion to generated code and all of the processes in Sourcemap.

AST Node Type

Common types of abstract syntax tree nodes in Babel below:

The Babel AST node Type in the figure combined with @babel/types can help us to better complete the judgment, creation and search of nodes. (😏😏 can collect oh!)

From the Options to Composition

Vue3.2 supports script setup. If you are not familiar with it, check out the RFC.

It’s time, the demand is coming! For some of the older codeoptions apiThrough theBabelThe plug-in automatically converts them tocomposition api.

To get a feel for the difference, use the simple 🌰 code:

import HelloWorld from 'HelloWorld'

export default {
  name: 'App'.components: {
    HelloWorld
  },

  props: {
    firstName: {
      type: String.default: 'Jour'
    }
  },

  data () {
    return {
      lastName: 'Tom'}},computed: {
    name () {
      return this.firstName + this.lastName
    },

    secondName: {
      get () {
        return this.lastName + this.firstName
      }
    } 
  },

  watch: {
    name: {
      deep: true,
      handler (cur, prev) {
        console.log(cur, prev)
      }
    }
  },

  methods: {
    sayHello (aa) {
      console.log('say Hi')
      return aa
    }
  },

  beforeUnmount () {
    console.log('before unmount! ')}}Copy the code

The code covers the common properties of writing a component, which translates to composition API writing as follows:

import { computed, defineProps, onBeforeUnmount, reactive, watch } from 'vue'

const props = defineProps({
  firstName: {
    type: String."default": 'Jour'}});const state = reactive({
  lastName: 'Tom'
});

const name = computed(function name() {
  return props.firstName + state.lastName;
});
const secondName = computed({
  get: function get() {
    returnstate.lastName + props.firstName; }}); watch('name'.(cur, prev) = > {
  console.log(cur, prev);
}, {
  deep: true
});

const sayHello = aa= > {
  console.log('say Hi');
  return aa;
};

onBeforeUnmount(() = > {
  console.log('before unmount! ');
});
Copy the code

Analysis of the

From the above 🌰 we can get what we need to do:

  1. Basic writing conversion,propsdefineProps.datareactive.computedcomputedThe function,watchwatchThe function,methodsTo a separate function, the lifecycle hook toonXXXFunctions;
  2. Get rid ofthiscompositionthisIt doesn’t matter, so we have tothisReplace with the corresponding variable, such as that used in 🌰this.lastName,this.firstNameTo replacestate.lastNamestate.firstName;
  3. automaticimportThe method used, the method useddefineProps,reactive,computedAnd other methods need to be separateimportCome in.

coding

@babel/helper-plugin-utils:

import { declare } from '@babel/helper-plugin-utils'

export default declare((api) = > {
  api.assertVersion(7)

  return {
    name: 'transform-options-to-composition'.visitor: {
      Program (path) {
       
      },
    },
  }
})

Copy the code

Let’s take a look at the overall structure before conversion:

Take a look at the overall structure after the transformation:

As you can see, after 🌰 uses the setup method, it is necessary to replace the whole body under the Program node with the new content, and then convert all properties in the red box to the new writing method. The plug-in code is changed to:

import { declare } from '@babel/helper-plugin-utils'

export default declare((api) = > {
  api.assertVersion(7)
   
  // types 即是 @babel/types
  const { types } = api
  
  // Define a variable to store the results of all changes, and then assign the array of nodes to the original body
  const bodyContent = []

  return {
    name: 'transform-options-to-composition'.// Visitor mode. This function is entered when the type=Program node is accessed
    // Visitor attributes can be defined as functions (the default is Enter) or objects {enter () {}, exit () {}}, which enter and exit will be covered later. 🤫🤫🤫 is omitted here
    visitor: {
      Program (path) {
        / / by types. The isExportDefaultDeclaration positioning to export the default structure
        const exportDefaultDeclaration = path.node.body.filter((item) = >
          types.isExportDefaultDeclaration(item)
        )
        // Get the properties shown above
        const properties = exportDefaultDeclaration[0].declaration.properties
      	
        // ...
          
        path.node.body = bodyContent
      },
    },
  }
})
Copy the code

OK, now that we have all the properties, we need to do the basic conversion for each property. Here we get the handler from the policy mode and store the conversion results into bodyContent:

import { declare } from '@babel/helper-plugin-utils'

export default declare((api) = > {
  api.assertVersion(7)
   
  // types 即是 @babel/types
  const { types } = api
  
  // Define a variable to store the results of all changes, and then assign the array of nodes to the original body
  const bodyContent = []
  
  function genDefineProps (property) {}
  function genReactiveData (property) {}
  function genComputed (property) {}
  function genWatcher (property) {}
  function genMethods (property) {}
  function genBeforeUnmount (property) {}

  return {
    name: 'transform-options-to-composition'.// Visitor mode. This function is entered when the type=Program node is accessed
    // Visitor attributes can be defined as functions (the default is Enter) or objects {enter () {}, exit () {}}, which enter and exit will be covered later. 🤫🤫🤫 is omitted here
    visitor: {
      Program (path) {
        / / by types. The isExportDefaultDeclaration positioning to export the default structure
        const exportDefaultDeclaration = path.node.body.filter((item) = >
          types.isExportDefaultDeclaration(item)
        )
        // Get the properties shown above
        const properties = exportDefaultDeclaration[0].declaration.properties
      	
        // Only the attributes in 🌰 are listed here
        // Key is a configuration item in the Options API
        const GEN_MAP = {
          props: genDefineProps,
          data: genReactiveData,
          computed: genComputed,
          watch: genWatcher,
          methods: genMethods,
          beforeUnmount: genBeforeUnmount,
        }

        properties.forEach((property) = > {
          // Get the name of the key, such as name, components, props, data...
          const keyName = property.key.name
          // Do not handle unnecessary attributes such as name and components
          letnewNode = GEN_MAP? .[keyName]? .(property)if (newNode) {
             Array.isArray(newNode) ? bodyContent.push(... newNode) : bodyContent.push(newNode) } }) path.node.body = bodyContent }, }, } })Copy the code

Here, even if the basic framework is built, the next is the scene analysis of each attribute. This article will share the Babel plug-in, so the scenario will not be particularly complete, but the tools involved in the plug-in will be covered next. The next step is to supplement the logic in the above analysis section by completing the genXXX function.

Basic writing conversion

Let’s look at the props node structure before compiling:

Take a look at the transformed node structure:

To do this conversion, treat the props value in the Options API as the defineProps parameter. GenDefineProps does this with @babel/types:

// options api
props: {... }// composition api
const props = defineProps({ ... })                          
Copy the code

[AST Node Type](#AST Node Type) const props = defineProps({… }) is a variabledeclaration statement, then go to variabledeclaration to learn about the creation parameters of this node:

According to the variableDeclaration definition, we can derive the genDefineProps function:

/** * props options api => compostion api */
function genDefineProps(property) {
    return types.variableDeclaration('const', [
        types.variableDeclarator(
            types.identifier('props'),
            // Make the property.value of the options API parameter to defineProps
            types.callExpression(types.identifier('defineProps'), [property.value])
        ),
    ])
}
Copy the code

Let’s look at the processing of the data function. First, compare the AST structure before and after the conversion:

Before conversion:

After the transformation:

Transformation analysis: ReturnStatement in data structure of Options API is used as parameter of Reactive in Compostion API. The rest of the logic in data is executed via IIFE. Fork transform-options-to-fun ~~😊😊😊)

// options api
data () {
    return {
        lastName: 'Tom'}}// compostion api
const state = reactive({
  lastName: 'Tom'
})
Copy the code

This part of the logic is done with @babel/template:

/** * data options api => compostion api */
function genReactiveData(property) {
    // Const state = reactive(); Statement of ast
    const reactiveDataAST = template.ast(`const state = reactive(); `)

    // Get the ReturnStatement expression in data
    const returnStatement = property.value.body.body.filter(node= > types.isReturnStatement(node))
    
    // Assign parameters to reactive ReturnStatement
    reactiveDataAST.declarations[0].init.arguments.push(
        returnStatement[0].argument
    )

    return reactiveDataAST
}
Copy the code

As you can see, it is much faster and cleaner to generate an AST of the source code using template and then modify the AST node information to complete the transformation process quickly than to generate and combine nodes using @babel/types directly.

For computed, Watch, methods, and lifecycle functions, the idea is to quickly generate a composition AST using @babel/template, and then modify the parameters.

Get rid of this

In the Options API, there is a lot of this.XXX ThisExpression, but in the Composition API, there is no way to access this. So how do I get rid of this? Let’s also make an assumption here: let’s say that this is the key accessing props, data, computed, and Methods. Some special cases, such as vue2 variables passing this.$set, will not be dealt with in this article, but will focus on the Babel plug-in.

After simplifying the scene, we can set the parent object to the variable generated by genXXX by identifying the keys under props, data, computed, and Methods and then iterating through ThisExpression. The text description is too abstract. Let’s analyze it through AST below:

When we traverse nodes like props and data, we map the bottom key to the generated variable (const props = defineProps({… }), const state = reactive({… })), and finally, by traversing ThisExpression, replace the expression by the relationship of nodes:

import { declare } from '@babel/helper-plugin-utils'

export default declare((api) = > {
  api.assertVersion(7)
   
  // types 即是 @babel/types
  const { types } = api
  
  // Define a variable to store the results of all changes, and then assign the array of nodes to the original body
  const bodyContent = []
  const thisExpressionMap = {}
  
  // ...

  return {
    name: 'transform-options-to-composition'.visitor: {
      // Enter when type = ObjectProperty is entered
      ObjectProperty: {
        enter (path) {
          // Get the name of the current key. The value is the key exported from export default under options API
          const keyName = path.node.key.name
          // Map the names of the child nodes' keys to the current key, for example 🌰 :
          // props: {firstName: 'xx'}, which generates {firstName: 'props'}
          if (['props'.'computed'.'methods'].includes(keyName)) {
            path.node.value.properties.map(property= > {
              thisExpressionMap[property.key.name] = keyName
            })
          }

           // data is collected separately from the ReturnStatement
          if (keyName === 'data') {
            const returnStatement = path.node.value.body.body.filter(node= > types.isReturnStatement(node))
            returnStatement[0].argument.properties.map(property= > {
              thisExpressionMap[property.key.name] = 'state'}}}}),Program: {
        // select * from (); Distinguish between the previous writing method! Distinguish between the previous writing method!
        ThisExpressionMap () {thisExpressionMap (); // thisExpressionMap (); // thisExpressionMap ()
        exit(path) {
          path.traverse({
            ThisExpression (p) {
              const propertyName = p.parent.property.name
			 // From this's cache information, we can figure out which variable the attribute of the current this binding belongs to
              if(thisExpressionMap[propertyName]) { p.parent.object = types.identifier(thisExpressionMap[propertyName]); }}},}})Copy the code

One more thing:

// options api
export default {
    props: {
        age: 1
    },

    computed: {
        info () {
            return ` My age:The ${this.age}`}}}Copy the code

ThisExpressionMap generated:

thisExpressionMap = {
    age: 'props'
}
Copy the code

Final generated result:

const props = defineProps({
    age: 1
})

const info = computed(() = > {
    return ` My age:${props.age}`
})
Copy the code

The import method

A quick rundown of the process: While iterating through properties, identify which keys are being processed, do a simple property-to-method mapping, and finally generate an AST through template that is inserted into bodyContent. Directly on the code:

import { declare } from '@babel/helper-plugin-utils'

export default declare((api) = > {
  api.assertVersion(7)
  const { types, template } = api

  // Which APIS are used for caching
  const importIdentifierMap = {}

  function hasImportIndentifier(item) {
    return ['props'.'data'.'computed'.'watch'.'beforeUnmount'].includes(
      item
    )
  }

  function genImportDeclaration(map) {
    const importSpecifiers = []
    const importMap = {
      props: 'defineProps'.data: 'reactive'.computed: 'computed'.watch: 'watch'.beforeUnmount: 'onBeforeUnmount',}Object.keys(map).forEach((item) = > {
      const importIdentifier = hasImportIndentifier(item) ? importMap[item] : ' '

      if (importIdentifier) {
        importSpecifiers.push(importIdentifier)
      }
    })
    return template.ast(`import {${importSpecifiers.join(', ')}} from 'vue'`)}return {
    name: 'transform-options-to-composition'.visitor: {
      // ...
      Program: {
        exit(path) {
          // ...

          properties.forEach((property) = > {
            // ...

            if (newNode) {
              // Cache the functions used
              importIdentifierMap[keyName] = true

              // ...}})// Generate import declarations depending on which functions are introduced
          bodyContent.unshift(genImportDeclaration(importIdentifierMap))

          path.node.body = bodyContent
        },
      },
    },
  }
})
Copy the code

conclusion

At this point, the plug-in requirements in 🌰 are complete. But that’s just the requirement in the example! Actual use there are many many functions need to be realized, many details need to be added! If you are interested, you can fork the source code.

This article has really improved the development experience by going from the options API component writing of VUe2 to the Setup writing of VUe3.2. It’s really time to learn and use! Historical baggage is inevitable, and this article uses the Babel plug-in to provide ideas for upgrading. Toolkits like @babel/core, @babel/types, @babel/traverse, @babel/ Template are used in the examples, as well as interspersed references to path concepts (refer to the Babel plugin manual for details).

Why not strike while the iron is hot? Come and play ~ 🤙🤙🤙