(本文叙述的内容所涉及到的相关特性以Babel V7为准)
Babel :原名6to5, 2015年更名为babel,取名灵感来自于BabelFish 是一种虚拟出来的鱼,可以翻译任何物种的语言,,Babel也不负盛名,成了目前最知名的JavaScript语法“编译器”、一种源码转译源码到编译器(source-to-source)。官方说是compiler、也有人称之为transpiler(转译器),stackoverflow也有人提问Is Babel a compiler or transpiler?。总结一下大致意思,编译器是将一种语言转为另一种相对低级一些的语言(比如 Java到字节码。C到二进制)。 转移器是将一种语言转为另一种同等级别的代码(比如JavaScript to python),那么ES6 到 ES5 算同一个level的转换吗??自行理解吧。 对我们来说Babel是JavaScript语法转换工具或是翻译工具,因此不必太纠结compiler还是transpiler 。除了能够转换标准ES以及草案之外,它还支持JSX、typescript、flow。 另外babel方便的插件扩展机制,众多的开发者也相继开发出许多babel插件,让babel不只是作为一个工具 更是一个平台
babel是一个组合套装,拆分为了几十个包,均在@babel
命名空间下
- @babel/parser (原名Babylon) 基于 acorn and acorn-jsx。用于语法解析
- @babel/traverse 遍历AST 调用Plugin转换代码
- @babel/generator 将AST生成代码
- @babel/core 转换流程控制器,整合 parser、traverse plugin generator 完成语法转换。
- 一堆plugin+5个preset
babel的编译流程如下:
- Parse 词法分析得到 Tokens,JS代码生成 抽象语法树(AST)
- Transform 遍历语法树,通过Babel-plugin操作 AST,完成语法树的改变。
- Code Generate 将新的语法树生成代码。
总结 :输入字符串 -> @babel/parser parser -> AST -> traverse ->@babe/plugin--xxxx-> AST -> @babel/generator -> 输出字符串
@babel/types
、@babel/template
、@babel/helpers
、@
babel/code-frames` 方便操作语法树、提供的工具类,写插件的时候会用到@babel/polyfill
。ES标准中包括两部分: 新增的语法+新增的API。 Babel编译流程只提供了语法的转换(比如const、let、async、await、箭头函数等), babel/polyfill是ES标准新增的原生对象以及API的模拟实现(比如Promise
、Map
、Set
、Object.assign
、Array.from
、Object.assig
等),其实@babel/polyfill
上仅仅是core-j
s和regenerator-runtime
两个包的简单封装,之前版本中我们都是在文件的收口处手动的引入babel polyfill
。整个PolyFill文件较大、没必要全部导入、也没必要手动导入、在babel V7之后推荐新的使用方法。通过配置 env选项的时候添加"useBuiltIns":"usage"
,会自动分析代码根据需求来按需针对性的导入polyfill。另外polyfill是全局导入的,像Array.prototype.includes
还会修改原生函数的原型。polyfill只包含了超过stage4
以上的规范。如果要使用更高级草案标准的API 需要自己手动去引入core-js里面的函数。举个例子 .babel配置文件如下
{
"presets": [["@babel/env", {
"useBuiltIns": "usage"
}]]
}
源码
var str = ' foo ';
str.trimLeft();
str.padStart(10);
babel 编译输出结果
require("core-js/modules/es7.string.pad-start");
var str = ' foo ';
str.trimLeft();
str.padStart(10);
String.prototype.padStart
是 ES(7) 正式版的规范,因此会自动引入pad-start的polyfill、而 trimLeft
截止目前还在 Stage 3
。需要自己手动引入core-js(-pure)/features/string/trim-left
。(每一项新特性,要最终纳入ECMAScript规范中,TC39拟定了一个处理过程,称为TC39 process、其**包含5个阶段,Stage 0 ~ Stage 4 stage0开始有初级的想法,不断升级到stage4基本才算确定是进入正式标准了的规范)
既然说到polyfill了 还得说一个函数 :regeneratorruntime
, 这个函数也被算在了babel-polyfill里面了,它不是ES规范新增的API,只是babel在做async/await 语法转换的时候,转换后的结果代码调用到了这个函数,转换结果并没有提供这个函数的定义,所以要让代码能够正常运行,必须要引入regeneratorruntime
这个函数,如果配置babel的时候 env里面设置 "useBuiltIns": "usage"
属性,业务里面如果用到async/await 会自动引入regeneratorruntime这个polyfill
- @babel/runtime:功能类似babel-polyfill,提供了一些帮助函数(regeneratorruntime 等一些代码正常执行需要的辅助函数)以及非实例方法(Array.from,Object.assign、Promise、Map等)的shim,一般用于library或plugin中,最大好处减少工具代码重复引用、且不会污染全局作用域,配合babel/plugin-transform-runtime插件使用 更多信息可以参考babel-polyfill 和 runtime的区别
- @babel/cli babel命令行工具
- @babel/node nodeJS环境下使用babel的功能,由于性能问题不得在生产环境下使用
- @babel/register 通过绑定node.js的require来自动转译require引用的js代码文件
上面已经介绍过babel语法转换流程主要包含三个步骤:词法分析语法分析生成AST->Plugin操作AST->生成代码
那么什么是AST?AST 即抽象语法树 它是源代码语法结构的一种抽象表示(类似浏览器页面用抽象为DOM树来表示)、方便对编程语言进行语法分析、语法检查、代码风格检查、语法转换、代 码优化等,UglifyJS、ESLint、JSDoc、Babel等常用的工具,这背后的就是对AST的应用,另外像目前的mpvue、taro 也都用到AST转换的**。babel通过语法分析,把代码解析为AST,这是基于ESTree稍微改造的结构(EStree可以认为是一些权威大佬联合制定的标准)。JavaScript代码的语法树可以通过astexplorer中查看。可以把AST类比DOM Tree,那么Babel相当于操作AST的jQuery
Babel语法转换的本质是:将源代码解析为AST后、对AST进行遍历(先序深度优先遍历),并对节点进行操作(增、删、改,最后将AST生成代码的过程,这个操作过程采用的是Visitors 模式对节点进行访问,每一个visitor在babel里面有babel-plugin承担。babel-core负责解析语法,每一个语法的转换需要一个单独的plugin来变更AST,转换后的AST生成代码也由babel自动完成。babel官方包内置了一些丰富的plugin来完成语法的转译(比如最新的ES标准/草案、JSX、typescript、flow),一个插件只完成一个特定的功能,启用该功能需要在babel的配置文件里面添加即可,为了方便分享,也方便集中配置,把插件的列表封装为preset,preset即是一堆插件的组合合。babel的插件启用需要单独配置,支持多种配置方式 使用 babel.config.js
.babelrc
.babelrc.js
或者package.json中添加 babel的属性.官方也有详细的介绍。
(官网写的很清楚,感觉没必要多写)简单提一下,Preset是plugin组合,翻译过来叫"预设",官网的提供的一些预设(env、flow、react、typescript)我们也能很容易的创造自己的预设,比如create-react-app ,react-native都是自定义了preset,preset可以有预设和plugin组成。自定义预设也跟配置 babel.config.js类似。另外babel在执行的时候
module.exports = () => ({
presets: [
require("@babel/preset-env"),
],
plugins: [
[require("@babel/plugin-proposal-class-properties"), { loose: true }],
require("@babel/plugin-proposal-object-rest-spread"),
],
});
babel7之后推荐使用@babel/preset-env来转换正式版ES语法,目前preset-env===ES3+ES5+ES2015+ES2016+ES2017,还有三个比较常用的preset( typescript flow react),之前的stage0~stage3 在Babel 7中已经被干掉了。对于目前处于草案的语法需要手动添加相关plugin。具体到特定的语法是否为草案还是正式标准可以去babel的插件列表看一下,不要被这么多插件吓到,其实也没多少,不必熟知,尽量都要清楚每个插件的作用。另外babel-preset-env 还支持根据指定代码的环境,来过滤语法转换,用来表示来支持到什么程度,比如浏览器版本,Node版本,通过这样等减少一些不必要的转换,降低冗余代码
关于plugin和preset执行顺序,Babel遍历到每个AST节点的时候,按规则来执行plugin和preset。执行规则就是 :先执顺序行完所有Plugin,再逆序执行Preset。这个配置的时候可能注意。有时候出错的话,可能跟这个执行顺序有关。仔细想一下,那么多节点,都要被每个插件轮流执行一遍。这个对性能影响也是很大的。所以尽量用具体的babel plugin来配置,干掉stage的preset从一方面避免了这个问题。如果不配置插件任何插件及preset、babel对代码不会做做任何转换 将会输出最初的代码。关于具体的配置,简单介绍下对async/await以及decorators配置方式。
- async/await 在ES7的正式版发布了,目前属于ES的正式标准,理论上来说配置下env即可以使用了,但是上面也有提到 babel的plugin只做语法转换,通过env的配置可以将async,await语法转换为旧式的语法。但是转换后的的代码里面使用了Promise 和 regeneratorRuntime 这两个API。
如果配置为
{
"presets": [["@babel/env"]]
}
源码
async function name(params) {
await 1
}
转化后的代码为
很明显这个代码是执行的话 报错 regeneratorRuntime,对于不支持Promise的浏览器也会 报错。在配置中添加 "useBuiltIns": "usage"
{
"presets": [["@babel/env", {
"useBuiltIns": "usage"
}]]
}
编译之后的代码为 发现顶部多了如下两行代码 引入了 如上两个方法的polyfill
require("regenerator-runtime/runtime");
require("core-js/modules/es6.promise");
刚才说过了,还可以使用 @babel/plugin-transform-runtime
结合@babel/runtime
来实现polyfill的功能。
需要安装两个依赖包
npm install --save @babel/runtime
npm install --svae-dev @babel/plugin-transform-runtime
修改babel的配置
{
"presets": [["@babel/env"]],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
运行babel之后编译的代码为
,可以看到 _regenerator(即regeneratorRuntime)以局部变量的形式被引入了,非上面的全局作用域。另外 asyncToGenerator
也作为一个工具函数被提取至@babel/runtime
,通过导入包,以局部变量的形式在代码里面呈现。另外由于编译后的代码在执行的时候用到了 @babel/runtime
包里面的代码,因此安装依赖的包的时候,根据原则将安装到dependencies里面(--save) 。那么还有个问题,asyncToGenerator用了Promise,对于不支持Promise的浏览器依然会报错。
“Decorators”从好三年前就开始炒的特性,这个特性在Typescript
、 angular
、 mobx
中广为使用,然而经过多年的努力,Decorators
的是目前仍然处于Stage2
,babel官方从babel 5 就有支持 Decorators
的plugin,因为草案不稳定的原因,在babel6中从内置插件中移除了对“装饰器”语法转换的支持,之前我们都是使用 “民间”的第三方插件(babel-plugin-transform-decorators-legacy)来转换装饰器语法,babel7中把这个插件纳入了babel的内置插件列表中,名字也改为 @babel/plugin-proposal-decorators
。配置起来比较简单
{
"presets": [["@babel/env"]],
"plugins": [["@babel/plugin-proposal-decorators", {
"legacy": true
}]]
}
官方提供的plugin 一般是用来对目前成型的标准或者草案进行通用的转换,那么我们是否可以根据自己的需求,来定制自己的转换规则?受益于babel提供的插件扩展机制,我们可以完成对语法树进行操作,从而对代码的转换,只需要关注语法的transform这个关键的步骤,自定义Visitor,利用babel提供的API可以方便的操作语法树的节点,其他的工作比如语法树解析,遍历算法、代码生成等由 Babel帮我们自动完成这些步骤。如果对编写babel插件有兴趣,可以去参考babel插件手册。(这篇文章写很详细,熟悉之后写插件没啥问题了, 如果第一次看,不用害怕,细心的看下去,多看一遍,可能一些陌生的词汇有些唬人, 利用Babel API操作AST ,相当于使用jQuery来操作DOM树)。一定要善于利用显示AST的神器astexplorer 或者 http://esprima.org/demo/parse.html 或者 使用JAVASCRIPT AST VISUALIZER可视化查看语法树结构
社区中有一些为了满足特定需求的plugin,对实际项目开发中很有用处,这里举例介绍两个,我们通过读这些插件的源码也能有助于我们掌握babel的插件开发
- babel-plugin-import 在import阶段进行转换,只导入需要的文件,将导入整个库的代码转为只导入单个的组件文件、避免导入整个库。适用于 antd, antd-mobile, lodash, material-ui
- babel-plugin-preval 在nodeJS环境下执行代码返回函数执行的结果。
- babel-plugin-codegen 在nodeJS环境下执行代码,返回的结果字符串,作为JavaScript语句表达式插入到代码中 比如楼上功能更强
- [babel-plugin-transform-remove-console] 功能如其名
这个时候就有一种需求:不改变babel配置的情况下,在应用的代码里面动态为特定的代码应用指定的语法转换,babel macros就是满足为了这个需求,babel macros 的想法的来源于create-react-app的一个issue,目前macros 已经在这个项目中使用了。宏的功能与babel plugin功能上差不多,babel plugin的引入需要在babel的配置文件中添加配置,只是宏是让我们可以对手动指定的代码进行语法转换的。babel 有许多plugin,也有许多的macro可用,不同的宏功能不同。macro的出现让我们在代码中显示的使用babel的转换功能。
如果是使用macros 需要先安装babel-plugin-macros,启用macros的能力 npm install --save-dev babel-plugin-macros
。babel-plugin-macros 不是一个具体的macro,只是给macro提供了一个运行的平台,不具有转换特定代码的功能,这里可以查看具体可用macros列表 根据需求安装对应的macros 在代码里面使用即可。说了这么多还是不直观,下面介绍下具体如何使用宏。
- penv.macro 它能用来在一个代码文件中统一管理你的环境变量, 并且只保留与当前环境变量匹配的值。与当前环境无关的代码被移除,确保不会将与指定环境不相干的代码发布到对应的环境上.
npm install --save-dev babel-plugin-macros
修改babel配置文件在plugins中 添加 macros (使用宏功能必须有这一步,提供一个运行宏的环境)npm install penv.macro --save-dev
- 编写源码
import env from 'penv.macro'
const BASE_URL = env({
development: 'https://development.example.com',
staging: 'https://staging.example.com',
production: (() => 'https://production.example.com')(),
})
假设编译的时候 env 为 process.env.NODE_ENV,编译后的结果为,功能上与webpack中的DefinePlugin功能类似,然而使用起来要感觉舒服很多。统一管理,不依赖webpack,方便就近维护
const BASE_URL = (() => 'https://production.example.com')()
宏为我们提供了解决问题的另一种思路,所有的宏都以/macro为后缀,在代码中显示引用,在需要的地方调用具体的 宏,也能方便对功能的理解(使用插件,语法被转换了,可能不清楚是什么原因造成的),增加新的宏也不需要修改babel的配置,同时也能避免插件顺序配置问题导致的冲突。这里有一些可用的宏列表 另外把插件转换为宏也很容易,比如 上面介绍的 preval、和codegen插件就有对应的 preval.macro 和 codegen.macro。也有人基于codegen.macro来实现国际化方案。另外更好的国际化方案可以使用 @lingui/macro
通过本篇 希望能对babel有一些了解,更多的是介绍介绍一些思路以及学习方向,很多东西要写,受限于笔墨,不敢写太多,里面涉及到的知识点,都可以深入来研究,另外文章中涉及链接文章都有很好的指导意义。
- babel官网 以及相关博客
- Babel 插件开发
- http://www.alloyteam.com/2017/04/analysis-of-babel-babel-overview/
- babel-plugin-macros
- zero-config-with-babel-macros
- How writing custom Babel & ESLint plugins can increase productivity & improve user experience
- https://github.com/kentcdodds/babel-plugin-macros/tree/master/other/docs
- Babel 用户手册
- penv.macro - 使环境变量的设置更加简单
- How to use Babel macros with React Native
- astexplorer.net
- Writing custom Babel and ESLint plugins
- Christoph Pojer: Evolving Complex Systems Incrementally | JSConf EU 2015
- Ramana Venkata: How to write a codemod
- Jamund Ferguson: Harnessing The Power of Abstract Syntax Trees
- James Kyle: How to Build a Compiler
- ESLint: Working with Plugins
- ESTree "spec"
- "How Writing a Babel Plugin is like jQuery"
- Babel Types Documentation
- All about macros