經過之前的三篇文章介紹,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
轉小程式
的部分程式碼,以後可以考慮繼續介紹其他模組。