Babylon-AST初探-實戰

Summer肉欣發表於2018-05-31

  經過之前的三篇文章介紹,ASTCRUD都已經完成。下面主要通過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程式碼轉小程式

  對比文章一開始展示的兩份程式碼,為了實現轉換,我們需要以下步驟:

  1. data函式轉data屬性,然後刪除data函式
  2. methods裡的屬性提取出來,放到和data同一層級中,methods也要刪除
  3. 將所有的this.[data member]轉換為this.data.[data member]。注意這裡只轉data中的屬性
  4. 在變更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-typesAPI,然後找到從外向內一層一層的對照。這段程式碼的邏輯大概如下:

  1. 找到要插入的程式碼的位置,首先要判斷是不是賦值操作,如果是的話找到this.member的父結點
  2. 新建要插入的結點
  3. 插入節點
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小程式的部分程式碼,以後可以考慮繼續介紹其他模組。

相關文章