探索babel和babel插件是怎么工作的
你有可能会听到过这个词 webpack工程师 ,这个看似像是一个专业很强的职位其实很多时候是一些前端对现在前端工作方式对一些吐槽,对于一个之前没有接触过webpack
,nodejs
,babel
之类的工具的人来说,看到大量的配置文件后很多人都会看懵
很多人就干脆不管这些东西,直接上手写业务代码,把这些构建工具就相当于黑科技
,我们把所有的文件都经过这些工具最终生成一个或者几个打包后的文件,其中关于优化和代码转换问题其实一大部分都是在这些配置里面的。如果我们不去了解其中的一部分原理,后面遇到很多问题(如打包后文件体积过大
)时候都是束手无策,而且万一哪天构建工具出现问题时候可能连工作都开展不下去了。
既然我们日常都要用到,最好的方式就是去研究一下这些工具的原理的作用,让这些工具成为我们手中的利器,而不是工作上的绊脚石,而且这些工具的设计者都是顶级的工程师,当你敲开壁垒探究内部秘密时候,我相信你会感受到其中的编程之美。
这里我们去探索一下babel
的原理
babel 是什么?
Babel · The compiler for writing next generation JavaScript
6to5
你在npm
上可以看到这样一个包名字是6to5, 光看名字可能会让人感觉到很诧异,名字看起来可能有点奇怪,其实babel
在开始的时候名字就是这个。简单粗暴es6 -> es5
,一下子就看懂了babel
是用来干啥的,但是很明显这不是一个好名字,这个名字会让人感觉到es6
普及之后这个库就没用了,为了保持活力这个库可能要不停的修改名字。下面是babel
作者一次分享中假设如果按这个命名法则可能出现的名称
很明显发生这种情况是很不合理的,团队内部经过大量讨论后,最终选择了babel
,这与电影银河系漫游指南中的Babel fish相应,也有关系到圣经中的一个故事Tower of Babel。(ps.优秀的人总是也很有情怀。)
babel is the new jQuery
redux
的作者曾说过这样一句话,可以换一种理解为
babel : AST :: jQuery : DOM
babel
对于 AST
就相当于 jQuery
对于 DOM
, 就是说babel
给予了我们便捷查询和修改 AST
的能力。(AST -> Abstract Syntax Tree) 抽象语法树 后面会讲到。
为什么要用babel转换代码
我们之前做一些兼容都会都会接触一些 Polyfill
的概念,比如如果某个版本的浏览器不支持 Array.prototype.find
方法,但是我们的代码中有用到Array
的find
函数,为了支持这些代码,我们会人为的加一些兼容代码
if (!Array.prototype.find) {Object.defineProperty(Array.prototype, 'find', {// 实现代码...});
}
对于这种情况做兼容也很好实现,引入一个 Polyfill
文件就可以了,但是有一些情况我们使用到了一些新语法,或者一些其他写法
// 箭头函数
var a = () => {}
// jsx
var Component = () => <div />
这种情况靠 Polyfill
, 因为一些浏览器根本就不识别这些代码,这时候就需要把这些代码转换成浏览器识别的代码。babel
就是做这个事情的。
babel做了哪些事情
为了转换我们的代码,babel
做了三件事
Parser
解析我们的代码转换为AST
。Transformer
利用我们配置好的plugins/presets
把Parser
生成的AST
转变为新的AST
。Generator
把转换后的AST
生成新的代码
从图上看 Transformer
占了很大一块比重,这个转换过程就是babel
中最复杂的部分,我们平时配置的plugins/presets
就是在这个模块起作用。
从简单的说起
可以看到要想搞懂babel
, 就是去了解上面三个步骤都是在干什么,我们先把比较容易看懂的地方开始了解一下。
Parser 解析
解析步骤接收代码并输出 AST
,这其中又包含两个阶段词法分析和语法分析。词法分析阶段把字符串形式的代码转换为 令牌(tokens)
流。语法分析阶段会把一个令牌流转换成 AST
的形式,方便后续操作。
Generator 生成
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
babel的核心内容
看起来babel
的主要工作都集中在把解析生成的AST
经过plugins/presets
然后去生成新的AST
这上面了。
AST抽象语法树
我们一直在提到AST
它究竟是什么呢,既然它的名字叫做抽象语法树
,我们可以想象一下如果把我们的程序用树状表示会是什么样呢。
var a = 1 + 1
var b = 2 + 2
我们想象一下要表示上述代码应该是什么样子,首先必须有东西可以表示这些具体的声明
,变量
,常量
的具体信息,比如(这棵树上肯定有二个变量,变量名是a和b,肯定有两个运算语句,操作符是 + )
,有了这些信息还不够,我们必须建立起它们之间的关系,比如一个声明语句,声明类型是 var, 左侧是变量, 右侧是表达式
。有了这些信息我们就可以还原这个程序,这也是把代码解析成AST
时候所做的事情,对应上面我们说的词法分析
和 语法分析
。
在AST
中我们用node
(节点)来表示各个代码片段,比如我们上面程序整体就是一个节点Program
节点(所有的 AST 根节点都是 Program 节点),因为它下面有两条语句所以它的 body
属性上就两个声明节点VariableDeclaration
。所以上面程序的AST
就类似这样
可以看到在节点上用各个的属性去表示各种信息以及程序之间的关系,那这些节点每一个叫什么名字,都用哪些属性名呢?我们可以在说明文档上找到这些说明。
关于接口
看这个文档时候我们可以看到说明大多是类似这种
interface Node {type: string;loc: SourceLocation | null;
}
这里提到interface
这个我们在其他语言中是比较常见的,比如Node
规定了type
和loc
属性,如果其他节点继承自Node
,那么它也会实现type
和loc
属性就是说继承自Node
的节点也会有这些属性,基本所有节点都继承自Node
,所以我们基本可以看到loc
这个属性loc
表示个一些位置信息。
节点单位
我们程序很多地方都会被拆分成一个个的节点,节点里面也会套着其他的节点,我们在文档中可以看到AST
结构的各个 Node
节点都很细微,比如我们声明函数,函数就是一个节点FunctionDeclaration
,函数名和形参那么参数都是一个变量节点Identifier
。生成的节点往往都很复杂,我们可以借助astexplorer来帮助我们分析AST
结构。
图像展示
有了上面这些概念我们已经可以大概了解AST
的概念,以及各个模块代表的含义,假设我们有这样一个程序,我们用图形简易的分析下它的结构
function square (n) {return n * n
}
节点遍历
经过一番努力我们终于了解了AST
以及其中内容的含义,但是这一部分基本不需要我们做什么,babel
会借助Babylon帮我们生成我们需要的AST
结构。我们更多要去做的是去修改和改变Babylon
生成的这个抽象语法树。
babel
拿到抽象语法树后会使用babel-traverse
进行递归的树状遍历,对于每一个节点都会向下遍历到尽头,然后向上遍历退出分支去寻找下一个分支。这样确保我们能找到任何一个节点,也就是能访问到我们代码的任何一个部分。可是我们要怎么去完成修改操作呢,babel
给我们提供了下面这两个概念。
visitor
我们已经知道babel
会遍历节点组成的抽象语法树,每一个节点都会有自己对应的type
,比如变量节点Identifier
等。我们需要给babel
提供一个visitor
对象,在这个对象上面我们以这些节点的type
做为key
,已一个函数作为值,类似如下,
const visitor = {Identifier: {enter() {console.log('traverse enter a Identifier node!')},exit() {console.log('traverse exit a Identifier node!')}}
}
这样在遍历进入到对应到节点时候,babel
就会去执行对应的enter
函数,向上遍历退出对应节点时候,babel
就会去执行对应的exit
函数,接着上面的代码我们可以做一个测试
const babel = require('babel-core')const code = `var a = b + c + d`// 如果plugins是个函数则返回的对象要有visitor属性,如果是个对象则直接定义visitor属性
const MyVisitor = {visitor
}babel.transform(code, {plugins: [MyVisitor]
})
我们执行对应代码可以看到上面enter
和exit
函数分别执行了四次
traverse enter a Identifier node!
traverse exit a Identifier node!
... x4
从上面简单的代码上也可以看到a,b,c,d
四个变量,它们应该属于同一级别的节点树上,所以遍历时候会分别进入对应节点然后退出再去下一个节点。
Paths
我们通过visitor
可以在遍历到对应节点执行对应的函数,可是要修改对应节点的信息,我们还需要拿到对应节点的信息以及节点和所在的位置(即和其他节点间的关系)
, visitor
在遍历到对应节点执行对应函数时候会给我们传入path
参数,辅助我们完成上面这些操作。注意 Path
是表示两个节点之间连接的对象,而不是当前节点,我们上面访问到了Identifier
节点,它传入的 path
参数看起来是这样的
{"parent": {"type": "VariableDeclarator","id": {...},....},"node": {"type": "Identifier","name": "..."}
}
从上面我们可以看到 path
表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法。我们修改一下上面的 visitor
函数
const visitor = {Identifier: {enter(path) {console.log('traverse enter a Identifier node the name is ' + path.node.name)},exit(path) {console.log('traverse exit a Identifier node the name is ' + path.node.name)}}
}
在执行一下上面的代码就可以看到name
打印出来的依次是a
,b
,c
,d
。这样我们就有可以修改操作我们需要改变的节点了。另外path
对象上还包含添加、更新、移动和删除节点有关的其他很多方法,我们可以通过文档去了解。
一些有用的工具
babel
为了方便我们开发,在每一个环节都有很多人性化的定义也提供了很多实用性的工具,比如之前我们在定义visitor
时候分别定义了enter
,exit
函数,可很多时候我们其实只用到了一次在enter
的时候做一些处理就行了。所以我们如果我们直接定义节点的key
为函数,就相当于定义了enter
函数
const visitor = {Identifier(){// dosmting}
}// 等同于 ↓ ↓ ↓ ↓ ↓ ↓const visitor = {Identifier: {enter() {// dosmting}}
}
上面我们还提到了plugins是函数的情况,其实我们写的差距一般都是一个函数,这个入口函数上babel
也会穿入一个babel-types
,这是一个用于AST
节点的 Lodash
式工具库(类似lodash
对于js
的帮助), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
实际运用
假如我们有如下代码
const a = 3 * 103.5 * 0.8
log(a)
const b = a + 105 - 12
log(b)
我们发现这里把console.log
简写成了log
,为了让这些代码可以执行,我们现在用babel
装置去转换一下这些代码。
改变log函数调用本身
既然是console.log
没有写全,我们就改变这个log
函数调用的地方,把每一个log
替换成console.log
,我们看一下log(*)
属于函数执行语句,相对应的节点就是CallExpression
,我们看下它的结构
interface CallExpression <: Expression {type: "CallExpression";callee: Expression | Super | Import;arguments: [ Expression | SpreadElement ];optional: boolean | null;
}
callee
是我们函数执行的名称,arguments
就是我们穿入的参数,参数我们不需要改变,只需要把函数名称改变就好了,之前的callee
是一个变量,我们现在要把它变成一个表达式(取对象属性值的表达式)
,我们看一下手册可以看到是一个MemberExpression
类型的值,这里也可以借助之前提到的网站astexplorer来帮助我们分析。有了这些信息我们就可以去实现我们的目的了,我们这里手动引入一下babel-types
辅助我们创建新的节点
const babel = require('babel-core')
const t = require('babel-types')const code = `const a = 3 * 103.5 * 0.8log(a)const b = a + 105 - 12log(b)
`const visitor = {CallExpression(path) {// 这里判断一下如果不是log的函数执行语句则不处理if (path.node.callee.name !== 'log') return// t.CallExpression 和 t.MemberExpression分别代表生成对于type的节点,path.replaceWith表示要去替换节点,这里我们只改变CallExpression第一个参数的值,第二个参数则用它自己原来的内容,即本来有的参数path.replaceWith(t.CallExpression(t.MemberExpression(t.identifier('console'), t.identifier('log')),path.node.arguments))}
}const result = babel.transform(code, {plugins: [{visitor: visitor}]
})console.log(result.code)
执行后我们可以看到结果
const a = 3 * 103.5 * 0.8;
console.log(a);
const b = a + 105 - 12;
console.log(b);
直接在模块中声明log
我们已经知道每一个模块都是一个对于的AST
,而AST
根节点是 Program
节点,下面的语句都是body
上面的子节点,我们只要在body
头声明一下log
变量,把它定义为console.log
,后面这样使用就也正常了。
这里简单的修改下visitor
const visitor = {Program(path) {path.node.body.unshift(t.VariableDeclaration('var',[t.VariableDeclarator(t.Identifier('log'),t.MemberExpression(t.identifier('console'), t.identifier('log')))]))}
}
执行后生成的代码为
var log = console.log;const a = 3 * 103.5 * 0.8;
log(a);
const b = a + 105 - 12;
log(b);
总结
到这里我们已经简单的分析代码,修改一些抽象语法树上的内容来达到我们的目的,但是还是有很多中情况还没考虑进去,而babel
现阶段不仅仅代表着去转换es6
代码之类的功能,实际上我们自己可以写出很多有意思的插件,欢迎来了解babel
,按照自己的想法写一些插件或者去贡献一些代码,相信在这个过程中你收获的绝对比你想象中的要更多!
本文首发与个人博客
探索babel和babel插件是怎么工作的相关推荐
- sublime 插件安装;sublime的 babel、sublime-jsfmt插件
前提: 在开发过程中,使用到es6语法,sublime不能够自动的识别代码,不能高亮等: babel是一个更好显示高亮,和代码格式的插件,并且 sublime-jsfmt让 更好: 安装sublime ...
- Babel常用的插件
Babel常用的插件 1. @babel-plugin-dynamic-import-node 作用:在开发环境下解决vue热加载编译速度慢的问题. 2. @babel/plugin-syntax-d ...
- Babel 之 @babel/preset-env
Babel 已经成为前端工程化开发的必备工具链.自 Babel 7.0 以后,Babel 进一步对工具优化和插件统一管理,全面迁移到 @babel 中.@babel/preset-env 是十分常用的 ...
- 干货|10个C4D必备插件,让工作事半功倍
今天我要跟大家分享一些实际工作中常用的C4D插件,这些插件能让工作事半功倍. 话不多说,下面就逐个介绍这10个C4D必备插件吧: 1.超级独显插件(MagicSolo) 支持:R12 - R21 wi ...
- 不坑盒子:强大的word插件,让工作更高效
不坑盒子简介 很多朋友在工作过程中需要对Word文档进行编辑处理,如果想让Word排版更有效率可以试试小编带来的这款不坑盒子软件,这是一个非常好用的插件工具,专门应用在Word文档中,支持Office ...
- Excel 操作 插件 方方格子 工作簿未按字母顺序排列 重名工作簿名 并拆分成工作表
1.未处理的站明细 百步亭 在前 白沙洲在后 格式未处理 未重命名 未拆分工作簿 2.处理后的站明细 格式处理 F 列宽 32G列宽拉紧A列定位到 A400 - A7 选中 A400 到 A7时 按s ...
- html5播放器怎么小窗播放器,悬浮画中画播放器插件,边工作边用小窗口看视频的摸鱼神器...
悬浮画中画播放器插件,可以使用chrome提供的picture-in-picture api让网页上的视频以画中画小窗口悬浮在任意网页上播放,目前支持youtube, 腾讯视频.爱奇艺以及各种使用ht ...
- 大漠插件在多线程工作下找字失效原因及解决办法
最近费心费力写了个贼棒棒的多线程工作脚本 一个大漠对象绑定一条线程工作 但是在使用过程中发现:停止正在工作的线程,再次启动后会有概率找字失效. 脚本使用了全局字库,首先停止使用全局字库,改为在启动线程 ...
- Android开发艺术探索 - 第9章 四大组件的工作过程
1.Activity启动过程 ref 从Activity的startActivity方法开始.startActivity的多个重载方法,最终都会调用startActivityForResult方法.m ...
- Android开发艺术探索——第九章:四大组件的工作过程(下)
我們继续来看四大组件的工作过程 一.BroadcastReceiver的工作过程 广播的工作过程,我们主要看两个方面,一个是注册过程,另一个就是接收的过程,我们想使用广播是非常简单的,只需要继承Bro ...
最新文章
- 前端的单页面模式和多页面模式
- 百度ERNIE登顶GLUE榜单,得分首破90大关
- 基于python的堡垒机
- OSChina 周一乱弹 —— 程序员用什么浏览器能看出来品位么
- LVS(3)——针对于真实主机的增删改操作
- [转]memset用法详解
- c语言输入一段字符,C语言实现输入一个字符串后打印出该字符串中字符的所有排列...
- go程序的跨平台编译
- 在ASP.NET Core中使用AOP来简化缓存操作
- SVC较好的介绍资料
- oracle安装实验,Oracle之课程实验一(安装oracle)
- 14. Thinking carefullly about copying behavior in resource-managing classes
- c语言汉诺塔问题详解
- 单片机:DS1302时钟
- VSCode中文版快捷键
- latex中的对与错(对号与叉号)
- linux中文成方块,给linux添加字体
- 安全模式解除android,手机安全模式怎么解除
- Python实现GWO智能灰狼优化算法优化支持向量机回归模型(svr算法)项目实战
- linux下的通配符与特殊符号