说实话,我从工作开始就一直在接触babel
,然而对于babel
并没有一个清晰的认识,只知道babel
是用于编译javascript
,让开发者能使用超前的ES6+
语法进行开发。自己配置babel
的时候,总是遇到很多困惑,下面我就以babel@7
为例,重新简单认识下babel
。
什么是babel
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
babel
的配置文件一般是根目录下的.babelrc
,babel@7
目前已经支持babel.config.js
,不妨用babel.config.js
试试。
泰拳警告
babel
提供的基础能力是语法转换,或者叫语法糖转换。比如把箭头函数转为普通的function
,而对于ES6
新引入的全局对象是默认不做处理的,如Promise
, Map
, Set
, Reflect
, Proxy
等。对于这些全局对象和新的API
,需要用垫片polyfill
处理,core-js
有提供这些内容。
所以babel
做的事情主要是:
- 根据你的配置做语法糖解析,转换
- 根据你的配置塞入垫片
polyfill
如果不搞清楚这点,babel
的文档看起来会很吃力!
必须掌握的概念
plugins
babel
默认不做任何处理,需要借助插件来完成语法的解析,转换,输出。
插件分为语法插件Syntax Plugins
和转换插件Transform Plugins
。
语法插件
语法插件仅允许babel
解析语法,不做转换操作。我们主要关注的是转换插件。
转换插件
转换插件,顾名思义,负责的是语法转换。
转换插件将启用相应的语法插件,如果启用了某个语法的转换插件,则不必再另行指定相应的语法插件了。
语法转换插件有很多,从ES3
到ES2018
,甚至是一些实验性的语法和相关框架生态下的语法,都有相关的插件支持。
语法转换插件主要做的事情有:
利用@babel/parser
进行词法分析和语法分析,转换为AST
–> 利用babel-traverse
进行AST
转换(涉及添加,更新及移除节点等操作) –> 利用babel-generator
生成目标环境js
代码
插件简写
babel@7
之前的缩写形式是这样的:
1 | // 完整写法 |
而在babel@7
之后,由于plugins
都归到了@babel
目录下,所以简写形式也有所改变:
1 | // babel@7插件完整写法 |
插件开发
我们自己也可以开发插件,官网上的一个非常简单的小例子:
1 | export default function() { |
presets
preset
,意为“预设”,其实是一组plugin
的集合。我的理解是,根据这项配置,babel
会为你预设(或称为“内置”)好一些ECMA
标准,草案,或提案下的语法或API
,甚至是你自己写的一些语法规则。当然,这都是基于plugin
实现的。
官方presets
@babel/preset-env
@babel/preset-env
提供了一种智能的预设,根据配置的options
来决定支持哪些能力。
我们看看关键的options
有哪些。
- targets
描述你的项目要支持的目标环境。写法源于开源项目browserslist。这项配置应该根据你需要兼容的浏览器而设置,不必与其他人一模一样。示例如下:
1 | "targets": { |
- loose
可以直译为“松散模式”,默认为false
,即为normal
模式。简单地说,就是normal
模式转换出来的代码更贴合ES6
风格,更严谨;而loose
模式更像我们平时的写法。以class
写法举例:
我们先写个简单的class
:
1 | class TestBabelLoose { |
使用normal
模式编译得到结果如下:
1 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } |
而使用loose
模式编译得到结果是这样的,是不是更符合我们用prototype
实现类的写法?
1 | "use strict"; |
个人推荐配置loose: false
,当然也要结合项目实际去考量哪种模式更合适。
- modules
可选值有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false
,默认是auto
该配置将决定是否把ES6
模块语法转换为其他模块类型。注意,cjs
是commonjs
的别名。
其实我一直有个疑惑,为什么我看到的开源组件中,基本都是设置的modules: false
?后面终于明白了,原来这样做的目的是把转换模块类型的处理权交给了webpack
,由webpack
去处理这项任务。所以,如果你也使用webpack
,那么设置modules: false
就没错啦。
- useBuiltIns
可选值有:"entry" | "usage" | false
,默认是false
该配置将决定@babel/preset-env
如何去处理polyfill
"entry"
如果useBuiltIns
设置为"entry"
,我们需要安装@babel/polyfill
,并且在入口文件引入@babel/polyfill
,最终会被转换为core-js
模块和regenerator-runtime/runtime
。对了,@babel/polyfill
也不会处理stage <=3
的提案。
我们用一段包含了Promise
的代码来做下测试:
1 | import "@babel/polyfill"; |
但是编译后,貌似引入了很多polyfill
啊,一共149个,怎么不是按需引入呢?嗯…你需要往下看了。
1 | import "core-js/modules/es6.array.map"; |
"usage"
如果useBuiltIns
设置为"usage"
,我们无需安装@babel/polyfill
,babel
会根据你实际用到的语法特性导入相应的polyfill
,有点按需加载的意思。
1 | // 上个例子中,如果改用useBuiltIns: 'usage',最终转换的结果,只有四个模块 |
配置"usage"
时,常搭配corejs
选项来指定core-js
主版本号
1 | useBuiltIns: "usage", |
false
如果useBuiltIns
设置为false
,babel
不会自动为每个文件加上polyfill
,也不会把import "@babel/polyfill"
转为一个个独立的core-js
模块。
@babel/preset-env
还有一些配置,自己慢慢去折腾吧……
stage-x
stage-x
描述的是ECMA
标准相关的内容。根据TC39
(ECMA
39号技术专家委员会)的提案划分界限,stage-x
大致分为以下几个阶段:
- stage-0:
strawman
,还只是一种设想,只能由TC39
成员或者TC39
贡献者提出。 - stage-1:
proposal
,提案阶段,比较正式的提议,只能由TC39
成员发起,这个提案要解决的问题须有正式的书面描述,一般会提出一些案例,以及API
,语法,算法的雏形。 - stage-2:
draft
,草案,有了初始规范,必须对功能的语法和语义进行正式描述,包括一些实验性的实现,也可以提出一些待办事项。 - stage-3:
condidate
,候选,该提议基本已经实现,需要等待实践验证,用户反馈及验收测试通过。 - stage-4:
finished
,已完成,必须通过Test262
验收测试,下一步就是纳入到ECMA
标准中。比如一些ES2016
,ES2017
的语法就是通过这个阶段被合入ECMA
标准中了。
有兴趣了解的可以关注ecma262。
需要注意的是,babel@7已经移除了stage-x的preset,stage-4部分的功能已经被@babel/preset-env集成了,而如果你需要stage <= 3部分的功能,则需要自行通过plugins组装。
1 | As of v7.0.0-beta.55, we've removed Babel's Stage presets. |
自己写preset
如需创建一个自己的preset
,只需导出一份配置即可,主要是通过写plugins
来实现preset
。此外,我们也可以在自己的preset
中包含第三方的preset
。
1 | module.exports = function() { |
@babel/runtime
babel
运行时,很重要的一个东西,它一定程度上决定了你产出的包的大小!一般适合于组件库开发,而不是应用级的产品开发。
说明
这里有两个东西要注意,一个是@babel/runtime
,它包含了大量的语法转换包,会根据情况被按需引入。另一个是@babel/plugin-transform-runtime
,它是插件,负责在babel
转换代码时分析词法语法,分析出你真正用到的ES6+
语法,然后在transformed code
中引入对应的@babel/runtime
中的包,实现按需引入。
举个例子,我用到了展开运算符...
,那么经过@babel/plugin-transform-runtime
处理后的结果是这样的:
1 | /* 0 */ |
安装和简单配置
@babel/runtime
是需要按需引入到生产环境中的,而@babel/plugin-transform-runtime
是babel
辅助插件。因此安装方式如下:
1 | npm i --save @babel/runtime |
配置时也挺简单:
1 | const buildConfig = { |
@babel/runtime和useBuiltIns: ‘usage’有什么区别?
两者看起来都实现了按需加载的能力,但是实际上作用是不一样的。@babel/runtime
处理的是语法支持,把新的语法糖转为目标环境支持的语法;而useBuiltIns: 'usage'
处理的是垫片polyfill
,为旧的环境提供新的全局对象,如Promise
等,提供新的原型方法支持,如Array.prototype.includes
等。如果你开发的是组件库,一般不建议处理polyfill
的,应该由调用者去做这些支持,防止重复的polyfill
。
- 开发组件时,如果仅使用
@babel/plugin-transform-runtime
- 加上
useBuiltIns: 'usage'
,多了很多不必要的包。
babel@7要注意的地方
最后简单地提一下使用babel@7
要注意的地方,当然更详细的内容还是要看babel官方。
babel@7
相关的包命名都改了,基本是@babel/plugin-xxx
,@babel/preset-xxx
这种形式。这是开发插件体系时一个比较标准的命名和目录组织规范。- 建议用
babel.config.js
代替.babelrc
,这在你要支持不同环境时特别有用。 babel@7
已经移除了stage-x
的presets
,也不鼓励再使用@babel/polyfill
。- 不要再使用
babel-preset-es2015
,babel-preset-es2016
等preset
了,应该用@babel/preset-env
代替。 - ……
结语
本人只是对babel
有个粗略的认识,所以这是一篇babel
入门的简单介绍,并没有提到深入的内容,可能也存在错误之处。自己翻来覆去也看过好几遍babel
的文档了,一直觉得收获不大,也没理解到什么东西,在与webpack
配合使用的过程中,还是有很多疑惑没搞懂的。其实错在自己不该在复杂的项目中直接去实践。在最近重新学习webpack
和babel
的过程中,我觉得,对于不是很懂的东西,我们不妨从写一个hello world
开始,因为不是每个人都是理解能力超群的天才……