babel的基本概念
babel-core
可以看做babel的编译器。babel的核心api都在这里面,比如transform:主要都是处理转码的。它会把我们的js代码抽象成ast(abstract syntax tree),即源代码的抽象语法结构的树状表现形式。我们可以理解为,它定义的一种分析js语法的树状结构。也就是说es6的新语法与老语法是不一样的,那么我们怎么去定义这个语法的?所以必须要先转成ast,去发现这个语法的kind,分别做对应的处理,才能转化成es5
@babel/cli
@babel/cli是babel提供的命令行工具,用于命令行下编译源代码。
这里假定我们已通过npm init初始化项目。
首先,在项目中安装 @babel/cli:
npm install --save-dev @babel/core @babel/cli
如果你用过babel 6 可能就要问了,怎么不是 npm install --save-dev babel-cli ? 这个@符号又是什么?这是babel 7的一大调整,原来的babel-xxx包统一迁移到babel域下并且域由@符号来标识,一来便于区别官方与非官方的包,二来避免可能的包命名冲突。
现在假定我们的项目下有一个 script.js文件,内容是:
let fun = () => console.log('hello babel.js');
我们试试运行
npx babel script.js
结果
let fun = () => console.log('hello babel.js');
问题:
还是原来的代码,没有任何变化。说好的编译呢?
这个调整则是在babel 6里发生的。babel 6 做了大量模块化的工作,将原来集成一体的各种编译功能分离出去,独立成插件;这意味着,默认情况下,当下版本的babel不会编译代码,我们需要安装各式各样的插件。
babel插件
现在我们要将上面的箭头函数编译成es5 函数,需要安装额外的babel插件。
首先安装 @babel/pluin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions
然后,在命令行编译时指定使用该插件:
npx babel script.js --plugins @babel/plugin-transform-arrow-functions
编译后的内容输出为一个文件:
npx babel script.js -o ./dist/script.js --plugins @babel/plugin-transform-arrow-functions
结果:
let fun = function() {
return console.log('hello babel.js');
}
.babelrc
随着各种插件的加入,我们的命令行参数会越来越长。这时,我们可以新建一个 .babelrc文件,把各种命令行参数统一到其中。比如,要配置前面提到的箭头函数插件:
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
之后,在命令行只要运行 npx babel script.js 即可,babel会自动读取 .babelrc里的配置并应用到编译中
babel套餐:@babel/preset-env
假如我们现在有一个项目,页面需要支持 IE 10,但IE 10 不支持箭头函数、class及const,可是你喜欢用这些新增的javascript语法,你在项目里写了这么一段代码:
const alerMe = (msg) => {
window.alert(msg);
}
class Robot {
constructor(msg) {
this.message = msg;
}
say () {
alertMe(this.message);
}
}
const marvin = new Robot('hello babel');
显然,在IE 10 下这段代码报错了。好消息是,babel有各种插件满足你的上述需求。我们来安装相应插件:
npm install --save-dev
@babel/plugin-transform-arrow-functions
@babel/plugin-transform-block-scoping
@babel/plugin-transform-classes
接着,将它们加入 .babelrc配置文件中:
"plugins": [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-classes"
]
然后运行下面的命令,就有编译结果了。
npx babel script.js
问题:
这样安装插件、配置 .babelrc的过程非常乏味,而且很容易错。通常,我们不会关心到具体的某个ES5特性支持情况这个层面,我们更关心浏览器版本这个层面。我不想关心babel插件的配置,我只希望给bebel一个我想支持ID 10 的提示,babel就帮我编译出能在 IE 10 上运行的 JavaScript 代码。
!!!欢迎使用 @babel/preset-env
我们不妨把preset理解为套餐(预设),每个套餐里打包不同的插件,这样安装套餐就等于一次性安装各类babel插件。
首先在项目下安装:
npm install --save-dev @babel/preset-env
然后修改 .babelrc :
{
"presets": ["@babel/preset-env"]
}
运行
npx babel script.js
输出的结果与前面辛苦配置各种插件后输出的结果一样。可是,我们还没告诉 babel 我们要支持 IE 10,为什么它却好像预知一切:
我们来看 babel-preset-env 的一段文档:
Without any configuration options,babel-preset-env behaves exactly the same as babel-preset-latest (or babel-preset-es2015,babel-preset-es2016,babel-preset-es2017 together). 默认情况下,babel-preset-env 等效于三个套餐,而不巧我们前面安装的几个插件已经囊括在babel-preset-es2015中。
那么,如果我只想支持最新版本的Chrome呢?
这时我们可以调整 .babelrc的配置:
{
"presets": [
"@babel/preset-env", {
"target" : {
"browsers": ["last 1 Chrome version"],
"node": "current"
}
}
]
}
最新版本的Chrome已经支持箭头函数、class、const,所以Babel编译过程中不会编译
babel-polyfill
babel includes a polyfill that includes a custom regenerator runtime and core.js
基本上,babel-polyfill 就是regenerator runtime加 core-js。可是为什么需要polyfill这所谓的垫片?前面聊到@babel/preset-env时,不是说只要定义好我想支持的目标浏览器,babel就能编译出运行在目标浏览器上的代码。
我们暂时从polyfill 说起。举例说findIndex来说,IE 11仍不支持该方法,假如你的代码写了 findIndex,IE 11 浏览器会报:Object doesn't support property or method 'findIndex',怎么办呢?这时我们可以写个ployfill;那么什么是polyfill?如果目标环境中已经存在findIndex,我们什么都不做,如果没有,我们就在Array的原型中定义一个,这便是polyfill的意义。babel-polyfill同理。
虽然说浏览器的特性支持状况千差万别,但其实可以提炼出两类:
大家(浏览器)都有(语法),只有A语法和B语法的区别;
不是大家都有(api): 有的有,有的没有;
babel编译过程处理第一种情况,统一语法形态,通常是高版本语法编译成低版本的,比如ES6 语法编译成 ES5或者 ES3
babel-polyfill处理第二种情况,让目标浏览器支持所以特性,不管它是全局的还是原型或者其他。这样通过babel-polyfill,不同浏览器在特性支持上就站在同一起跑线上了。
babel-polyfill 的用法
安装babel-ployfill
npm install --save @babel/polyfill
使用 babel-polyfill
我们需要在程序入口文件的顶部引用 @babel-polyfill:
require('@babel/polyfill')
[].findIndex('babel')
或者使用ES6的语法:
import '@babel/polyfill'
[].findIndex('babel')
需要注意的是,babel-polyfill不能够多次引用。如果我们的代码中有多个require('babel/polyfill'),则执行时会报告错误;
only one instance of @babel/polyfill is allowed
这是因为引入的babel-polyfill 会在全局写入一个_babelPolyfill变量。第二次引入时,会检测变量是否已经存在,如果已存在,则抛出错误。(历史问题,现在已兼容)
babel-runtime
@babel/runtime是babel生态里最让人困惑的一个包,babel-runtime与babel-polyfill的区别究竟是什么?babel-polyfill把衬垫代码植入到了整个运行环境中去了,而babel-runtime是把衬垫代码放在了模块中,不会污染全局的环境;
我们拿Object.assign为例,剖析下 babel-polyfill与Babel-runtime的异同。
我们知道,IE 11不支持 Object.assign,此时,我们有两种候选方案: 1. 引入babel-polyfill,补丁一打,Object.assign 就被创造出来,配置@babel/plugin-transform-object-assign 2. babel会将所有的Object.assign 替换成_extends这样一个辅助函数。
如下所示:
Object.assign({}, {})
编译成:
function _extends() {
_extends = Object.assign || function (target) {for(var i = 1; i < arguments.length; i++){.....}
}
babel-runtime问题:
把衬垫代码独立的放在每个模块中,如果多个模块都有相同的api需要衬垫代码,每个模块都会重复声明一个函数?因为存在这个问题,则又有一个插件是可以解决这个问题的,那就是 @babel/plugin-transform-runtime
@babel/plugin-transform-runtime
我们首先安装插件
npm install --save-dev @babel/plugin-transform-runtime
然后再安装 babel-runtime:
npm install @babel/runtime
再然后在 .babelrc 中配置:
{
"plugins": [
"@babel/plugin-transform-object-assign",
"@babel/plugin-transform-runtime"
]
}
最后在命令行中执行
babel app2.js -o ./dist/index.js
编译后的结果:
这时候是api的衬垫代码也是个模块,通过在代码中引入衬垫模块的方式实现;这样我们不需要 babel-polyfill 也一样可以在程序中使用 Object.assign,编译后的代码最终能够运行在 IE 11 下。
至此,我想问题个问题,在经过 @babel/plugin-transform-runtime的处理后,IE 11 下现在有 Object.assign吗? 答案是,仍然没有。这正是 babel-polyfill与 babel-runtime的一大区别,前者改变目标浏览器,让你的浏览器拥有本来不支持的特性;后者改造你的代码,让你的代码能在所以目标浏览器上运行,但不改造浏览器。
babel-register
babel-register则提供了动态编译。换句话说,我们的源码能够真正运行在生产环境下,不需要babel编译这个环节。
那么如何使用呢?
1. 我们先在项目下安装 babel-register:
npm install --save-dev @babel/core @babel/register
2. 在入口文件中require:
require('@babel/register')
require('./app')
在入口文件头部引入@babel/register后,我们的app文件中即可任意使用 es2015(ES6)的新特性。
当然,坏处就是动态编译,导致程序速度性能上有所损耗。
babel-node
我们上面说,babel-register 提供动态编译,能够让我们的源代码真正在生产环境下,但其不然,我们仍需要做部分调整,比如新增一个入口文件,并在该文件中require('@babel/register')。而babel-node能够做到一行代码都不需要调整:
npm install --save-dev @babel/core @babel/node
npx babel-node app.js
只是,不要在生产环境中时候babel-node,因为它是动态编译源代码,应用启动速度非常慢。