前端开发者丨JavaScript
webpacknodejsbabel很多人就干脆不管这些东西,直接上手写业务代码,把这些构建工具就相当于 黑科技 ,我们把所有的文件都经过这些工具最终生成一个或者几个打包后的文件,其中关于优化和代码转换问题其实一大部分都是在这些配置里面的。
如果我们不去了解其中的一部分原理,后面遇到很多问题( 如打包后文件体积过大 )时候都是束手无策,而且万一哪天构建工具出现问题时候可能连工作都开展不下去了。
既然我们日常都要用到,最好的方式就是去研究一下这些工具的原理的作用,让这些工具成为我们手中的利器,而不是工作上的绊脚石,而且这些工具的设计者都是顶级的工程师,当你敲开壁垒探究内部秘密时候,我相信你会感受到其中的编程之美。
这里我们去探索一下 babel 的原理babel 是什么?Babel · The compiler for writing next generation JavaScript6to5你在 npm 上可以看到这样一个包名字是6to5, 光看名字可能会让人感觉到很诧异,名字看起来可能有点奇怪,其实 babel 在开始的时候名字就是这个。
简单粗暴 es6 -> es5 ,一下子就看懂了 babel 是用来干啥的,但是很明显这不是一个好名字,这个名字会让人感觉到 es6 普及之后这个库就没用了,为了保持活力这个库可能要不停的修改名字。
下面是 babel 作者一次分享中假设如果按这个命名法则可能出现的名称很明显发生这种情况是很不合理的,团队内部经过大量讨论后,最终选择了 babel ,这与电影银河系漫游指南中的Babel fish相应,也有关系到圣经中的一个故事Tower of Babel。
(ps.优秀的人总是也很有情怀。
)babel is the new jQueryredux 的作者曾说过这样一句话,可以换一种理解为babel : AST :: jquery : DOM 1 babel : AST :: jquery : DOMbabel 对于 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’, { // 实现代码 … }); } 1 2 3 4 5 6 if ( ! Array . prototype . find ) { Object . defineProperty ( Array . prototype , ‘find’ , { // 实现代码 . . . } ) ; }对于这种情况做兼容也很好实现,引入一个 Polyfill 文件就可以了,但是有一些情况我们使用到了一些新语法,或者一些其他写法// 箭头函数 var a = () => {} // jsx var Component = () =>
1 2 3 4 // 箭头函数 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 1 2 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; } 1 2 3 4 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 } 1 2 3 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!’) } } } 1 2 3 4 5 6 7 8 9 10 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] }) 1 2 3 4 5 6 7 8 9 10 11 12 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 1 2 3 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”: “…” } } 1 2 3 4 5 6 7 8 9 10 11 { “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) } } } 1 2 3 4 5 6 7 8 9 10 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 } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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) 1 2 3 4 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; } 1 2 3 4 5 6 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.8 log(a) const b = a + 105 – 12 log(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) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const babel = require ( ‘babel-core’ ) const t = require ( ‘babel-types’ ) const code = ` const a = 3 * 103.5 * 0.8 log ( a ) const b = a + 105 – 12 log ( 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); 1 2 3 4 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 ,后面这样使用就也正常了。
这里简单的修改下visitorconst visitor = { Program(path) { path.node.body.unshift( t.VariableDeclaration( ‘var’, [t.VariableDeclarator( t.Identifier(‘log’), t.MemberExpression(t.identifier(‘console’), t.identifier(‘log’)) )] ) ) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 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); 1 2 3 4 5 6 var log = console . log ; const a = 3 * 103.5 * 0.8 ; log ( a ) ; const b = a + 105 – 12 ; log ( b ) ;
前端开发者丨JavaScript
评论前必须登录!
注册