- 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 api
Through theBabel
The 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:
- Basic writing conversion,
props
到defineProps
.data
到reactive
.computed
到computed
The function,watch
到watch
The function,methods
To a separate function, the lifecycle hook toonXXX
Functions; - Get rid of
this
,composition
跟this
It doesn’t matter, so we have tothis
Replace with the corresponding variable, such as that used in 🌰this.lastName
,this.firstName
To replacestate.lastName
和state.firstName
; - automatic
import
The method used, the method useddefineProps
,reactive
,computed
And other methods need to be separateimport
Come 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 ~ 🤙🤙🤙