babel 是一个转码器, 目前开发 react,vue 项目都要使用到它. 它可以把 es6 + 的语法转换为 es5, 也可以转换 JSX 等语法.
我们在项目中都是通过配置插件和预设 (多个插件的集合) 来转换特定代码, 例如 env,stage-0 等.
实际上 babel 可以通过自定义插件的方式实现任何代码的转换, 接下来我们通过一个 "把 es6 的 class 转换为 es5" 的例子来了解一下 babel.
内容如下:
webpack 环境配置
大家应该都配置过 babel-core 这个 loader, 它的作用是提供 babel 的核心 Api, 实际上我们的代码转换都是通过插件来实现的.
接下来我们不用第三方的插件, 自己实现一个 es6 类转换插件. 先执行以下几步初始化一个项目:
npm install webpack webpack-cli babel-core -D
新建一个 webpack.config.js
配置 webpack.config.js
如果我们的插件名字想叫 transform-class, 需要在 webpack 配置中做如下配置:
接下来我们在 node_modules 中新建一个 babel-plugin-transform-class 的文件夹来写插件的逻辑(如果是真实项目, 你需要编写这个插件并发布到 npm 仓库), 如下图:
红色区域是我新建的文件夹, 它上面的是一个标准的插件的项目结构, 为了方便我只写了核心的 index.js 文件.
如何编写 bable 插件
babel 插件其实是通过 AST(抽象语法树)实现的.
babel 帮助我们把 js 代码转换为 AST, 然后允许我们修改, 最后再把它转换成 js 代码.
那么就涉及到两个问题: js 代码和 AST 之间的映射关系是什么? 如何替换或者新增 AST?
好, 先介绍一个工具: http://astexplorer.net/ :
这个工具可以把一段代码转换为 AST:
如图, 我们写了一个 es6 的类, 然后网页的右边帮我们生成了一个 AST, 其实就是把每一行代码变成了一个对象, 这样我们就实现了一个映射.
再介绍一个文档: babel-types https://github.com/babel/babel/tree/master/packages/babel-types :
这是创建 AST 节点的 api 文档.
比如, 我们想创建一个类, 先到 astexplorer.net 中转换, 发现类对应的 AST 类型是 ClassDeclaration . 好, 我们去文档中搜索, 发现调用下面的 api 就可以了:
创建其他语句也是一样的道理, 有了上面这两个东西, 我们可以做任何转换了.
下面我们开始真正编写一个插件, 分为以下几步:
在 index.js 中 export 一个函数
函数中返回一个对象, 对象有一个 visitor 参数(必须叫 visitor)
通过 astexplorer.net 查询出 class 对应的 AST 节点为 ClassDeclaration
在 vistor 中设置一个捕获函数 ClassDeclaration , 意思是我要捕获 js 代码中所有 ClassDeclaration 节点
编写逻辑代码, 完成转换
- module.exports = function ({ types: t }) {
- return {
- visitor: {
- ClassDeclaration(path) {
- // 在这里完成转换
- }
- }
- };
- }
代码中有两个参数, 第一个 {types:t} 东西是从参数中解构出变量 t, 它其实就是 babel-types 文档中的 t(下图红框), 它是用来创建节点的:
第二个参数 path , 它是捕获到的节点对应的信息, 我们可以通过 path.node 获得这个节点的 AST, 在这个基础上进行修改就能完成了我们的目标.
如何把 es6 的 class 转换为 es5 的类
上面都是预备工作, 真正的逻辑从现在才开始, 我们先考虑两个问题:
我们要做如下转换, 首先把 es6 的类, 转换为 es5 的类写法(也就是普通函数), 我们观察到, 很多代码是可以复用的, 包括函数名字, 函数内部的代码块等.
如果不定义 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 函数.
结尾
来源: http://www.jb51.net/article/137692.htm