详解如何用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 03 Javascript
Jquery遍历Json数据的方法
Apr 20 Javascript
jquery实现移动端点击图片查看大图特效
Sep 11 Javascript
js实现文字闪烁特效的方法
Dec 17 Javascript
一句jQuery代码实现返回顶部效果(简单实用)
Dec 28 Javascript
使用Angular CLI生成 Angular 5项目教程详解
Mar 18 Javascript
使用layer弹窗和layui表单实现新增功能
Aug 09 Javascript
深入理解JS中Number(),parseInt(),parseFloat()三者比较
Aug 24 Javascript
vue.js多页面开发环境搭建过程
Apr 24 Javascript
微信小程序常用赋值方法小结
Apr 30 Javascript
layui实现多图片上传并限制上传的图片数量
Sep 26 Javascript
解决Vue + Echarts 使用markLine标线(precision精度问题)
Jul 20 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支持页面回退的两种方法
2008/01/10 PHP
php 操作excel文件的方法小结
2009/12/31 PHP
PHP微框架Dispatch简介
2014/06/12 PHP
PHP实现生成透明背景的PNG缩略图函数分享
2014/07/08 PHP
PHP上传文件参考配置大文件上传
2015/12/16 PHP
Zend Framework教程之Zend_Db_Table表关联实例详解
2016/03/23 PHP
PHP面向对象程序设计之命名空间与自动加载类详解
2016/12/02 PHP
Yii2.0框架behaviors方法使用实例分析
2019/09/30 PHP
?牟┛途W扣了一??效果出?? target=
2007/05/27 Javascript
javascript+css 网页每次加载不同样式的实现方法
2009/12/27 Javascript
LazyLoad 延迟加载(按需加载)
2010/05/31 Javascript
jQuery ul标签下拉菜单演示代码
2010/12/11 Javascript
jquery引用方法时传递参数原理分析
2014/10/13 Javascript
浅谈EasyUI中Treegrid节点的删除
2015/03/01 Javascript
JavaScript DOM进阶方法
2015/04/13 Javascript
js接收并转化Java中的数组对象的方法
2016/08/11 Javascript
JavaScript面向对象编写购物车功能
2016/08/19 Javascript
NodeJS实现客户端js加密
2017/01/09 NodeJs
vue 粒子特效的示例代码
2017/09/19 Javascript
EasyUI Tree树组件无限循环的解决方法
2017/09/27 Javascript
微信小程序 input输入及动态设置按钮的实现
2017/10/27 Javascript
js正则取值的结果数组调试方法
2018/10/10 Javascript
js中的数组对象排序分析
2018/12/11 Javascript
nodejs实现日志读取、日志查找及日志刷新的方法分析
2019/05/20 NodeJs
[48:22]VGJ.S vs VG 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
python基础教程之Filter使用方法
2017/01/17 Python
Python标准模块--ContextManager上下文管理器的具体用法
2017/11/27 Python
Python3.5面向对象编程图文与实例详解
2019/04/24 Python
pandas删除行删除列增加行增加列的实现
2019/07/06 Python
通过实例解析Python文件操作实现步骤
2020/09/21 Python
html5记忆翻牌游戏实现思路及代码
2013/07/25 HTML / CSS
印尼在线购买隐形眼镜网站:Lensza.co.id
2019/04/27 全球购物
素食餐饮项目创业计划书
2014/02/02 职场文书
交通安全教育制度
2014/02/02 职场文书
企业业务员岗位职责
2014/03/14 职场文书
MySQL数据库表约束讲解
2022/06/21 MySQL