Babylon-AST初探-实战

  通过以前的三篇文章介绍,ASTCRUD都已经完成。下面主要经过vue小程序过程当中须要用到的部分关键技术来实战。vue

下面的例子的核心代码依然是最简单的一个vue示例node

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']
})
复制代码

  通过本文中的一些操做,咱们将得到最终的小程序代码以下:git

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,查看其前后的结构对比。github

vue代码转小程序

  对比文章一开始展现的两份代码,为了实现转换,咱们须要如下步骤:express

  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,能够很方便的找到这一路径。bash

const dataObject = ast.program.body[0].declaration.properties[0].body.body[0].argument
console.log(dataObject)
复制代码

  但是这段代码的可读性和鲁棒性基本是0啊。它强依赖咱们书写的data函数是第一个属性。因此这里咱们仍是主要使用traverse来访问节点:babel

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)
复制代码

程序输出:ide

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小程序的部分代码,之后能够考虑继续介绍其余模块。

相关文章
相关标签/搜索