8

github: babel归纳总结

在前端的发展过程中,javascript的兼容性,一直是前端头痛的问题,在以前的一些有些项目中,为解决浏览器兼容而花费的时间甚至还要多余实际的业务逻辑开发时间,babel就是其中处理兼容的转译工具(或者叫平台)。

babel是什么

javascript在不断发展,新的提案标准每年都会有,在得到广泛普及之前,Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本

babel的编译过程分为3步,解析(parse),转换(transform),生成(generate),对应的三个插件分别是Babylonbabel-traversebabel-generator
babylon将源码转换为抽象语法树(AST);babel-traverse通过AST生成一个便于操作、转换的path对象,供我们的babel插件处理;babel-generator读取AST并将其转换为代码和源码映射。这些过程不是本文的关注点,我们关注的是结果,哪些插件与我们的生产息息相关,我们如何去使用babel的插件。

通过vue中的babel配置来了解babel

vue脚手架生成的项目在.babelrc文件中的配置:

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"]
}

plugin配置项

babel插件推崇的是功能的单一性,就是每个插件的功能尽可能的单一,比如我要使用es6的箭头函数,那就可以装一个转换插件npm i -D @babel/plugin-transform-arrow-functions,将其写进.babelrc文件里就行了:详情

{
  "presets": [],
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

这样,我们写的:

(a) => [...a]

会被该插件转换为:

function (a) {
  return [...a]
}

这个插件也只解决箭头函数的转换,就算函数内部用了其它新语法也不管,这个好处是很明显的,就跟咱写项目推崇组件的细腻度一个道理

presets配置项

然而呢,js发展有点快,想一下那个es2015(es6)一下加了多少东西,我们要使用还得一个一个的npm i -D xxx,这个有点小麻烦,所以就可以采用presets配置项。npm i -D babel-preset-es2015,然后配置.babelrc详情

为了承接上文,这里暂时先用babel6的写法,babel7里也可以用babel-preset-es2015,但是文档里去掉了,es2015、es2016、es2017(2018年的东西直接写在env里了,7月份2019年的新标准就要来罗@_@)等都被放在env里面了,以后这几个preset会不会砍掉就不知道咯
{
  "presets": ["es2015"],
  "plugins": []
}

这样我们就可以使用包括箭头函数在内的es6的新语法而不用去担心兼容问题。这下这两个的关系也就清晰了,presets里面配置的是一些plugins集合

babel 7.3.0里面,presets -- 对应插件有这些:

  • env -- @babel/preset-env
  • stage-0 -- @babel/preset-stage-0
  • stage-1 -- @babel/preset-stage-1
  • stage-2 -- @babel/preset-stage-2
  • stage-3 -- @babel/preset-stage-3
  • flow -- @babel/preset-flow
  • react -- @babel/preset-react
  • minify -- babel-preset-minify
  • typescript -- @babel/preset-typescript

env

在presets配置里面,我们看到了:

["env", {
  "modules": false,
  "targets": {
    "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
  }
}]

这个env是@babel/preset-env这个集合插件配置项,这里的配置项:

  • modules:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false, defaults to "auto".

    • 意思就是让babel把es6 模块转化为其它模块化类型。如果选择 false 就不进行模块化转,我们的目标是浏览器,es6以前js是没有模块化的,commonjs、amd等只是社区方案,没有浏览器支持的,所以我们设置为false,如果我们写node上运行的代码,就要设置为"commonjs"
  • target:就是告诉babel你的js要兼容哪些环境,它会帮你将你写的js转译成目标环境兼容的js语法,这个具体配置可以看browserslist

那就是说,js无论用什么新玩意,@babel/preset-env都能跟我兼容到我想要的环境?带着问题,我们再看看官网的介绍,What is Babel

  1. Transform syntax(转换语法)
  2. Polyfill features that are missing in your target environment (Ployfill新特性--也就是Api)
  3. Source code transformations(源码转换)
  4. And more!

再看看通过env转换的几个demo:

a => a
// 转为
function (a) {return a}

function func (b = false) {return false}
// 转为
function func (b) {
  b = b || false
  return b
}

比较明显,上面的都属于转换语法,所以应该说“js无论用什么新语法@babel/preset-env都能帮你兼容到目标环境”。

@babel/preset-env具体可以帮我们转化哪些呢?看这儿JavaScript新特性和Babel插件的映射关系,这个是@babel/preset-env集合插件所包含的插件列表,每个插件对应转换一个新特性,至于没有的,比如promise、Array.from等,请往下看。

stage-2

在上面的配置中,我们看到env下面有个stage-2。stage-x,这里面包含的都是当年最新规范的草案,每年更新。细分为如下几步

  • Stage 0 - 稻草人: 只是一个想法,经过 TC39 成员提出即可。
  • Stage 1 - 提案: 初步尝试。
  • Stage 2 - 初稿: 完成初步规范。
  • Stage 3 - 候选: 完成规范和浏览器初步实现。
  • Stage 4 - 完成: 将被添加到下一年度发布。

官网里有一句话It is important to note that @babel/preset-env does not support stage-x plugins.,就是说@babel/preset-env中不包含在草案阶段的新属性的转换插件

其实我们通过plugin-features,以及proposals/finished-proposals(其中2019就是今年的stage-4),可以发现@babel/preset-env是包含了stage-4阶段的plugins的。

比如写react的同学比较熟悉的decorators目前就处于stage-2阶段,我们要用这些处于草案阶段的新属性,可以安装npm i -D @babel/preset-stage-2,然后在presets里写上stage-2,babel就会通过那些处于草案阶段的新属性的插件将我们代码中的用到的新属性转译成为es5

此外,低一级的 stage 会包含所有高级 stage 的内容,例如 stage-2 会包含 stage-2, stage-3 的所有内容。

babel-ployfill

Babel 几乎可以编译所有时新的 JavaScript 语法,但对于 APIs 来说却并非如此。比如说:PromiseWeakMapArray.fromObject.assignArray.prototype.includesgenerator等。为了解决这个问题,我们使用一种叫做 Polyfill(代码填充,也可译作兼容性补丁) 的技术。能让你提前使用还不可用的 APIs。

引入它很简单,我们npm i -S @babel/polyfill

  • 在vue中的入口文件main.js文件的最上面:

    import "@babel/polyfill";
  • 或者在webpack入口里引入:

      module.exports = {
        entry: ["@babel/polyfill", "./main.js"],
      };

两者任选其一

上面这两种方式是将整个polyfill都引入了,很多代码其实对我们是没有用的,比如,我们的env配置的是不需要兼容ie9以下的浏览器,这种引入方式把所有兼容ie的代码都引入了,包含ie8以下,所以,一般我们会在.babelrc文件里的env里配置下useBuiltIns参数,这样babel在引入的时候会根据我们env环境去加载相应的polyfill:详细

有如下三种方式

  • 如果在 .babelrc 中设置 useBuiltIns: 'usage',则不要在 webpack.config.js entry 数组或 源码中包含 @babel/polyfill。注意,仍然需要安装 babel-polyfill(就是说 npm i -S @babel/polyfill后就不管了)。
  • 如果在 .babelrc 中设置 useBuiltIns: 'entry',在应用程序入口(main.js)的顶部引入 @babel/polyfill。
  • 如果在 .babelrc 中没有明确设置useBuiltIns的值(就是你没有去配置这项)或者设置了 useBuiltIns: false,将 @babel/polyfill 添加到 webpack.config.js 的入口数组中。
// .babelrc
{
  ["env", {
    "modules": false,
    "targets": {
      "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
    },
    // 是否自动引入polyfill,开启此选项必须保证已经安装了babel-polyfill
    // 在这里设置自动引入后,babel会根据你配置的兼容的环境,去按需加载polyfill的代码,这样能保证代码量最少
    // 参数:Boolean,默认为false.
    "useBuiltIns": false
  }]
}
// webpack.base.conf.js
module.exports = {
  entry: ["@babel/polyfill", "./main.js"],
};

@babel/plugin-transform-runtime

我们看到上面的配置中有个transform-runtime,这个是配置@babel/plugin-transform-runtime,它是做什么的呢?官网说:一个插件,通过重复使用babel注入的助手(helper)代码,来减少代码体积,我们看看它是如何工作的。

npm i -D @babel/plugin-transform-runtime

// .babelrc
{
  "plugins": [
    "@babel/plugin-transform-runtime",
    // 默认配置
      {
        "absoluteRuntime": false,
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
  ]
}

比如这个es6的class类:

class Person {
}

在没有使用transform-runtime时,每个使用class函数处,Babel 会生成class的helper函数放置在文件顶部,就是说在多个文件里使用了class, babel就会在每个文件里面都生成相同的helper:

"use strict";

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

var Person = function Person() {
  _classCallCheck(this, Person);
};

这样不必要的重复会使我们的代码体积非常雍肿,transform-runtime就是来解决这个重复生成helper的问题的,它会将这个es6的class语法的helper函数放在babel-runtime/helpers里,然后在使用处通过require引入,这样就没必要在每个使用处重复定义helper了,达到了减少代码体积的效果。

"use strict";

var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var Person = function Person() {
  (0, _classCallCheck3.default)(this, Person);
};

@babel/runtime 对比 babel-polyfill

@babel/runtime和@babel/polyfill这两个模块功能几乎相同,就是转码新增 api

  • @babel/polyfill 把原生的方法重写了,以promise为例,判断环境promise存不存在,不存在就写个全局作用域的promise。它会一次引入所有的api的polyfill,只是根据env配置引入的包大小可能会不同。
  • @babel/runtime 是写了个helper函数,以promise为例,你代码中的promise都会被换成_promise,然后babel会生成一个_promise helper函数,大致也是目标环境存在就用原生的,不存在就用polyfill的promise。而且这个是按需引入的,如果你的项目中只使用了promise,就只会引入promise的polyfill。但是它有个问题,实例上的方法无能为力,比如 Array上的form方法,String上的includes等

根据它们两的特点,@babel/polyfil一般用于前端项目,@babel/runtime一般用来写插件

几个常用的babel插件

babel-cil

Babel 的 CLI 是一种在命令行下使用 Babel 编译文件的简单方法。有时候我们只是写一个插件,需要用babel转一下我们代码中的高阶语法,因为项目可能不太大,用不到构建工具,就可以用babel-cil。转换依据我们的.babelrc文件或者package.json中babel选项

  • 编译一个文件

    babel my-file.js
  • 如果我们想要把一个目录整个编译成一个新的目录,可以使用 --out-dir 或者 -d。.

    $ babel src --out-dir lib
    # 或
    $ babel src -d lib

babel-loader

babel-loader是什么呢?前面说了,我们可以通过babel-cil在命令行里告诉babel转译哪些js,也可以通过babel-register,在代码里通过require来转,但是,现在前端开发是一个工程化过程,依赖关系比较复杂,在一个稍微大点儿的项目中还真没法手动告诉babel要处理哪些文件,比如一个.vue文件,里面还包含html、css,还有一些不是js的鬼语法,这时候就要借助其它插件先提前处理下,所以,webpack根据依赖关系,加载文件的时候遇到js文件后,会将文件内容的js字符串根据loader配置的先后顺序,挨个儿传递给它们处理,babel-loader就是其中之一

总结

什么是babel

babel就是将目标环境(浏览器)通过打补丁升级成支持最新javascript语法的环境的工具。

用vue脚手架生成的项目,js怎么兼容到ie9

// .babelrc
{
    "presets": [
      ["env", {
        // 这里默认是false,不用再写一遍
-        // "modules": false,
        // 一般不单独写出来,babel/preset-env会自个读取package里面的browserslist,与css兼容环境保持一致
        // https://github.com/browserslist/browserslist
-      //  "targets": {
-        //  "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
-      //  },
      }],
      "stage-2"
    ],
    "plugins": ["transform-vue-jsx", "transform-runtime"]
  }

// webpack.base.conf.js
module.exports = {
  entry: ["@babel/polyfill", "./main.js"],
};

插件快照

名称 作用 备注
babel/cli 允许命令行使用 babel 命令转译文件 一般在写插件时使用
babel/polyfill 为所有 API 增加兼容方法 需要在所有代码之前 require,且体积比较大
babel/plugin-transform-runtime 把帮助类方法从每次使用前定义改为统一 require,精简代码 ---
babel/runtime helper库 需要安装为依赖,而不是开发依赖,node环境使用,web环境不需要
babel/loader babel插件在webpack项目中的一个入口 ---
babel/core babel的polyfill库 ---
babel/preset-env babel预制环境的集合插件,通过配置目标环境,转换标准上的新特性 只转新特性,不转api
babel/preset-stage-2 转换草案stage-2以及stage-3阶段的的新属性 ---

参考:


张功涛
312 声望15 粉丝

good good study, day day up