详解如何用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 相关文章推荐
jQuery实现可拖动的浮动层完整代码
May 27 Javascript
Jquery获取元素的父容器对象示例代码
Feb 10 Javascript
JS实现超过长度限制后自动跳转下一款文本框的方法
Feb 23 Javascript
js html css实现复选框全选与反选
Oct 09 Javascript
jQuery插件form-validation-engine正则表达式操作示例
Feb 09 Javascript
Canvas 绘制粒子动画背景
Feb 15 Javascript
微信小程序网络请求wx.request详解及实例
May 18 Javascript
浅谈Vue路由快照实现思路及其问题
Jun 07 Javascript
vue实现循环滚动列表
Jun 30 Javascript
使用 Github Actions 自动部署 Angular 应用到 Github Pages的方法
Jul 20 Javascript
vue双击事件2.0事件监听(点击-双击-鼠标事件)和事件修饰符操作
Jul 27 Javascript
vue实现两个区域滚动条同步滚动
Dec 13 Vue.js
为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
php跨站攻击实例分析
2014/10/28 PHP
分享8个Laravel模型时间戳使用技巧小结
2020/02/12 PHP
jquery.jstree 增加节点的双击事件代码
2010/07/27 Javascript
Js获取事件对象代码
2010/08/05 Javascript
基于jQuery的遍历同id元素 并响应事件的代码
2012/06/14 Javascript
html+css+js实现xp window界面及有关功能
2013/03/26 Javascript
浅谈JavaScript Math和Number对象
2015/01/26 Javascript
基于JavaScript实现高德地图和百度地图提取行政区边界经纬度坐标
2016/01/22 Javascript
BootStrap学习系列之Bootstrap Typeahead 组件实现百度下拉效果(续)
2016/07/07 Javascript
javascript函数中的3个高级技巧
2016/09/22 Javascript
JS实现控制图片显示大小的方法【图片等比例缩放功能】
2017/02/18 Javascript
Angularjs处理页面闪烁的解决方法
2017/03/09 Javascript
Angular模版驱动表单的使用总结
2018/05/05 Javascript
karma+webpack搭建vue单元测试环境的方法示例
2018/05/24 Javascript
vue项目部署上线遇到的问题及解决方法
2018/06/10 Javascript
Vue 页面权限控制和登陆验证功能的实例代码
2019/06/20 Javascript
解决vue中使用proxy配置不同端口和ip接口问题
2019/08/14 Javascript
Vue路由守卫之路由独享守卫
2019/09/25 Javascript
详解Vue Cli浏览器兼容性实践
2020/06/08 Javascript
Python中functools模块函数解析
2017/03/12 Python
TensorFlow实现模型评估
2018/09/07 Python
Pandas统计重复的列里面的值方法
2019/01/30 Python
关于pymysql模块的使用以及代码详解
2019/09/01 Python
Python操作Mongodb数据库的方法小结
2019/09/10 Python
基于python爬取有道翻译过程图解
2020/03/31 Python
python3.8.3安装教程及环境配置的详细教程(64-bit)
2020/11/28 Python
html5 实现客户端验证上传文件的大小(简单实例)
2016/05/15 HTML / CSS
工程部主管岗位职责
2013/11/17 职场文书
优秀大学生推荐信范文
2013/11/28 职场文书
工商企业管理实习自我鉴定
2013/12/04 职场文书
打架检讨书500字
2014/01/29 职场文书
团购业务员岗位职责
2014/03/15 职场文书
高中生第一学年自我鉴定
2014/09/12 职场文书
期末复习计划
2015/01/19 职场文书
2015年新学期寄语
2015/02/26 职场文书
nodejs利用readline提示输入内容实例代码
2021/07/15 NodeJs