大前端的自动化工厂(3)—— babel
2019-11-30

一. 关于babel

babel是ES6+语法的编译器,官方网址:www.babeljs.io,用于将旧版本浏览器无法识别的语法和特性转换成为ES5语法,使代码能够适用更多环境。

最初的babel使用起来是非常方便的,几乎仅使用少量的配置就可以使用,但随着工具的快速升级和代码架构的转变,babel已经裂变成非常多的部分,每个部分各司其职,这样做的好处是可以缩小生产环境的正式包的代码体积(因为可以按需引用)而加重了开发环境(开发阶段需要引入更多碎片化的插件),但劣势就是将其使用门槛提得非常高,对软件架构不熟悉的开发者难以使用。

比如babel官方网站在webpack配置的章节,提及了babe-loader,babel-corebabel-preset-env三个插件,而当开发者在webpack中实际进行配置时除了上述三个基本插件外,又会遇到babel-polyfill,babel-runtime,babel-plugin-transform-runtime等等一系列插件,或许通过查看插件说明能够理解插件的功能,但开发者却很难判断自己是否该使用这个功能或者什么时候使用。

二. 基本需求推演

我们从工具设计的角度,通过问题推演的方式来看看babel的变化。

ES6标准推出时,浏览器还不能很好地支持,但ES6的许多特性和语法又很诱人,所以大家想了个办法,那就是用ES6编写代码,然后出包的时候拿个工具转换一下,变成能被更多浏览器识别的ES5语法不就行了么,于是,Babel基本模型就出现了:

babel的功能被定义为编译工具,那么理论上来说它就可以使用编译器的通用代码框架,通过ASTparser --> traverse --> stringify 的步骤实现编译功能,在关键的traverse环节,是需要一个规则集合的,可是转码所参考的ES6的标准并不是一个定案的标准,其中每一个特性都需要经过从stage0stage4这样5个阶段才能正式定稿,只有stage-2草案(draft)阶段以上的特性才会在未来被支持,而处于这个阶段以下的标准是有可能被废的,如果一味地全部转换,不仅会降低工具效率,也会为代码未来的维护造成隐患。

那如果我们有一个工厂函数,接受数字0-4作为参数,然后返回所有经历了stage-x的规则集(是ES6规则的子集)作为规则集合,那么就可以在最终生成生产环境的代码时减小代码体积,假如在项目中通过babel_get_es6_by_stage(2)这样一个函数返回了规则集,那么正式代码中就不需要stage-0stage-1的实现代码了。基于以上的考虑,我们对Babel工具进行第一次功能剥离:

推演继续,在对规则集进行了一次体积缩减后,我们得到了一个相对精简的规则集,它包含了诸多新的语法和方法,如果直接使用那的确很爽,毕竟引入了一个工具后就可以毫无后顾之忧地使用新特性,但对于生产环境的代码包来说,这种做法造成的代码冗余确是非常难以接受的。

用大家都熟悉的bootstrap为例,bootstrap.min.css的体积大约为120k,可你会发现很多人引入它完全是出于心里惯性,而在最后仅仅使用了非常基础的btn相关的样式类,或者仅仅为了使用col-md-4这种响应式布局的样式,所有使用到的样式可能只占了20k-30k的空间,但是却不得不为项目引进一个120k大的库,当然并不是所有的项目都会在意20k和120k之间的差别的。

那么我们就需要一个能够按更小粒度组合的方法babel_get_es6_by_rules([rule , ...]),让使用者可以选择自己所使用到的语法和方法,从而达到缩小引用库体积的目的:

推演继续进行。处理过兼容性问题的开发者都知道,浏览器是存在版本区分的,许多特性在不同浏览器中的实现和表现都不一样,对于ES6也是这样,较高版本的浏览器对于ES6中的一些特性是已经逐步实现支持了的,如果我们的目标用户所使用的运行环境对某些ES6特性已经提供了原生支持,或者目标用户的运行环境根本就是由开发者直接封装好的,那么原先“一锅端”的转码方式里就会存在很多没有必要的部分。

比如你在规则集中选择了对Class关键字来定义类这个特性进行转码,那么babel就需要将其转码成为使用functionprototype的ES5的实现方式,但如果你的目标用户全都是程序员,几乎全都是使用高版本的chrome作为项目环境,那么上面的转码可能就是画蛇添足了。

综上所述,我们就需要为babel提供一个判断目标环境是否需要转码的方法babel_get_rule_as_need( rule_set , env_info),将经过第一次筛选后的规则集和目标用户的环境信息传入方法,对规则集进行再一次的精简,那么我们需要再次对babel进行优化:

至此,babel便具备了针对不同的使用环境进行必要转码的能力,可这并不是问题的全部,ES6的新特性除了语法的更新外,还增加了很多原生方法或类型,例如Map,Set,Promise等这类新的全局对象,或是Array.from这类静态方法等等,语法转义并不能完成对这些特性的识别,因为无论在ES5环境还是ES6环境你都是这么写的,只有运行的时候,浏览器才会报错,告诉你某个对象或者某个方法不存在。

比如下面的代码:

function addAll() { return Array.from(arguments).reduce((a, b) => a + b);}

转义后会变为:

function addAll() { return Array.from(arguments).reduce(function(a, b) { return a + b; });}

然而,它依然无法随处可用因为不是所有的 JavaScript 环境都支持 Array.from。对于这一类非语法层面的特性,我们希望在工具中能够自动提供支持,这项工作有一个专有的称谓,叫做【polyfill】(或称为垫片)。

我们既可以主动提供一个polyfill列表指明需要添加的垫片插件数组,也可以采用被动的方式,在转码过程中遇到的这种API类型的新特性放进一个数组,通过babel_add_polyfill ( polyfill_list )为根据安装相应的垫片,需要注意的是,polyfill相当于为浏览器进行功能扩展,需要优先于项目业务逻辑代码运行,那么babel的逻辑框架就变成了:

推演继续。在上面的逻辑结构中,我们只是简单地将polyfill库添加至全局变量,而全局变量是很有可能被重写而失效或是与其他第三方库发生代码冲突的。那么如果不将polyfill添加至全局,就需要将其剥离为一个具有同等功能的独立模块,通过类似于lodash或是underscore那样的方式调用,我们对逻辑结构进行再一次拆分:

至此,我们已经完成了babel工具集基本功能的*逻辑层划分*,通过传说中的多退少补(也就是语法超前了就回退,方法不够了就打补丁)的方式来实现代码编译。

三. 模块划分

根据上述业务逻辑层的划分结果,我们需要对Babel工具进行代码层的模块划分:

四. 真正的babel

如果你能够理解上述的需求推演和模块划分的章节,那么恭喜你已经掌握了babel的基本结构,我们将原本模块图中的信息更换成实际的名称或是插件,并进行一些组件划分,就可以看到真正的babel工具集的基本架构:

当然真正的babel功能远不止这样,它为各种环境,编辑器和自动化工具提供了接口,也开放了插件开发的API给开发者,感兴趣的读者可以继续深入了解。

五. 使用babel

babel8.0以上的版本将许多插件移入官方仓库,安装方式发生了改变,例如babel-preset-env地址变为了@babel/preset-env,使用时请参考babel官网进行配置。

1.babel-cli

为了方便直接在命令行使用babel的功能,通过yarn global add babel-cli在全局安装命令行工具babel-cli,在package.json中加入如下脚本:

"scripts":{ "babel":"babel main.js -o maines5.js"}

然后通过yarn run babel即可在命令行使用babel进行编译了,但查看编译后的代码就可以发现,编译前后的文件是一样的,因为我们没有为其指定任何转码规则,运行babel只是把生成的AST遍历了一下而已,想要babel能够实现转码,请继续向下看。

2.babel-preset-env

提供转码规则,它低版本babel中使用的几个插件的结合。babel-preset-env实际上实现的,就是我们在问题推演中所描述的【All Rules规则集 + get_rules()方法集】,你会在node_modules文件夹中找到许多babel-plugin-transform-***这种命名的包,他们就是规则集,你既可以通过设置preset属性来使用,也可以通过在plugins属性中挑选需要的转码规则进行引用。

安装babel-preset-env后在项目文件夹新建.babelrc文件并添加如下配置:

{ "presets":["env"], "plugins": []}

或自定义所需要支持的转义规则:

{ "presets":[], "plugins": [ "babel-plugin-transform-es2015-arrow-functions"//箭头函数转换规则 ]}

再次运行babel,就可以看到所编写的代码已经进行了转换。

转换前:

//Arrow Function Array.from methodArray.from([1, 2, 3]).map((i) => { return i * i;});

转换后:

"use strict";//Arrow Function Array.from methodArray.from([1, 2, 3]).map(function (i) { return i * i;});

当然也可以指定目标浏览器,去除不必要的转码,例如在.babelrc指定要匹配的浏览器为较高版本的chrome:

//.babelrc{ "presets":[ ["env", { "targets": { "browsers": "chrome 56" } }] ], "plugins":[]}

就可以发现编译后的脚本文件中箭头函数依然存在,说明这个版本的chrome浏览器已经支持箭头函数了,也就没有必要进行转义了。

新版本的babel已经计划支持在package.json中设置browserslist参数来指定需要适配的使用环境,也就是说同一套针对使用环境的配置被剥离出来,而被postcss,babel,autoprefixer等工具共享使用。

3.babel-polyfill

babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:

    全局对象:Promise、WeakMap 等。全局静态函数:Array.from、Object.assign 等。实例方法:比如 Array.prototype.includes 等。

此时,需要引入babel-polyfill来模拟实现这些对象、方法。

如果上面编译后的代码在IE10浏览器中打开,就会看到浏览器出现不支持Array.from方法的报错,如果生成的代码需要在IE10中运行,那我们就需要引入兼容补丁库,让IE10浏览器环境中能够支持这个方法。

babel-polyfill需要通过如下的方式引入,然后通过打包工具将其融入脚本:

//ES Moduleimport "babel-polyfill"//或 CommonJsrequire ("babel-polyfill")

当你真的这样去使用时,就会发现,它的确能够解决报错的问题,但是如此打包会引入整个babel-polyfill,打包后的代码增加了将近4000行(约400k体积增量),着实让人难以接受。那这个插件能否像babel-preset-env一样按需引用呢?必须可以的。babel-polyfill是基于core-jsregenerator构建的,只需要在引用时指明即可,例如:

import "core-js/modules/es6.array.from";//Arrow Function Array.from methodArray.from([1, 2, 3]).map((i) => { return i * i;});

再进行打包时就会发现bundle文件的体积减小了非常多。

babel-polyfill的实现方式如问题推演中所提到的那样,就是污染了全局环境,而且你可能已经意识到,这个工具,要么简单配置后代码量激增,要么按需引用配置繁琐。除非是在中型以上项目中有兼容低版本IE的需求,否则不建议使用。

4.babel-runtime/babel-plugin-transform-runtime

如果一个东西难用,那么很快就会有替代品出现,软件的世界也是这样,babel-runtime就是这样一个替代品。摘录下文资料推荐的博文中的解释:

babel-polyfill

简单粗暴,他会污染全局环境,比如在不支持Promise的浏览器会polyfill一个全局的Promise对象供调用;另外,不支持的实例方法也在对应的构造函数原型链上添加要polyfill的方法。

babel-runtime

不会污染全局环境,会在局部进行polyfill,另外不会转换一些实例方法,如"abc".includes("a"),其中的includes方法就不会翻译。它一般结合babel-plugin-transform-runtime来使用。

简单地说,除了实例方法以外,其他的特性babel-runtime都会帮你打好补丁。使用时直接在plugins配置项中添加babel-plugin-transform-runtime即可。

总的来说,babel-polyfillbabel-plugin-transform-runtime都有各自的使用场景,也是可以结合使用的,需要根据实际项目需求进行筛选和引入

六. 资料推荐

《webpack+babel项目在IE下报Promise未定义错误引出的思考》

博文里详细解说了babel-runtimebabel-plugin-transform-runtime的相关问题。

《如何写好.babelrc?》

博文里详细解说了各个配置项和可选参数的意思,非常实用。

入门指南:babel-handbook

非常棒的入门指南,对babel中的概念和用法都做了一定解释,建议优先阅读,可以帮助开发者了解本篇中未涉及的babel模块。

官方网站:www.babeljs.io

很多开发者喜欢看教程却容易忽略官网,这是非常奇怪的。官方网站会链接到非常多优秀的github仓库,不仅包括babel中封装的底层模块,还包括能够帮助我们理解的指引仓库,甚至ES2015主要特性的解释的网站,是学习babel的主要资源。

, 1, 0, 9);