详解如何用babel转换es6的class语法


Posted in Javascript onApril 03, 2018

babel是一个转码器,目前开发react、vue项目都要使用到它。它可以把es6+的语法转换为es5,也可以转换JSX等语法。

我们在项目中都是通过配置插件和预设(多个插件的集合)来转换特定代码,例如env、stage-0等。

实际上babel可以通过自定义插件的方式实现任何代码的转换,接下来我们通过一个“把es6的 class 转换为es5”的例子来了解一下babel。

内容如下:

webpack环境配置

大家应该都配置过babel-core这个loader,它的作用是提供babel的核心Api,实际上我们的代码转换都是通过插件来实现的。

接下来我们不用第三方的插件,自己实现一个es6类转换插件。先执行以下几步初始化一个项目:

  1. npm install webpack webpack-cli babel-core -D
  2. 新建一个webpack.config.js
  3. 配置webpack.config.js

如果我们的插件名字想叫transform-class,需要在webpack配置中做如下配置:

详解如何用babel转换es6的class语法

接下来我们在node_modules中新建一个babel-plugin-transform-class的文件夹来写插件的逻辑(如果是真实项目,你需要编写这个插件并发布到npm仓库),如下图:

详解如何用babel转换es6的class语法

红色区域是我新建的文件夹,它上面的是一个标准的插件的项目结构,为了方便我只写了核心的index.js文件。

如何编写bable插件

babel插件其实是通过AST(抽象语法树)实现的。

babel帮助我们把js代码转换为AST,然后允许我们修改,最后再把它转换成js代码。

那么就涉及到两个问题:js代码和AST之间的映射关系是什么?如何替换或者新增AST?

好,先介绍一个工具:astexplorer.net:

这个工具可以把一段代码转换为AST:

详解如何用babel转换es6的class语法

如图,我们写了一个es6的类,然后网页的右边帮我们生成了一个AST,其实就是把每一行代码变成了一个对象,这样我们就实现了一个映射。

再介绍一个文档: babel-types :

这是创建AST节点的api文档。

比如,我们想创建一个类,先到astexplorer.net中转换,发现类对应的AST类型是 ClassDeclaration 。好,我们去文档中搜索,发现调用下面的api就可以了:

详解如何用babel转换es6的class语法

创建其他语句也是一样的道理,有了上面这两个东西,我们可以做任何转换了。

下面我们开始真正编写一个插件,分为以下几步:

  1. 在index.js中export一个函数
  2. 函数中返回一个对象,对象有一个visitor参数(必须叫visitor)
  3. 通过astexplorer.net查询出 class 对应的AST节点为 ClassDeclaration
  4. 在vistor中设置一个捕获函数 ClassDeclaration ,意思是我要捕获js代码中所有 ClassDeclaration 节点
  5. 编写逻辑代码,完成转换
module.exports = function ({ types: t }) {
 return {
  visitor: {
   ClassDeclaration(path) {
    //在这里完成转换
   }
  }
 };
}

代码中有两个参数,第一个 {types:t} 东西是从参数中解构出变量t,它其实就是babel-types文档中的t(下图红框),它是用来创建节点的:

详解如何用babel转换es6的class语法

第二个参数 path ,它是捕获到的节点对应的信息,我们可以通过 path.node 获得这个节点的AST,在这个基础上进行修改就能完成了我们的目标。

如何把es6的class转换为es5的类

上面都是预备工作,真正的逻辑从现在才开始,我们先考虑两个问题:

我们要做如下转换,首先把es6的类,转换为es5的类写法(也就是普通函数),我们观察到,很多代码是可以复用的,包括函数名字、函数内部的代码块等。

详解如何用babel转换es6的class语法 

如果不定义class中的 constructor 方法,JavaScript引擎会自动为它添加一个空的 constructor() 方法,这需要我们做兼容处理。

接下来我们开始写代码,思路是:

  1. 拿到老的AST节点
  2. 创建一个数组用来盛放新的AST节点(虽然原class只是一个节点,但是替换后它会被若干个函数节点取代) 初始化默认的 constructor 节点(上文提到,class中有可能没有定义constructor)
  3. 循环老节点的AST对象(会循环出若干个函数节点)
  4. 判断函数的类型是不是 constructor ,如果是,通过取到数据创建一个普通函数节点,并更新默认 constructor 节点
  5. 处理其余不是 constructor 的节点,通过数据创建 prototype 类型的函数,并放到 es5Fns
  6. 循环结束,把 constructor 节点也放到 es5Fns
  7. 判断es5Fns的长度是否大于1,如果大于1使用 replaceWithMultiple 这个API更新AST
module.exports = function ({ types: t }) {
 return {
  visitor: {
   ClassDeclaration(path) {
    //拿到老的AST节点
    let node = path.node
    let className = node.id.name
    let classInner = node.body.body
    //创建一个数组用来成盛放新生成AST
    let es5Fns = []
    //初始化默认的constructor节点
    let newConstructorId = t.identifier(className)
    let constructorFn = t.functionDeclaration(newConstructorId, [t.identifier('')], t.blockStatement([]), false, false)
    //循环老节点的AST对象
    for (let i = 0; i < classInner.length; i++) {
     let item = classInner[i]
     //判断函数的类型是不是constructor
     if (item.kind == 'constructor') {
      let constructorParams = item.params.length ? item.params[0].name : []
      let newConstructorParams = t.identifier(constructorParams)
      let constructorBody = classInner[i].body
      constructorFn = t.functionDeclaration(newConstructorId, [newConstructorParams], constructorBody, false, false)
     } 
     //处理其余不是constructor的节点
     else {
      let protoTypeObj = t.memberExpression(t.identifier(className), t.identifier('prototype'), false)
      let left = t.memberExpression(protoTypeObj, t.identifier(item.key.name), false)
      //定义等号右边
      let prototypeParams = classInner[i].params.length ? classInner[i].params[i].name : []
      let newPrototypeParams = t.identifier(prototypeParams)
      let prototypeBody = classInner[i].body
      let right = t.functionExpression(null, [newPrototypeParams], prototypeBody, false, false)
      let protoTypeExpression = t.assignmentExpression("=", left, right)
      es5Fns.push(protoTypeExpression)
     }

    }
    //循环结束,把constructor节点也放到es5Fns中
    es5Fns.push(constructorFn)
    //判断es5Fns的长度是否大于1
    if (es5Fns.length > 1) {
     path.replaceWithMultiple(es5Fns)
    } else {
     path.replaceWith(constructorFn)
    }
   }
  }
 };
}

优化继承

其实,类还涉及到继承,思路也不复杂,就是判断AST中没有 superClass 属性,如果有的话,我们需要多添加一行代码 Bird.prototype = Object.create(Parent) ,当然别忘了处理 super 关键字。

打包后代码

详解如何用babel转换es6的class语法 

运行 npm start 打包后,我们看到打包后的文件里 class

语法已经成功转换为一个个的es5函数。

结尾

现在一个类转换器就写完了,希望能对大家了解babel有一点帮助。也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript getElementsByClassName 和js取地址栏参数
Jan 02 Javascript
基于JQuery的Pager分页器实现代码
Jul 17 Javascript
使用js对select动态添加和删除OPTION示例代码
Aug 12 Javascript
js和php如何获取当前url的内容
Sep 22 Javascript
eclipse如何忽略js文件报错(附图)
Oct 30 Javascript
jquery树形菜单效果的简单实例
Jun 06 Javascript
javascript 的变量、作用域和内存问题
Apr 19 Javascript
Electron中实现大文件上传和断点续传功能
Oct 28 Javascript
在vue中利用全局路由钩子给url统一添加公共参数的例子
Nov 01 Javascript
ant-design-vue 快速避坑指南(推荐)
Jan 21 Javascript
uniapp电商小程序实现订单30分钟倒计时
Nov 01 Javascript
JavaScript十大取整方法实例教程
Dec 03 Javascript
为vue-router懒加载时下载js的过程中添加loading提示避免无响应问题
Apr 03 #Javascript
新版vue-cli模板下本地开发环境使用node服务器跨域的方法
Apr 03 #Javascript
使用react实现手机号的数据同步显示功能的示例代码
Apr 03 #Javascript
JS遍历DOM文档树的方法实例详解
Apr 03 #Javascript
JavaScript同源策略和跨域访问实例详解
Apr 03 #Javascript
vue 组件中slot插口的具体用法
Apr 03 #Javascript
react 实现页面代码分割、按需加载的方法
Apr 03 #Javascript
You might like
不用数据库的多用户文件自由上传投票系统(3)
2006/10/09 PHP
浅谈php自定义错误日志
2015/02/13 PHP
Redis使用Eval多个键值自增的操作实例
2016/11/04 PHP
javascript 尚未实现错误解决办法
2008/11/27 Javascript
jQuery 通过事件委派一次绑定多种事件,以减少事件冗余
2010/06/30 Javascript
实现变速回到顶部的JavaScript代码
2011/05/09 Javascript
jquery 提交值不为空的元素示例代码
2013/05/10 Javascript
在jquery中的ajax方法怎样通过JSONP进行远程调用
2014/04/04 Javascript
node.js中的path.resolve方法使用说明
2014/12/08 Javascript
JQuery实现超链接鼠标提示效果的方法
2015/06/10 Javascript
jQuery鼠标经过方形图片切换成圆边效果代码分享
2015/08/20 Javascript
Angularjs中数据绑定的实例详解
2017/08/25 Javascript
mpvue跳转页面及注意事项
2018/08/03 Javascript
vue单页面应用打开新窗口显示跳转页面的实例
2018/09/21 Javascript
浅谈vue中关于checkbox数据绑定v-model指令的个人理解
2018/11/14 Javascript
Angular使用Restful的增删改
2018/12/28 Javascript
vue + typescript + video.js实现 流媒体播放 视频监控功能
2019/07/07 Javascript
基于Taro的微信小程序模板消息-获取formId功能模块封装实践
2019/07/15 Javascript
微信小程序如何引用外部js,外部样式,公共页面模板
2019/07/23 Javascript
微信小程序 可搜索的地址选择实现详解
2019/08/28 Javascript
微信接入之获取用户头像的方法步骤
2019/09/23 Javascript
在vue中实现禁止回退上一步,路由不存历史记录
2020/07/22 Javascript
[08:38]DOTA2-DPC中国联赛 正赛 VG vs Elephant 选手采访
2021/03/11 DOTA
Python实现对excel文件列表值进行统计的方法
2015/07/25 Python
django开发之settings.py中变量的全局引用详解
2017/03/29 Python
Python函数式编程
2017/07/20 Python
利用Python批量压缩png方法实例(支持过滤个别文件与文件夹)
2017/07/30 Python
解决python写入带有中文的字符到文件错误的问题
2019/01/31 Python
python利用paramiko实现交换机巡检的示例
2020/09/22 Python
联想新西兰官方网站:Lenovo New Zealand
2018/10/30 全球购物
管理学专业个人求职信范文
2013/12/13 职场文书
群众路线个人整改措施
2014/10/24 职场文书
银行求职信模板
2015/03/20 职场文书
公司费用报销管理制度
2015/08/04 职场文书
springboot中rabbitmq实现消息可靠性机制详解
2021/09/25 Java/Android
zabbix自定义监控nginx状态实现过程
2021/11/01 Servers