Skip to content

Latest commit

 

History

History
164 lines (143 loc) · 9.09 KB

Babel.md

File metadata and controls

164 lines (143 loc) · 9.09 KB
title date tags categories
这一次我真的搞懂了Babel
2020-04-12
js基础
前端基础

::: tip 前言: 之前虽然大致看了看Babel相关的知识,但是并没有深入了解。本文前半部分先详细介绍一下Babel是如何编译JS代码并且转化为浏览器能够理解的代码, 后半部分将会实现一个简单的babel插件。 :::

什么是Babel?

Babel is a compiler for writing next generation JavaScript. 
Babel是编写下一代JavaScript的编译器。

Babel的作用?

帮我们编写的es6,es7...解析成浏览器能够理解的代码

Babel是如何完成解析工作的?

  • 解析: 使用babel-parser将我们编写的高级JS代码转化成AST语法树。
  • 转换: 配合babel-traverse将AST语法树进行遍历转换。
  • 生成: 使用babel-generator将转换后的AST语法树转化为JS代码。
接下来我们开始第一步:解析
  • 在开始之前我们先介绍一下这一步涉及到的名词: AST。它的全称是Abstract Syntax Tree(抽象语法树)。在百度百科中, 它是这么介绍的: 在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码的结构。其实简单可以理解为:它就是我们所编写代码的树状结构化的一种表现形式

  • 有了基本的概念之后我们来举一个例子来说明一下这个过程。

    function getResult(x, y) {
      return x + y;
    }
    

    在下面的解释中会出现一些关键词。我们先不用深究,先理解这个过程。首先当我们的babel-parser遇到这个代码块的时候,他会认为这是一个FunctionDeclaration(函数定义)对象。然后他会先把这一大块拆成三个小块:

    • id: 就是它的名称标识getResult
    • 两个参数: [x,y]
    • 一大块Body: { return x + y }

    第一块已经无法继续拆下去了那么它就会被解析成:

    {
      name: 'getResult'
      type: 'Identifier',
      ...
    }
    

    第二块就是一个数组,它也无法拆下去了,同样的它被解析成:

    [
      {
      name: 'x'
      type: 'Identifier',
      ...
      },
      {
      name: 'y'
      type: 'Identifier',
      ...
      }
    ]
    

    第三块是一个Body,很明显第三块还可以继续拆: 首先解析器解析到了一个BlockStatement(块区域)对象, 它用来表示 { return x + y }.继续解析它我们得到了一个 ReturnStatement(Return域)对象, 它用来表示return x + y;我们再解析它,我们又得到了一个BinaryExpression(二项式)对象,它用来表示 x + y;我们再解析这个二项式对象,它分成了三部分, left, operator, right

    • left: x
    • operator: +
    • right: y 下面这张图就是这个function解析后AST树的样子。 image

    实战操作

    const parse = require('@babel/parser');
    const code = 'function getResult(x, y) {return x + y; }';
    console.log(parse.parse(code));
    

    我们引入@babel/parser把我们刚刚的表达式当成输入,我们看看输出是怎么样的, 发现他解析出来的AST树正是我们所解释的样子。 至于上面提到的关键词(比如BlockStatementReturnStatement...)我们可以在MDN中寻找到。 image

然后我们开始第二步:遍历转换
  • 我们这里使用@babel/traverse来遍历AST,继续上面的代码,我们简单的使用一下traverse,我们可以在控制台看到path输出的其实每一个节点的信息,我们可以在enter里面对某一个节点进行快速的操作。

    traverse.default(ast,{
      enter(path) {
        if (
          path.node.type === "Identifier" &&
          path.node.name === "x"
        ) {
          path.node.name = "n";
        }
      }
    });
    
最后一步: AST语法树转化为JS代码
  • 这里我们使用babel-generator将AST转化为JS, generate的第一个参数就是转化后的ast了, 第二个参数是一些生成js代码时候的一些选项比如要不要注释,压缩等等。至此上半节的内容已经结束了。下半节我们将会开始编写插件。
 const oj = generate.default(ast,{ },code)
 console.log(oj.code);   // function getResult(n, y) {  return n + y; }

编写Babel插件

前言: 在我们编写Babel插件之前我们还需要了解几个知识点:

  • Visitor: 当Babel处理每一个节点的时候,是通过访问者的形式获取节点的从而处理节点的, 而这种方式是通过visitor对象来操作的,在visitor对象中内置了一系列对节点的访问的函数,这样我就可以针对不同的节点进行快速的处理。我们编写的Babel插件其实也就是实例化一个Visitor对象处理一系列的AST节点。我们举一个简单的例子。假如我们想按需加载react-ladingg。那么很简单我只需要把下面这段代码 import BabelLoading from 'react-ladingg/lib/BabelLoading'即可。

    import { BabelLoading } from  'react-ladingg'
    

    那么我们定义的Babel插件的对象就应该是:

    visitor: {
    ImportDeclaration: (path, state) => {
    }
    }
    

    当我们的Babel处理节点的时候 遇到了 import语句的时候那么它就会进入ImportDeclaration方法.我们来看一下它的传参。

    • path: 它包含了属性和方法: 属性: node: 当前节点, parent: 父节点, scope: 作用域... 方法:replaceWith: 用AST节点替换当前节点, replaceWithMultiple: 用多个AST节点替换当前节点, remove: 删除节点...
    • state: state是visitor对象中每次访问节点方法时传入的第二个参数。包含诸如当前plugin的信息、plugin传入的配置参数信息,甚至当前节点的path信息也能获取到,当然也可以把babel插件处理过程中的自定义状态存储到state对象。
  • Babel/Types: 这是一个处理AST很强大的一个工具它包含了构造、验证以及变换AST节点的方法。该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。就比如我们这个例子: 有三种基本的import方法

    1. import { BabelLoading } from 'reactloadingg'
    2. import BabelLoading from 'reactloadingg'
    3. import * as Loading from 'reactloadingg'.他们分别对应Types中ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier。现在可以又可以继续编写Babel插件了
     visitor: {
             ImportDeclaration (path, state) { 
         	    const specifiers = path.node.specifiers;
         	    specifiers.forEach((specifier) => {
                     if (!types.isImportDefaultSpecifier(specifier) && !types.isImportNamespaceSpecifier(specifier)) {
             	        // do something
             	    }
     	        })
           }
     }

    这边我们过滤掉第二种和第三种写法。我们只处理第一种。当我们过滤掉我们不想处理的节点之后,我们得对应节点进行一些简单的处理。首先我们来明确一下我们的目标,我们想要把import { CommonLoaidng, BabelLoadng } from 'react-loadingg' 转化为 import CommonLoadng from 'react-loadingg/lib/CommonLoadng' import CommonLoadng from 'react-loadingg/lib/BabelLoadng。好我们来看关键代码:

     visitor: {
             ImportDeclaration (path, state) { 
         	    const specifiers = path.node.specifiers;
         	    specifiers.forEach((specifier) => {
                 if (!types.isImportDefaultSpecifier(specifier) &&
               !types.isImportNamespaceSpecifier(specifier)) {
    +         let newImports = specifiers.map(specifier => {
    +         return types.importDeclaration(
    +         [types.importDefaultSpecifier(specifier.local)],
    +         types.stringLiteral(`${node.source.value}/lib/${specifier.local.name}`)
    +        )
    +      })
    +       path.replaceWithMultiple(newImports)
    +         	    }
    +	        })
           }
     }

    首先specifiers可能是包含多个节点的。比如CommonLoaidng和BabelLoadng,因此我们需要遍历,让我们需要将其转化为ImportSpecifier,我们使用Babel/types 中的 importDeclaration函数来操作,它的第一个参数传入节点,查看官方文档再结合我们的需求我们需要对节点做ImportDefaultSpecifier转化,第二个参数就是stringLiteral,我们传入组件包的目的地。最后一步,我们用replaceWithMultiple来完成多个AST节点替代掉当前节点。我们的简易版Babel已经完成了。它已经可以满足我们按需加载的需求了。