Babylon-AST初探-实战

news/2023/12/1 8:26:49

  经过之前的三篇文章介绍,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.i...]操作,忘记的可以去复习下哦,下面我们直接上代码,大家看这段代码一定要对照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小程序的部分代码,以后可以考虑继续介绍其他模块。


http://www.niftyadmin.cn/n/2013685.html

相关文章

中国的支付清算体系是怎么玩的?

中国的支付清算体系是怎么玩的? AllenChen 关注人,关注产品,关注利润 我一直对支付感兴趣,因为工作需要,也接触过好多第三方支付公司,做过支付路由,也处理了很多支付异常情况。但是支付中的清…

JavaScript中遍历数组 最好不要使用 for in 遍历

先看一段代码 1 <!DOCTYPE html>2 <html lang"en">3 <head>4 <meta charset"UTF-8">5 <title>Document</title>6 </head>7 <body>8 9 <script> 10 // 一个普通的数组 …

ZooKeeper启动数据初始化分析

在ZooKeeper服务器启动期间&#xff0c;首先会进行数据初始化工作&#xff0c;用于将存储在磁盘上的数据文件加载到ZooKeeper服务器内存中。初始化流程数据初始化工作其实就是从磁盘中加载数据的过程&#xff0c;主要包括了从快照文件中加载快照数据的根据事务日志进行数据订正…

java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder 问题

搭建spring cloud的时候&#xff0c;报以下错误&#xff1a; java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder.<init>([Ljava/lang/Object;)V是由于spring boot版本兼容性导致的 获取sc & sp 的对应关系 https://www.ji…

iOS UIImageView自适应图片大小

窗口大小获取&#xff1a; CGRect screenBounds [ [UIScreenmainScreen]bounds];//返回的是带有状态栏的Rect CGRect rect [ [UIScreenmainScreen]applicationFrame];//不包含状态栏的Rect UIImageView&#xff1a; 一 &#xff1a;圆角以及自适应图片大小 UIImage* imag…

万字详解!从零开始搭建创业公司后台技术栈

万字详解&#xff01;从零开始搭建创业公司后台技术栈 转自 : http://ju.outofmemory.cn/entry/351897 前言 说到后台技术栈&#xff0c;脑海中是不是浮现的是这样一幅图&#xff1f; 图 1 有点眼晕&#xff0c;以下只是我们会用到的一些语言的合集&#xff0c;而且只是语言层面…

地址搜索栏设置 极速浏览器

最近一直在用360极速浏览器&#xff0c;无论是界面还是风格都挺喜欢的。 只有一点不太爽&#xff0c;就是浏览器地址栏输入汉字的话一直推荐使用360搜索引擎进行搜索。 个人不是很喜欢360的搜索结果页&#xff0c;但是热词联想功能还是挺实用的就一直忍受着&#xff1b; 直到…

Java自留地(6)

原文内容已经删除。

网络通信协议--程序开发必备基本概念

TCP的 “三次握手” 和“四次挥手” 了解HTTP协议 客户端&#xff1a;通过发送请求获取服务器资源的Web浏览器等就叫做客户端。 Web浏览器作为客户端&#xff0c;一个为服务器&#xff0c;Web浏览器通过指定的访问地址获取服务器上的资源&#xff0c;服务器使用HTTP协议的通信返…

Flutter 从入门到实战

本文由玉刚说写作平台提供写作赞助 版权归玉刚说微信公众号所有 原作者&#xff1a;杨哲丶 版权声明&#xff1a;未经玉刚说许可&#xff0c;不得以任何形式转载 前言 2018年2月27日&#xff0c;在2018世界移动大会上&#xff0c;Google发布了Flutter的第一个Beta版本。Flutter…

从字符中提取出中文

//gb2312的话preg_match_all("/[".chr(0xa1)."-".chr(0xff)."]/", $str, $chinese);echo implode("", $chinese[0]);//utf-8的话preg_match_all("/[\x{4e00}-\x{9fa5}]/u", $str, $chinese);

node的读写流简易实现

readStream 流都是基于EventEmitter实现的 我们先看看node自带的读流用法&#xff1a; let fs require(fs); // 一般情况下我们不会使用后面的参数 let rs fs.createReadStream(./1.txt{highWaterMark: 3, // 字节flags:r,autoClose:true, // 默认读取完毕后自动关闭start:0,…

搭建生产可用的Nacos集群

搭建生产可用的Nacos集群 TIPS 本文使用Nacos 1.0.1 MySQL 8.0&#xff0c;理论兼容Nacos 1.1.3 前面部署的是单机版的Nacos Server&#xff0c;这一般不适用于生产。 本节详细探讨如何搭建一个生产可用的Nacos集群。讨论的内容主要包括&#xff1a;使用MySQL作为存储持久化数…

不吹不擂,你想要的Python面试都在这里了【315+道题】

https://www.cnblogs.com/wupeiqi/p/9078770.html 近日恰逢学生毕业季&#xff0c;课程后期大家“期待苦逼”的时刻莫过于每天早上内容回顾和面试题问答部分【临近毕业每天课前用40-60分钟对之前内容回顾、提问和补充&#xff0c;专挑班里不爱说话就的同学回答】。 期待的是可以…

@ResponseBody是如何起作用的

ResponseBody是如何起作用的 前言 最近参与的项目中&#xff0c;接口中返回的日期格式不对&#xff0c;发现项目中配置了fastjson作为spring的数据转换器&#xff0c;于是使用了fastjson的字段格式化转换注解 发现不起作用。这让我很疑惑&#xff0c;然后在fastjson的相关代码…

logstash 使用笔记

logstash-2.3.4的logstash.conf的配置文件一、监控日志文件&#xff0c;匹配关键字&#xff0c;输出到指定文件/发送邮件。input {file {path > ["你的日志文件"]}}filter {if ([message] !~ "你的匹配关键字") {drop {}}if [loglevel] "debug&qu…
最新文章