title | date | tags | categories | ||
---|---|---|---|---|---|
这一次我真的搞懂了Babel |
2020-04-12 |
|
|
::: tip 前言: 之前虽然大致看了看Babel相关的知识,但是并没有深入了解。本文前半部分先详细介绍一下Babel是如何编译JS代码并且转化为浏览器能够理解的代码, 后半部分将会实现一个简单的babel插件。 :::
Babel is a compiler for writing next generation JavaScript.
Babel是编写下一代JavaScript的编译器。
帮我们编写的es6,es7...解析成浏览器能够理解的代码
- 解析: 使用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域)对象, 它用来表示returnx + y
;我们再解析它,我们又得到了一个BinaryExpression(二项式)对象,它用来表示x + y
;我们再解析这个二项式对象,它分成了三部分,left
,operator
,right
实战操作
const parse = require('@babel/parser'); const code = 'function getResult(x, y) {return x + y; }'; console.log(parse.parse(code));
我们引入@babel/parser把我们刚刚的表达式当成输入,我们看看输出是怎么样的, 发现他解析出来的AST树正是我们所解释的样子。 至于上面提到的关键词(比如
BlockStatement
、ReturnStatement
...)我们可以在MDN中寻找到。
-
我们这里使用@babel/traverse来遍历AST,继续上面的代码,我们简单的使用一下traverse,我们可以在控制台看到path输出的其实每一个节点的信息,我们可以在enter里面对某一个节点进行快速的操作。
traverse.default(ast,{ enter(path) { if ( path.node.type === "Identifier" && path.node.name === "x" ) { path.node.name = "n"; } } });
- 这里我们使用babel-generator将AST转化为JS, generate的第一个参数就是转化后的ast了, 第二个参数是一些生成js代码时候的一些选项比如要不要注释,压缩等等。至此上半节的内容已经结束了。下半节我们将会开始编写插件。
const oj = generate.default(ast,{ },code)
console.log(oj.code); // function getResult(n, y) { return n + y; }
-
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方法
import { BabelLoading } from 'reactloadingg'
import BabelLoading from 'reactloadingg'
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已经完成了。它已经可以满足我们按需加载的需求了。