经过之前的三篇文章介绍,AST
的CRUD
都已经完成。下面主要通过vue
转小程序
过程中需要用到的部分关键技术来实战。
下面的例子的核心代码依然是最简单的一个vue
示例
const babylon = require('babylon')
const t = require('@babel/types')
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const code = `
export default {
data() {
return {
message: 'hello vue',
count: 0
}
},
methods: {
add() {
++this.count
},
minus() {
--this.count
}
}
}
`
const ast = babylon.parse(code, {
sourceType: 'module',
plugins: ['flow']
})
复制代码
经过本文中的一些操作,我们将获得最终的小程序代码如下:
Page({
data: (() => {
return {
message: 'hello vue',
count: 0
}
})(),
add() {
++this.data.count
this.setData({
count: this.data.count
})
},
minus() {
--this.data.count
this.setData({
count: this.data.count
})
}
})
复制代码
注意:,跟我们之前介绍的一致,为了完成上述转换,要把输入和输出均放入AST explorer,查看其先后的结构对比。
vue
代码转小程序
对比文章一开始展示的两份代码,为了实现转换,我们需要以下步骤:
- 将
data
函数转data
属性,然后删除data
函数 - 将
methods
里的属性提取出来,放到和data
同一层级中,methods
也要删除 - 将所有的
this.[data member]
转换为this.data.[data member]
。注意这里只转data
中的属性 - 在变更
this.data
的下面,插入this.setData
来触发数据变更
下面将按照这一步骤,一步一步完成转换,我觉得看到每一步的代码变化还是很有成就感滴。
生成data
属性
这一步,我们要先提取原data
函数中的return
的对象。结合AST explorer,可以很方便的找到这一路径。
const dataObject = ast.program.body[0].declaration.properties[0].body.body[0].argument
console.log(dataObject)
复制代码
可是这段代码的可读性和鲁棒性基本是0啊。它强依赖我们书写的data
函数是第一个属性。所以这里我们还是主要使用traverse
来访问节点:
traverse(ast, {
ObjectMethod(path) {
if (path.node.key.name === 'data') {
// 获取第一级的 BlockStatement,也就是data函数体
let blockStatement = null
path.traverse({ //将traverse合并的写法
BlockStatement(p) {
blockStatement = p.node
}
})
// 用blockStatement生成ArrowFunctionExpression
const arrowFunctionExpression = t.arrowFunctionExpression([], blockStatement)
// 生成CallExpression
const callExpression = t.callExpression(arrowFunctionExpression, [])
// 生成data property
const dataProperty = t.objectProperty(t.identifier('data'), callExpression)
// 插入到原data函数下方
path.insertAfter(dataProperty)
// 删除原data函数
path.remove()
// console.log(arrowFunctionExpression)
}
}
})
console.log(generate(ast, {}, code).code)
复制代码
程序输出:
export default {
data: (() => {
return {
message: 'hello vue',
count: 0
};
})(),
methods: {
add() {
++this.count;
},
minus() {
--this.count;
}
}
};
复制代码
将methods
中的属性提升一级
这里遍历methods
中的属性没有再采用traverse
,因为这里结构是固定的。
traverse(ast, {
ObjectProperty(path) {
if (path.node.key.name === 'methods') {
// 遍历属性并插入到原methods之后
path.node.value.properties.forEach(property => {
path.insertAfter(property)
})
// 删除原methods
path.remove()
}
}
})
复制代码
程序输出:
export default {
data: (() => {
return {
message: 'hello vue',
count: 0
};
})(),
minus() {
--this.count;
},
add() {
++this.count;
}
};
复制代码
this.member
转为this.data.member
这一步,首先要从data
属性中提取数据属性。这个有些依赖data
中的函数到底写成怎么样,如果写成:
data: (() => {
const obj = {}
obj.message = 'hello vue'
obj.count = 0
return obj
})(),
复制代码
这将不符合我们这里的转化方法。当然我们可以通过求值来获取最终的对象,但这里也有缺陷。另一个思路是遍历其他成员函数,使用排除法。
总之,我们需要一个方法来获取this.data
中的属性。本文将继续以代码中的例子,通过data
中的return
方法来获取。
// 获取`this.data`中的属性
const datas = []
traverse(ast, {
ObjectProperty(path) {
if (path.node.key.name === 'data') {
path.traverse({
ReturnStatement(path) {
path.traverse({
ObjectProperty(path) {
datas.push(path.node.key.name)
path.skip()
}
})
path.skip()
}
})
}
path.skip()
}
})
console.log(datas)
复制代码
程序输出:
[ 'message', 'count' ]
复制代码
修改数据属性至this.data.
traverse(ast, {
MemberExpression(path) {
if (path.node.object.type === 'ThisExpression' && datas.includes(path.node.property.name)) {
path.get('object').replaceWithSourceString('this.data')
}
}
})
复制代码
至此程序输出:
export default {
data: (() => {
return {
message: 'hello vue',
count: 0
};
})(),
minus() {
--this.data.count;
},
add() {
++this.data.count;
}
};
复制代码
添加this.setData
方法
要想在变更this.data
的下面,插入this.setData
,我们首先要找到它插入的位置,即this.data
的父节点,所以这就是我们的第一步操作:(MemberExpression
就是上一步的,因为这一步的path与上一步相同)
traverse(ast, {
MemberExpression(path) {
if (path.node.object.type === 'ThisExpression' && datas.includes(path.node.property.name)) {
path.get('object').replaceWithSourceString('this.data')
}
}
const expressionStatement = path.findParent((parent) =>
parent.isExpressionStatement()
)
})
复制代码
找到插入的位置后,我们就要构造要插入的函数,这时就用到了我们在这个系列第一篇文章中介绍的(Create
)[https://summerrouxin.github.io/2018/05/22/ast-create/Javascript-Babylon-AST-create/]操作,忘记的可以去复习下哦,下面我们直接上代码,大家看这段代码一定要对照AST explorerh和babel-types
的API
,然后找到从外向内一层一层的对照。这段代码的逻辑大概如下:
- 找到要插入的代码的位置,首先要判断是不是赋值操作,如果是的话找到
this.member
的父结点 - 新建要插入的结点
- 插入节点
traverse(ast, {
MemberExpression(path) {
if (path.node.object.type === 'ThisExpression' && datas.includes(path.node.property.name)) {
path.get('object').replaceWithSourceString('this.data')
//一定要判断一下是不是赋值操作
if(
(t.isAssignmentExpression(path.parentPath) && path.parentPath.get('left') === path) ||
t.isUpdateExpression(path.parentPath)
) {
// findParent
const expressionStatement = path.findParent((parent) =>
parent.isExpressionStatement()
)
// create
if(expressionStatement) {
const finalExpStatement =
t.expressionStatement(
t.callExpression(
t.memberExpression(t.thisExpression(), t.identifier('setData')),
[t.objectExpression([t.objectProperty(
t.identifier(propertyName), t.identifier(`this.data.${propertyName}`)
)])]
)
)
expressionStatement.insertAfter(finalExpStatement)
}
}
}
}
})
复制代码
程序输出:
export default {
data: (() => {
return {
message: 'hello vue',
count: 0
};
})(),
minus() {
--this.count;
this.setData({
count: this.data.count
})
},
add() {
++this.count;
this.setData({
count: this.data.count
})
}
};
复制代码
以上就是我们实战介绍,这边只涉及到vue
转小程序
的部分代码,以后可以考虑继续介绍其他模块。