详解如何用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 相关文章推荐
JS写的贪吃蛇游戏(个人练习)
Jul 08 Javascript
Jquery实现兼容各大浏览器的Enter回车切换输入焦点的方法
Sep 01 Javascript
JavaScript限定图片显示大小的方法
Mar 11 Javascript
IE6兼容透明背景图片及解决方案
Aug 19 Javascript
javascript 使用正则test( )第一次是 true,第二次是false
Feb 22 Javascript
JS简单实现获取元素的封装操作示例
Apr 07 Javascript
Vue 中使用vue2-highcharts实现曲线数据展示的方法
Mar 05 Javascript
在vue项目中使用sass的配置方法
Mar 20 Javascript
vue里如何主动销毁keep-alive缓存的组件
Mar 21 Javascript
Vue+Element实现动态生成新表单并添加验证功能
May 23 Javascript
微信小程序实现左侧滑栏过程解析
Aug 26 Javascript
js获取本日、本周、本月的时间代码
Feb 01 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
PHP那些琐碎的知识点(整理)
2017/05/20 PHP
Yii2使用表单上传文件的实例代码
2017/08/03 PHP
php web环境和命令行环境下查找php.ini的位置
2019/07/17 PHP
jQuery UI Dialog控件中的表单无法正常提交的解决方法
2010/12/19 Javascript
JavaScript 判断日期格式是否正确的实现代码
2011/07/04 Javascript
jQuery 绑定事件到动态创建的元素上的方法实例
2013/08/18 Javascript
javascript模拟实现ajax加载框实例
2014/10/15 Javascript
使用javascript实现json数据以csv格式下载
2015/01/09 Javascript
jQuery浏览器CSS3特写兼容实例
2015/01/19 Javascript
使用pjax实现无刷新更改页面url
2015/02/05 Javascript
jQuery时间轴插件使用详解
2015/07/16 Javascript
利用jQuery来动态为属性添加或者删除属性的简单方法
2016/12/02 Javascript
angular2+nodejs实现图片上传功能
2017/03/27 NodeJs
ES6中javascript实现函数绑定及类的事件绑定功能详解
2017/11/08 Javascript
JavaScript创建对象方法实例小结
2018/09/03 Javascript
vue学习笔记五:在vue项目里面使用引入公共方法详解
2019/04/04 Javascript
vue + elementUI实现省市县三级联动的方法示例
2019/10/29 Javascript
原生js实现html手机端城市列表索引选择城市
2020/06/24 Javascript
Python多线程编程(六):可重入锁RLock
2015/04/05 Python
浅谈Python NLP入门教程
2017/12/25 Python
Python3.6连接Oracle数据库的方法详解
2018/05/18 Python
Python中的单行、多行、中文注释方法
2018/07/19 Python
python使用pygame实现笑脸乒乓球弹珠球游戏
2019/11/25 Python
Python 脚本的三种执行方式小结
2019/12/21 Python
Python插入Elasticsearch操作方法解析
2020/01/19 Python
python使用gdal对shp读取,新建和更新的实例
2020/03/10 Python
使用keras实现非线性回归(两种加激活函数的方式)
2020/07/05 Python
ECCO爱步加拿大官网:北欧丹麦鞋履及皮具品牌
2017/07/08 全球购物
C#里面可以避免一个类被其他类继承么?如何?
2013/09/26 面试题
九州传奇上机题
2014/07/10 面试题
采购主管的岗位职责
2013/12/17 职场文书
人力资源作业细则
2014/03/03 职场文书
社团2014年植树节活动总结
2014/03/11 职场文书
电焊工岗位工作职责
2014/07/09 职场文书
将MySQL的表数据全量导入clichhouse库中
2022/03/21 MySQL
Python简易开发之制作计算器
2022/04/28 Python