入门babel--实现一个es6的class转换器

      babel是一个转码器,目前开发react、vue项目都要使用到它。它能够把es6+的语法转换为es5,也能够转换JSX等语法等,实际上他能经过自定义插件的方式完成任意转换。
      咱们在项目中都是经过配置插件和预设(多个插件的集合)来转换特定代码,例如env、stage-0等。那么这些库是如何实现的呢,下面就经过一个小例子探究一下--把es6的class转换为es5。
vue

文章结构:

webpack环境配置

      你们应该都配置过babel-core这个loader,实际上它的做用只是提供babel的核心Api,咱们的代码转换其实都是经过插件来实现的。
      接下来咱们不用第三方的插件,本身实现一个es6类转换插件。先执行如下几步初始化一个项目:
node

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

      若是咱们的插件名字想叫transform-class,须要在webpack配置中作以下配置:react

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

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

如何编写bable插件

      babel插件实际上是经过AST(抽象语法树)实现的。
      babel帮助咱们把js代码转换为AST,而后容许咱们修改,最后再把它转换成js代码。
      那么就涉及到两个问题:js代码和AST之间的映射关系是什么?如何替换或者新增AST?
es6

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

      这个工具能够把一段代码转换为AST: github

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

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

      这是建立AST节点的Api文档。
      好比,咱们想建立一个类,先到astexplorer.net中转换,发现类对应的AST类型是ClassDeclaration。好,咱们去文档中搜索,发现调用下面的api就能够建立这样一个节点: web

      同理,建立其余节点也是同样的道理。有了上面这两个东西,咱们就能够作任何转换了。

      下面咱们开始真正编写一个插件,分为如下几步:npm

  • 在index.js中export一个函数
  • 函数中返回一个对象,对象有一个visitor参数(必须叫visitor)
  • 经过astexplorer.net查询出class对应的AST节点为ClassDeclaration
  • 在vistor中设置一个捕获函数ClassDeclaration,意思是我要捕获js代码中全部ClassDeclaration节点
  • 编写逻辑代码,完成转换

      上面的步骤对应成代码:api

module.exports = function ({ types: t }) {
    return {
        visitor: {
            ClassDeclaration(path) {
                //在这里完成转换
            }
        }
    };
}
复制代码

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

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

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

上面都是预备工做,真正的逻辑从如今才开始,咱们先考虑两个问题:
  1. 咱们要作以下转换,首先把es6的类,转换为es5的类写法(也就是普通函数),咱们观察到,不少代码是能够复用的,包括函数名字、函数内部的代码块等。

  1. 若是不定义class中的constructor方法,JavaScript引擎会自动为它添加一个空的constructor()方法,这须要咱们作兼容处理。
接下来咱们开始写代码,思路是:
  • 拿到老的AST节点
  • 建立一个数组用来盛放新的AST节点(虽然原class只是一个节点,可是替换后它会被若干个函数节点取代)
  • 初始化默认的constructor节点(上文提到,class中有可能没有定义constructor)
  • 循环老节点的AST对象(会循环出若干个函数节点)
  • 判断节点的类型是否是constructor,若是是,经过老数据建立一个普通函数节点,并更新默认constructor节点
  • 处理其他不是constructor的节点,经过老数据建立prototype类型的函数,并放到es5Fns
  • 循环结束,把constructor节点也放到es5Fns
  • 判断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关键字。

打包后代码

      运行 npm start打包后,咱们看到打包后的文件里 class语法已经成功转换为一个个的es5函数。

结尾

      如今一个类转换器就写完了,但愿能对你们了解babel有一点帮助。

参考内容

github-babel插件开发指南
babel-types

相关文章
相关标签/搜索