了解Babel原理和手写一个babel插件

babel 简介

Babel 是一个 JavaScript 编译器,它能将 es2015,react 等低端浏览器无法识别的语言,进行编译。上图的左边代码中有箭头函数,Babel 将进行了源码转换,下面我们来看 Babel 的运行原理。

Babel 运行原理

Babel 的三个主要处理步骤分别是:

解析(parse),转换(transform),生成(generate)。

其过程分解用语言描述的话,就是下面这样:

解析

使用 babylon 解析器对输入的源代码字符串进行解析并生成初始 AST(File.prototype.parse)

利用 babel-traverse 这个独立的包对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程

转换

transform 过程:遍历 AST 树并应用各 transformers(plugin) 生成变换后的 AST 树,babel 中最核心的是 babel-core,它向外暴露出 babel.transform 接口。

let result = babel.transform(code, {    plugins: [        arrayPlugin    ]})

生成

利用 babel-generator 将 AST 树输出为转码后的代码字符串

AST 解析

AST 解析会把拿到的语法,进行树形遍历,对语法的每个节点进行响应的变化和改造再生产新的代码字符串

节点(node)

AST将开头提到的箭头函数转根据节点换为节点树

ES2015 箭头函数

codes.map(code=>{	return code.toUpperCase()})

AST 树形遍历转换后的结构

{    type:"ExpressionStatement",    expression:{        type:"CallExpression"        callee:{            type:"MemberExpression",            computed:false            object:{                type:"Identifier",                name:"codes"            }            property:{                type:"Identifier",                name:"map"            }            range:[]        }        arguments:{            {                type:"ArrowFunctionExpression",                id:null,                params:{                    type:"Identifier",                    name:"code",                    range:[]                }                body:{                    type:"BlockStatement"                    body:{                        type:"ReturnStatement",                        argument:{                            type:"CallExpression",                            callee:{                                type:"MemberExpression"                                computed:false                                object:{                                    type:"Identifier"                                    name:"code"                                    range:[]                                }                                property:{                                    type:"Identifier"                                    name:"toUpperCase"                                }                                range:[]                            }                            range:[]                        }                    }                    range:[]                }                generator:false                expression:false                async:false                range:[]            }        }    }}

我们从 ExpressionStatement 开始往树形结构里面走,看到它的内部属性有 callee、type、arguments,所以我们再依次访问每一个属性及它们的子节点。

于是就有了如下的顺序

进入  ExpressionStatement进入  CallExpression进入  MemberExpression进入  Identifier离开  Identifier进入  Identifier离开  Identifier离开  MemberExpression进入  ArrowFunctionExpression进入  Identifier离开  Identifier进入  BlockStatement进入  ReturnStatement进入  CallExpression进入  MemberExpression进入  Identifier离开  Identifier进入  Identifier离开  Identifier离开  MemberExpression离开  CallExpression离开  ReturnStatement离开  BlockStatement离开  ArrowFunctionExpression离开  CallExpression离开  ExpressionStatement离开  Program

Babel 的转换步骤全都是这样的遍历过程。有点像 koa 的洋葱模型?

AST转换

解析好树结构后,我们手动对箭头函数进行转换。发现不一样的地方就是两个函数的 arguments.type

let babel = require('babel-core');//babel核心库let types = require('babel-types');let code = `codes.map(code=>{return code.toUpperCase()})`;// 转换语句
let visitor = {
ArrowFunctionExpression(path) {//定义需要转换的节点
let params = path.node.params
let blockStatement = path.node.body
let func = types.functionExpression(null, params, blockStatement, false, false)
path.replaceWith(func) //
}
}
let arrayPlugin = { visitor }
let result = babel.transform(code, {
plugins: [
arrayPlugin
]
})
console.log(result.code)

注意: ArrowFunctionExpression() { ... }ArrowFunctionExpression: { enter() { ... } } 的简写形式。

Path 是一个对象,它表示两个节点之间的连接。

解析步骤

定义需要转换的节点

    ArrowFunctionExpression(path) {        ......    }

创建用来替换的节点

types.functionExpression(null, params, blockStatement, false, false)

babel-types 文档链接

  • 在 node 节点上找到需要的参数
  • replaceWith(替换)

手写一个babel插件

作用是给async的方法批量增加try catch

const template = require('@babel/template');

// 定义try语句模板     catch要打印的信息  合并选项  判断执行的file文
const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util')

module.exports = function (babel) {
  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
  let types = babel.types;
  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
  const visitor = {
    AwaitExpression(path) {
      // 通过this.opts 获取用户的配置
      if (this.opts && !typeof this.opts === 'object') {
        return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
      }
      // 判断父路径中是否已存在try语句,若存在直接返回
      if (path.findParent((p) => p.isTryStatement())) {
        return false;
      }
      // 合并插件的选项
      const options = mergeOptions(this.opts);
      // 获取编译目标文件的路径,如:D:\myapp\src\App.vue
      const filePath = this.filename || this.file.opts.filename || 'unknown';
      // 对排除列表的文件不编译
      if (matchesFile(options.exclude, filePath)) {
        return;
      }
      // 如果设置了include,只编译include中的文件
      if (options.include.length && !matchesFile(options.include, filePath)) {
        return;
      }
      // 获取当前的await节点
      let node = path.node;
      // 在父路径节点中查找声明 async 函数的节点
      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
      const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
      // 获取async的方法名
      let asyncName = '';

      let type = asyncPath.node.type;
      switch (type) {
        // 1️⃣函数表达式
        // 情况1:普通函数,如const func = async function () {}
        // 情况2:箭头函数,如const func = async () => {}
        case 'FunctionExpression':
        case 'ArrowFunctionExpression':
          // 使用path.getSibling(index)来获得同级的id路径
          let identifier = asyncPath.getSibling('id');
          // 获取func方法名
          asyncName = identifier && identifier.node ? identifier.node.name : '';
          break;

        // 2️⃣函数声明,如async function fn2() {}
        case 'FunctionDeclaration':
          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
          break;

        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
        case 'ObjectMethod':
          asyncName = asyncPath.node.key.name || '';
          break;
      }
      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
      let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';

      const temp = template(tryTemplate);

      // 给模版增加key,添加console.log打印信息
      let tempArgumentObj = {
        // 通过types.stringLiteral创建字符串字面量
        CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
      };

      // 通过temp创建try语句
      let tryNode = temp(tempArgumentObj);

      // 获取async节点(父节点)的函数体
      let info = asyncPath.node.body;

      // 将父节点原来的函数体放到try语句中
      tryNode.block.body.push(...info.body);

      // 将父节点的内容替换成新创建的try语句
      info.body = [tryNode];
    }
  };
  return {
    name: 'babel-plugin-await-add-trycatch',
    visitor
  };
}
相关推荐
也无晴也无风雨24 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational2 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui