Babel解析原理剖析

背景

Javascript属于web编程语言,本身语法设计存在缺陷,随着ES5、ES6、ES7等各个版本的推出,语法不断完善,但浏览器的兼容问题成为了目前最大的痛点,typescript的相继推出在一定程度上弥补了js语法本身的不足,但不能被浏览器识别,那么如何将tsjsxes6+这些代这些代码转换成浏览器可识别的代码呢?Babel的出现解决了这一问题。

什么是Babel

Babel 是一个 JavaScript 编译器

Babel 是一个JavaScript编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel能够实现的功能如下:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
  • 源码转换 (codemods)
  • 代码压缩混淆
  • ....

执行原理

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gq9x67ypxuj314205kjs5.jpg" alt="image-20210507155904069" style="zoom:50%;" />

parser解析

Babel会将拿到的源码提取出来,以一种抽象的树状语法结构表示,该结构被称为抽象语法树AST,全称为: Abstract Syntax Tree。当前解析阶段主要分为两个步骤:**词法分析 **和 语法分析

词法分析

将代码字符串转换成Token流(即将源码分割成最小单元),可以将Token流理解成一种扁平的语法片段数组,数组中的每一项都有一组属性来描述该Token。

代码中的语法单元主要包括以下几类:

关键字(let、const等)、标识符、运算符、数字、空格、注释;

根据语法类型,将源码字符串按指定的规则转换成数组片段,如下:

[ 
  { 
    "type" :  "Identifier" , 
    "value" :  "console"
   }, 
  { 
    "type" :  "Punctuator" , 
    "value" :  "."
   }, 
  { 
    "type" :  "Identifier" , 
    "value" :  "log"
   }, 
  { 
    "type" :  "Punctuator" , 
    "value" :  "("
   }, 
  { 
    "type" :  "String" ,
    "value" :  "'zcy'"
   }, 
  { 
    "type" : "Punctuator" , 
    "value" :  ")"
   }, 
  { 
    "type" :  "Punctuator" , 
    "value" :  ";"
   } 
]
语法分析

该阶段会将Token流转换为AST形式,将Token中的描述属性添加到AST语法结构中,方便后期操作语法树。

在babel中,将源代码转换成AST语法树的插件是@babel/parser

转换前:

const name = 'Jack';
const list = ['Jack','Joe'];
list.map(val=>{
    if( val == name ){
        alert('找到了')
    }
})

转换后:

当前AST描述了每一部分代码以及它们之间的关系:其中VariableDeclaration表示声明变量,kind表示声明类型如:const、let、var,function函数都属于ExpressionStatement类别;

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gq9xzxxw78j30uk0u0n2w.jpg" alt="image-20210507162740302" style="zoom:50%;" />

transform语法转换

该步骤实现了对AST语法树的遍历,对AST节点的操作,包括增加、删除、修改等,此时就需要一系列具备相应功能的插件来进行转换操作,例如ES6转ES5、jsx语法转js等;

当源代码通过@babel/parser 转换成AST后,通过配置一系列的pluginspresets转换成新的AST语法,

plugins

@babel/traverse

其中plugins 包括:@babel/traverse,该插件是一个对AST语法遍历的工具:

以下是AST语法树中import导入方法的节点描述信息

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gqtd4gdjoyj30tk0rw41s.jpg" alt="image-20210524113640574" style="zoom:50%;" />

使用traverse工具遍历查找

const traverse = require('babel-traverse').default
// 遍历查找指定节点
traverse(astContext, {
  // 查找import导入
  ImportDeclaration: ({ node }) => {
    if (node.source.value.match(regRules)) {

      node.specifiers && node.specifiers.map((item,index)=>{
        if( item.imported&&item.imported.name ){

        }
      })

    }
  }
});

traverse(ast, {
    enter(path){
    // isExpressionStatement查询符合条件的表达式节点
    if(path.isExpressionStatement(){
            console.log(path.node.start)
        }
    // isIdentifier查询符合条件的节点
        if(path.isIdentifier({name: 'test'})){
            console.dir(path.node)
        }
    if(){
       
    }
    }
})

presets

其中presets主要包括:

es2015 / es2016 / es2017 / env / stage-0 / stage-4,其中esXX表示转换成指定年份标准,env是最新标准,stage 系列集合了一些对 es7 的草案支持的插件,由于是草案,所以作为插件的形式提供。

以上pluginspresets主要是.babelrc配置文件中的参数;

ES各版本语法间的转换

babel将ES6及以上版本分为语法层api层:

语法层:let、const、class、=>等需要在语法层面上进行转义,Babel针对每一个新语法都提供了相应的转换插件,命名格式为:@babel/plugin-xxx,为避免一个一个的配置,通常使用 @babel/preset-env插件集,将新语法转换为var,function等;

api层:Promise、includes、map等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重
新定义,需要使用@babel/polyfill(babel7.4后废弃,由 core-js 替代)进行单独转译;

转换前:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `(${this.x},${this.y})`;
  }
}

转换后:

"use strict";
var Point = /*#__PURE__*/function () {
  function Point(x, y) {
    _classCallCheck(this, Point);

    this.x = x;
    this.y = y;
  }
  _createClass(Point, [{
    key: "toString",
    value: function toString() {
      return "(".concat(this.x, ",").concat(this.y, ")");
    }
  }]);
  return Point;
}();

generat生成器

在transform阶段实现的是对ast代码节点的替换和重组,接下来进入generat阶段,@babel/generator模块是代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

import { parse } from "@babel/parser";
import generate from "@babel/generator";
const code = "class Example {}";
const ast = parse(code);
const output = generate(
  ast,
  {
    /* options */
  },
  code
);

如何编写Babel插件

Babel插件基础结构:

// 该function传入babel对象或者babel.types
export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ImportDeclaration(path, state) {}
    }
  };
};

参数详解

  • types:传入的是babel.types,该工具集主要负责AST节点操作
  • visitor:babel通过递归的方式访问节点,该属性为插件的主要访问者
  • path:是代表AST节点之间关联关系的一个对象,可以通过path访问节点属性
  • state:插件状态,可访问插件配置项
  • Identifier,ImportDeclaration:AST的节点类型,主要包括标识符(Identifier)、函数声明(FunctionDeclaration)、表达式(VariableDeclaration)等

了解了Bable插件的基本结构,下面我们来写一个简单的变量名替换插件:将代码中变量名为a的统一替换成b;

我们先来看下转化后的语法树结构:

<img src="https://tva1.sinaimg.cn/large/008i3skNly1gqtlwq2kuyj30qy0jqabn.jpg" alt="image-20210524164039276" style="zoom:50%;" />

变量方法如下:

//plugin.js
export default function({ types: t }) {
  return {
    visitor: {
      VariableDeclarator(path, state) {
        if (path.node.id.name == 'a') {
          path.node.id = t.identifier('b')
        }
      }
    }
  }
}

调用代码:

// index.js
var a = "Hello!"; 

转换命令:

npx babel --plugins ./plugin.js index.js

Babel常用包及功能

@babel/core 核心编译器,包含了@babel/parser@babel/traverse@babel/generator

@babel/parser 将源码转换成AST语法树

@babel/traverse 遍历AST节点,实现对节点的增删改查

@babel/generator 将修改后的AST语法树编译成代码字符串

@babel/cli 终端命令集

@babel/preset-env 环境预设,包含了es2015,es2016, es2017等最新的语法转化插件

@babel/polyfill 兼容JS高本版全局或者Object、Array等的原型部分语法

总结

通过对Babel插件的学习,我们了解了Babel的编译原理,以及如何手动实现一个Babel插件,希望能够帮助我们更深入的去学习这一系列JS工具,在今后的项目开发过程中能够结合市面上已有工具来定制化开发指定功能的插件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容