与babel的初次邂逅

207 阅读8分钟

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"
            }
        }
    ]
}

github.com/browserslis…

最新版本的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

编译后的结果:

avatar

这时候是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,因为它是动态编译源代码,应用启动速度非常慢。