入门babel,咱们须要了解些什么

说实话,我从工做开始就一直在接触babel,然而对于babel并无一个清晰的认识,只知道babel是用于编译javascript,让开发者能使用超前的ES6+语法进行开发。本身配置babel的时候,老是遇到不少困惑,下面我就以babel@7为例,从新简单认识下babeljavascript

什么是babel

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便可以运行在当前和旧版本的浏览器或其余环境中。

babel的配置文件通常是根目录下的.babelrcbabel@7目前已经支持babel.config.js,不妨用babel.config.js试试。java

泰拳警告

泰拳警告

babel提供的基础能力是语法转换,或者叫语法糖转换。好比把箭头函数转为普通的function,而对于ES6新引入的全局对象是默认不作处理的,如Promise, Map, Set, Reflect, Proxy等。对于这些全局对象和新的API,须要用垫片polyfill处理,core-js有提供这些内容。node

因此babel作的事情主要是:react

  1. 根据你的配置作语法糖解析,转换
  2. 根据你的配置塞入垫片polyfill

若是不搞清楚这点,babel的文档看起来会很吃力!webpack

必须掌握的概念

plugins

babel默认不作任何处理,须要借助插件来完成语法的解析,转换,输出。git

插件分为语法插件Syntax Plugins和转换插件Transform Pluginses6

语法插件

语法插件仅容许babel解析语法,不作转换操做。咱们主要关注的是转换插件。github

转换插件

转换插件,顾名思义,负责的是语法转换。web

转换插件将启用相应的语法插件,若是启用了某个语法的转换插件,则没必要再另行指定相应的语法插件了。

语法转换插件有不少,从ES3ES2018,甚至是一些实验性的语法和相关框架生态下的语法,都有相关的插件支持。算法

语法转换插件主要作的事情有:

利用@babel/parser进行词法分析和语法分析,转换为AST --> 利用babel-traverse进行AST转换(涉及添加,更新及移除节点等操做) --> 利用babel-generator生成目标环境js代码

插件简写

babel@7以前的缩写形式是这样的:

// 完整写法
plugins: [
    "babel-plugin-transform-runtime"
]
// 简写形式
plugins: [
    "transform-runtime"
]

而在babel@7以后,因为plugins都归到了@babel目录下,因此简写形式也有所改变:

// babel@7插件完整写法
plugins: [
    "@babel/plugin-transform-runtime"
]
// 简写形式,须要保留目录
plugins: [
    "@babel/transform-runtime"
]

插件开发

咱们本身也能够开发插件,官网上的一个很是简单的小例子:

export default function() {
  return {
    visitor: {
      Identifier(path) {
        const name = path.node.name;
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name
          .split("")
          .reverse()
          .join("");
      },
    },
  };
}

presets

preset,意为“预设”,实际上是一组plugin的集合。个人理解是,根据这项配置,babel会为你预设(或称为“内置”)好一些ECMA标准,草案,或提案下的语法或API,甚至是你本身写的一些语法规则。固然,这都是基于plugin实现的。

官方presets

@babel/preset-env

@babel/preset-env提供了一种智能的预设,根据配置的options来决定支持哪些能力。

咱们看看关键的options有哪些。

  • targets

描述你的项目要支持的目标环境。写法源于开源项目browserslist。这项配置应该根据你须要兼容的浏览器而设置,没必要与其余人如出一辙。示例以下:

"targets": {
  "browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
}
  • loose

能够直译为“松散模式”,默认为false,即为normal模式。简单地说,就是normal模式转换出来的代码更贴合ES6风格,更严谨;而loose模式更像咱们平时的写法。以class写法举例:

咱们先写个简单的class

class TestBabelLoose {
    constractor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}

new TestBabelLoose('Tusi')

使用normal模式编译获得结果以下:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var TestBabelLoose =
/*#__PURE__*/
function () {
  function TestBabelLoose() {
    _classCallCheck(this, TestBabelLoose);
  }

  _createClass(TestBabelLoose, [{
    key: "constractor",
    value: function constractor(name) {
      this.name = name;
    }
  }, {
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }]);

  return TestBabelLoose;
}();

new TestBabelLoose('Tusi');

而使用loose模式编译获得结果是这样的,是否是更符合咱们用prototype实现类的写法?

"use strict";

var TestBabelLoose =
/*#__PURE__*/
function () {
  function TestBabelLoose() {}

  var _proto = TestBabelLoose.prototype;

  _proto.constractor = function constractor(name) {
    this.name = name;
  };

  _proto.getName = function getName() {
    return this.name;
  };

  return TestBabelLoose;
}();

new TestBabelLoose('Tusi');

我的推荐配置loose: false,固然也要结合项目实际去考量哪一种模式更合适。

  • modules

可选值有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认是auto

该配置将决定是否把ES6模块语法转换为其余模块类型。注意,cjscommonjs的别名。

其实我一直有个疑惑,为何我看到的开源组件中,基本都是设置的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的代码来作下测试:

import "@babel/polyfill";

class TestBabelLoose {
    constractor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
    testPromise() {
        return new Promise(resolve => {
            resolve()
        })
    }
}
new TestBabelLoose('Tusi')

可是编译后,貌似引入了不少polyfill啊,一共149个,怎么不是按需引入呢?嗯...你须要往下看了。

import "core-js/modules/es6.array.map";
import "core-js/modules/es6.map";
import "core-js/modules/es6.promise";
import "core-js/modules/es7.promise.finally";
import "regenerator-runtime/runtime";
// 此处省略了144个包。。。

"usage"

若是useBuiltIns设置为"usage",咱们无需安装@babel/polyfillbabel会根据你实际用到的语法特性导入相应的polyfill,有点按需加载的意思。

// 上个例子中,若是改用useBuiltIns: 'usage',最终转换的结果,只有四个模块
import "core-js/modules/es6.object.define-property";
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es6.function.name";

配置"usage"时,常搭配corejs选项来指定core-js主版本号

useBuiltIns: "usage",
corejs: 3

false

若是useBuiltIns设置为falsebabel不会自动为每一个文件加上polyfill,也不会把import "@babel/polyfill"转为一个个独立的core-js模块。

  • @babel/preset-env还有一些配置,本身慢慢去折腾吧......

stage-x

stage-x描述的是ECMA标准相关的内容。根据TC39ECMA39号技术专家委员会)的提案划分界限,stage-x大体分为如下几个阶段:

  • stage-0:strawman,还只是一种设想,只能由TC39成员或者TC39贡献者提出。
  • stage-1:proposal,提案阶段,比较正式的提议,只能由TC39成员发起,这个提案要解决的问题须有正式的书面描述,通常会提出一些案例,以及API,语法,算法的雏形。
  • stage-2:draft,草案,有了初始规范,必须对功能的语法和语义进行正式描述,包括一些实验性的实现,也能够提出一些待办事项。
  • stage-3:condidate,候选,该提议基本已经实现,须要等待实践验证,用户反馈及验收测试经过。
  • stage-4:finished,已完成,必须经过Test262验收测试,下一步就是归入到ECMA标准中。好比一些ES2016ES2017的语法就是经过这个阶段被合入ECMA标准中了。

有兴趣了解的能够关注ecma262

须要注意的是,babel@7已经移除了stage-x的preset,stage-4部分的功能已经被@babel/preset-env集成了,而若是你须要stage <= 3部分的功能,则须要自行经过plugins组装。
As of v7.0.0-beta.55, we've removed Babel's Stage presets.
Please consider reading our blog post on this decision at
https://babeljs.io/blog/2018/07/27/removing-babels-stage-presets
for more details. TL;DR is that it's more beneficial in the long run to explicitly add which proposals to use.
If you want the same configuration as before:
{
  "plugins": [
    // Stage 2
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",
    // Stage 3
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    ["@babel/plugin-proposal-class-properties", { "loose": false }],
    "@babel/plugin-proposal-json-strings"
  ]
}

本身写preset

如需建立一个本身的preset,只需导出一份配置便可,主要是经过写plugins来实现preset。此外,咱们也能够在本身的preset中包含第三方的preset

module.exports = function() {
  return {
    // 增长presets项去包含别人的preset
    presets: [
      require("@babel/preset-env")
    ],
    // 用插件来包装成本身的preset
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC"
    ]
  };
}

@babel/runtime

babel运行时,很重要的一个东西,它必定程度上决定了你产出的包的大小!通常适合于组件库开发,而不是应用级的产品开发。

说明

这里有两个东西要注意,一个是@babel/runtime,它包含了大量的语法转换包,会根据状况被按需引入。另外一个是@babel/plugin-transform-runtime,它是插件,负责在babel转换代码时分析词法语法,分析出你真正用到的ES6+语法,而后在transformed code中引入对应的@babel/runtime中的包,实现按需引入。

举个例子,我用到了展开运算符...,那么通过@babel/plugin-transform-runtime处理后的结果是这样的:

/* 0 */
/***/ (function(module, exports, __webpack_require__) {

var arrayWithoutHoles = __webpack_require__(2);

var iterableToArray = __webpack_require__(3);

var nonIterableSpread = __webpack_require__(4);

function _toConsumableArray(arr) {
  return arrayWithoutHoles(arr) || iterableToArray(arr) || nonIterableSpread();
}

module.exports = _toConsumableArray;
    
// EXTERNAL MODULE: ../node_modules/@babel/runtime/helpers/toConsumableArray.js
var toConsumableArray = __webpack_require__(0);
var toConsumableArray_default = /*#__PURE__*/__webpack_require__.n(toConsumableArray);

安装和简单配置

@babel/runtime是须要按需引入到生产环境中的,而@babel/plugin-transform-runtimebabel辅助插件。所以安装方式以下:

npm i --save @babel/runtime
npm i --save-dev @babel/plugin-transform-runtime

配置时也挺简单:

const buildConfig = {
    presets: [
        // ......
    ],
    plugins: [
        "@babel/plugin-transform-runtime"
    ],
    // ......
}

@babel/runtime和useBuiltIns: 'usage'有什么区别?

二者看起来都实现了按需加载的能力,可是实际上做用是不同的。@babel/runtime处理的是语法支持,把新的语法糖转为目标环境支持的语法;而useBuiltIns: 'usage'处理的是垫片polyfill,为旧的环境提供新的全局对象,如Promise等,提供新的原型方法支持,如Array.prototype.includes等。若是你开发的是组件库,通常不建议处理polyfill的,应该由调用者去作这些支持,防止重复的polyfill

  • 开发组件时,若是仅使用@babel/plugin-transform-runtime

@babel/runtime打包分析

  • 加上useBuiltIns: 'usage',多了不少没必要要的包。

@babel/runtime + useBuiltIns: 'usage'打包分析

babel@7要注意的地方

最后简单地提一下使用babel@7要注意的地方,固然更详细的内容仍是要看babel官方

  • babel@7相关的包命名都改了,基本是@babel/plugin-xxx, @babel/preset-xxx这种形式。这是开发插件体系时一个比较标准的命名和目录组织规范。
  • 建议用babel.config.js代替.babelrc,这在你要支持不一样环境时特别有用。
  • babel@7已经移除了stage-xpresets,也不鼓励再使用@babel/polyfill
  • 不要再使用babel-preset-es2015, babel-preset-es2016preset了,应该用@babel/preset-env代替。
  • ......

结语

本人只是对babel有个粗略的认识,因此这是一篇babel入门的简单介绍,并无提到深刻的内容,可能也存在错误之处。本身翻来覆去也看过好几遍babel的文档了,一直以为收获不大,也没理解到什么东西,在与webpack配合使用的过程当中,仍是有不少疑惑没搞懂的。其实错在本身不应在复杂的项目中直接去实践。在最近从新学习webpackbabel的过程当中,我以为,对于不是很懂的东西,咱们不妨从写一个hello world开始,由于不是每一个人都是理解能力超群的天才......


首发于掘金社区

欢迎关注

相关文章
相关标签/搜索