资讯专栏INFORMATION COLUMN

Babylon-AST初探-实战

godiscoder / 833人阅读

摘要:生成属性这一步,我们要先提取原函数中的的对象。所以这里我们还是主要使用来访问节点获取第一级的,也就是函数体将合并的写法用生成生成生成插入到原函数下方删除原函数程序输出将中的属性提升一级这里遍历中的属性没有再采用,因为这里结构是固定的。

  经过之前的三篇文章介绍,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代码转小程序

  对比文章一开始展示的两份代码,为了实现转换,我们需要以下步骤:

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.i...]操作,忘记的可以去复习下哦,下面我们直接上代码,大家看这段代码一定要对照AST explorerh和babel-typesAPI,然后找到从外向内一层一层的对照。这段代码的逻辑大概如下:

找到要插入的代码的位置,首先要判断是不是赋值操作,如果是的话找到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小程序的部分代码,以后可以考虑继续介绍其他模块。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107867.html

相关文章

  • Babylon-AST初探-代码更新&删除(Update & Remove)

    摘要:操作通常配合来完成。因为是个数组,因此,我们可以直接使用数组操作自我毁灭方法极为简单,找到要删除的,执行就结束了。如上述代码,我们要删除属性,代码如下到目前为止,的我们都介绍完了,下面一篇文章以转小程序为例,我们来实战一波。   通过前两篇文章的介绍,大家已经了解了Create和Retrieve,我们接着介绍Update和 Remove操作。Update操作通常配合Create来完成。...

    levius 评论0 收藏0
  • Babylon-AST初探-代码查询(Retrieve)

    摘要:针对语法树节点的查询操作通常伴随着和这两种方法见下一篇文章。注意上述代码打印出的和中的并不完全一致。如函数,在中的为,但其实际的为。这个大家一定要注意哦,因为在我们后面的实际代码中也有用到。   在上一篇文章中,我们介绍了AST的Create。在这篇文章中,我们接着来介绍AST的Retrieve。  针对语法树节点的查询(Retrieve)操作通常伴随着Update和Remove(这两...

    wangdai 评论0 收藏0
  • Netty4.x 源码实战系列(一):ServerBootstrap 与 Bootstrap 初探

    摘要:而用于主线程池的属性都定义在中本篇只是简单介绍了一下引导类的配置属性,下一篇我将详细介绍服务端引导类的过程分析。 从Java1.4开始, Java引入了non-blocking IO,简称NIO。NIO与传统socket最大的不同就是引入了Channel和多路复用selector的概念。传统的socket是基于stream的,它是单向的,有InputStream表示read和Outpu...

    BakerJ 评论0 收藏0
  • 云智慧压测实战分享之JMeter工具使用初探

    摘要:有了测试脚本,通过线程组来模拟真实用户对服务器的访问压力。不同的是,这些类型的线程执行测试结束后执行定期的线程组。线程组中包含的线程数量在测试执行过程中是不会发生改变的。逻辑控制器元件只对其子节点中的取样器和逻辑控制器作用。 工欲善其事必先利其器,要保证移动应用产品在上线之后能稳定运行于各种复杂环境,仅仅进行功能测试是远远不够的,压力测试越来越被应用开发商所重视。而压力测试从传统的内部...

    venmos 评论0 收藏0

发表评论

0条评论

godiscoder

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<