了解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
  };
}
相关推荐
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255023 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport5 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg5 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全