详解如何用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 Split方法,indexOf方法、lastIndexOf 方法和substring 方法
Mar 21 Javascript
Javascript 继承机制的实现
Aug 12 Javascript
一个挺有意思的Javascript小问题说明
Sep 26 Javascript
javascript中利用柯里化函数实现bind方法【推荐】
Apr 29 Javascript
Bootstrap php制作动态分页标签
Dec 23 Javascript
js实现无缝滚动图(可控制当前滚动的方向)
Feb 22 Javascript
BootStrap 获得轮播中的索引和当前活动的焦点对象
May 11 Javascript
使用淘宝镜像cnpm安装Vue.js的图文教程
May 17 Javascript
解决Vue axios post请求,后台获取不到数据的问题方法
Aug 11 Javascript
javascript实现贪吃蛇经典游戏
Apr 10 Javascript
小程序分享链接onShareAppMessage的具体用法
May 22 Javascript
JS前端可视化canvas动画原理及其推导实现
Aug 05 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&amp;&amp;mysql)三
2006/10/09 PHP
PHP关于IE下的iframe跨域导致session丢失问题解决方法
2013/10/10 PHP
分享一个Laravel好用的Cache宏
2015/03/02 PHP
jquery 简短几句代码实现给元素动态添加及获取提示信息
2011/09/01 Javascript
在js(jquery)中获得文本框焦点和失去焦点的方法
2012/12/04 Javascript
JS仿百度搜索自动提示框匹配查询功能
2013/11/21 Javascript
js实现图片放大和拖拽特效代码分享
2015/09/05 Javascript
JAVASCRIPT代码编写俄罗斯方块网页版
2015/11/26 Javascript
JS实现部分HTML固定页面顶部随屏滚动效果
2015/12/24 Javascript
基于JavaScript实现自定义滚动条
2017/01/25 Javascript
简单的vue-resourse获取json并应用到模板示例
2017/02/10 Javascript
ES6教程之for循环和Map,Set用法分析
2017/04/10 Javascript
详解vue的数据binding绑定原理
2017/04/12 Javascript
arctext.js实现文字平滑弯曲弧形效果的插件
2019/05/13 Javascript
jQuery编写QQ简易聊天框
2020/08/27 jQuery
基于element-ui对话框el-dialog初始化的校验问题解决
2020/09/11 Javascript
Python正则表达式匹配ip地址实例
2014/10/09 Python
Python3实现将文件归档到zip文件及从zip文件中读取数据的方法
2015/05/22 Python
Python使用cookielib模块操作cookie的实例教程
2016/07/12 Python
Python网络爬虫项目:内容提取器的定义
2016/10/25 Python
每天迁移MySQL历史数据到历史库Python脚本
2018/04/13 Python
Anaconda下安装mysql-python的包实例
2018/06/11 Python
Python使用一行代码获取上个月是几月
2018/08/30 Python
python 读取鼠标点击坐标的实例
2018/12/29 Python
Python绘图实现显示中文
2019/12/04 Python
重写django的model下的objects模型管理器方式
2020/05/15 Python
python 下划线的不同用法
2020/10/24 Python
python Timer 类使用介绍
2020/12/28 Python
Html5调用手机摄像头并实现人脸识别的实现
2018/12/21 HTML / CSS
自荐信格式的六要素
2013/09/21 职场文书
毕业实习自我鉴定范文2014
2014/09/26 职场文书
公务员年度考核登记表个人总结
2015/02/12 职场文书
婚礼男方父母答谢词
2015/09/29 职场文书
你为什么是穷人?可能是这5个缺点造成
2019/07/11 职场文书
导游词之新疆-喀纳斯
2019/10/10 职场文书
《黑岩★★射手 DAWN FALL》BD发售宣传CM公开
2022/04/04 日漫