博客
博客
文章目录
  1. 背景
  2. ES6
  3. Babel 的版本变更
  4. babel 各个模块介绍
    1. 1、babel-core
    2. 2、babel-cli
    3. 3、babel-node
    4. 4、babel-register
    5. 5、babel-polyfill
  5. .babelrc
    1. plugins
    2. presets
  6. 参考

babel 到底该如何配置?

背景

说起 ES6,webpack,打包,模块化总是离不开 babel,babel 作为一个 js 的编译器已经被广泛使用。在 babel 的 官网 是这样介绍它的:

Babel is a JavaScript compiler.

Use next generation JavaScript, today.

大家都知道 js 作为宿主语言,很依赖执行的环境(浏览器、node 等),不同环境对 js 语法的支持不尽相同,特别是 ES6 之后,ECMAScrip 对版本的更新已经到了一年一次的节奏,虽然每年更新的幅度不大,但是每年的提案可不少。babel 的出现就是为了解决这个问题,把那些使用新标准编写的代码转译为当前环境可运行的代码,简单点说就是把 ES6 代码转译(转码 + 编译)到 ES5。

经常有人在使用 babel 的时候并没有弄懂 babel 是干嘛的,只知道要写 ES6 就要在 webpack 中引入一个 babel-loader,然后胡乱在网上 copy 一个.babelrc 到项目目录就开始了(ps: 其实我说的是我自己)。理解 babel 的配置很重要,可以避免一些不必要的坑,比如:代码中使用 Object.assign 在一些低版本浏览器会报错,以为是 webpack 打包时出现了什么问题,其实是 babel 的配置问题。


ES6

正文之前先谈谈 ES6,ES 即 ECMAScript,6 表示第六个版本 (也被称为是 ES2015,因为是 2015 年发布的),它是 javascript 的实现标准。

被纳入到 ES 标准的语法必须要经过如下五个阶段:

  1. Stage 0: strawman
  2. Stage 1: proposal
  3. Stage 2: draft - 必须包含 2 个实验性的具体实现 ,其中一个可以是用转译器实现的,例如 Babel。
  4. Stage 3: candidate - 至少要有 2 个符合规范的具体实现
  5. Stage 4: finished

可以看到提案在进入 stage3 阶段时就已经在一些环境被实现,在 stage2 阶段有 babel 的实现。所以被纳入到 ES 标准的语法其实在大部分环境都已经是有了实现的,那么为什么还要用 babel 来进行转译,因为不能确保每个运行代码的环境都是最新版本并已经实现了规范。

更多关于 ES6 的内容可以参考 hax 的 live:Hax:如何学习和实践 ES201X?


Babel 的版本变更

写这篇文章时 babel 版本已经到了 v7.0.0-beta.3, 也就是说 7.0 的正式版就要发布了,可喜可贺。但是今天不谈 7.0,只谈 babel6,在我知道并开始使用的 babel 的时候 babel 已经到了版本 6,没有经历过 5 的时代。

在 babel5 的时代,babel 属于全家桶型,只要安装 babel 就会安装 babel 相关的所有工具,
即装即用。

但是到了 babel6,具体有以下几点变更:

  • 移除 babel 全家桶安装,拆分为单独模块,例如:babel-core、babel-cli、babel-node、babel-polyfill 等;
    可以在 babel 的 github 仓库看到 babel 现在有哪些模块。
    babel-package
  • 新增 .babelrc 配置文件,基本上所有的 babel 转译都会来读取这个配置;
  • 新增 plugin 配置,所有的东西都插件化,什么代码要转译都能在插件中自由配置;
  • 新增 preset 配置,babel5 会默认转译 ES6 和 jsx 语法,babel6 转译的语法都要在 perset 中配置,preset 简单说就是一系列 plugin 包的使用。

babel 各个模块介绍

babel6 将 babel 全家桶拆分成了许多不同的模块,只有知道这些模块怎么用才能更好的理解 babel。

下面的一些示例代码已经上传到了 github,欢迎访问,欢迎 star。

安装方式:

# 通过 npm 安装 
npm install babel-core babel-cli babel-node

# 通过 yarn 安装
yarn add babel-core babel-cli babel-node
1、babel-core

看名字就知道,babel-core 是作为 babel 的核心存在,babel 的核心 api 都在这个模块里面,比如:transform。

下面介绍几个 babel-core 中的 api

  • babel.transform:用于字符串转码得到 AST
/*
* @param {string} code 要转译的代码字符串
* @param {object} options 可选,配置项
* @return {object}
*/
babel.transform (code: string, options?: Object)

// 返回一个对象 (主要包括三个部分):
{
generated code, // 生成码
sources map, // 源映射
AST // 即 abstract syntax tree,抽象语法树
}

更多关于 AST 知识点请看 这里

一些使用 babel 插件的打包或构建工具都有使用到这个方法,下面是一些引入 babel 插件中的源码:

//gulp-babel
const babel = require('babel-core');
/*
some codes...
*/
module.exports = function (opts) {
opts = opts || {};
return through.obj (function (file, enc, cb) {
try {
const fileOpts = Object.assign ({}, opts, {
filename: file.path,
filenameRelative: file.relative,
sourceMap: Boolean(file.sourceMap),
sourceFileName: file.relative,
sourceMapTarget: file.relative
});
const res = babel.transform (file.contents.toString (), fileOpts);
if (res !== null) {
//some codes
}
} catch (err) {
//some codes
}
}
}

//babel-loader
var babel = require("babel-core");
/*
some codes...
*/
var transpile = function transpile(source, options) {
//some code
try {
result = babel.transform (source, options);
} catch (error) {
//some codes
}
//some codes
}

//rollup-pugin-babel
import { buildExternalHelpers, transform } from 'babel-core';
/*
some codes...
*/
export default function babel ( options ) {
//some codes
return {
//some methods
transform ( code, id ) {
const transformed = transform ( code, localOpts );
//some codes
return {
code: transformed.code,
map: transformed.map
};
}
}
}

上面是一些打包工具引入 babel 插件时的一些源码,可以看到基本都是先通过调用 transform 方法进行代码转码。

  • babel.transformFile
// 异步的文件转码方式,回调函数中的 result 与 transform 返回的对象一至。
babel.transformFile ("filename.js", options, function (err, result) {
result; // => { code, map, ast }
});
  • babel.transformFileSync
// 同步的文件转码方式,返回结果与 transform 返回的对象一至。
babel.transformFileSync (filename, options) // => { code, map, ast }
  • babel.transformFromAst
// 将 ast 进行转译 
const { code, map, ast } = babel.transformFromAst (ast, code, options);
2、babel-cli

babel-cli 是一个通过命令行对 js 文件进行换码的工具。

使用方法:

  • 直接在命令行输出转译后的代码

    babel script.js
  • 指定输出文件

    babel script.js --out-file build.js
    或者是
    babel script.js -o build.js

让我们来编写了一个具有箭头函数的代码:

//script.js
const array = [1,2,3].map ((item, index) => item * 2);

然后在命令行执行 babel script.js,发现输出的代码好像没有转译。

babel 转译

因为我们没有告诉 babel 要转译哪些类型,现在看看怎么指定转译代码中的箭头函数。

babel --plugins transform-es2015-arrow-functions script.js

转译箭头函数

或者在目录里添加一个.babelrc 文件,内容如下:

{
"plugins": [
"transform-es2015-arrow-functions"
]
}

.babelrc 是 babel 的全局配置文件,所有的 babel 操作(包括 babel-core、babel-node)基本都会来读取这个配置,后面会详细介绍。

3、babel-node

babel-node 是随 babel-cli 一起安装的,只要安装了 babel-cli 就会自带 babel-node。
在命令行输入 babel-node 会启动一个 REPL(Read-Eval-Print-Loop),这是一个支持 ES6 的 js 执行环境。

测试 babel-node

其实不用 babel-node,直接在 node 下,只要 node 版本大于 6 大部分 ES6 语法已经支持,况且现在 node 的版本已经到了 8.7.0。

node 环境箭头函数测试

babel-node 还能直接用来执行 js 脚本,与直接使用 node 命令类似,只是会在执行过程中进行 babel 的转译,并且 babel 官方不建议在生产环境直接这样使用,因为 babel 实时编译产生的代码会缓存在内存中,导致内存占用过高,所以我们了解了解就好。

babel-node script.js
4、babel-register

babel-register 字面意思能看出来,这是 babel 的一个注册器,它在底层改写了 node 的 require 方法,引入 babel-register 之后所有 require 并以.es6, .es, .jsx 和 .js 为后缀的模块都会经过 babel 的转译。

同样通过箭头函数做个实验:

//test.js
const name = 'shenfq';
module.exports = () => {
const json = {name};
return json;
};
//main.js
require('babel-register');
var test = require('./test.js'); //test.js 中的 es6 语法将被转译成 es5

console.log (test.toString ()); // 通过 toString 方法,看看控制台输出的函数是否被转译

register 转译

默认 babel-register 会忽略对 node_modules 目录下模块的转译,如果要开启可以进行如下配置。

require("babel-register")({
ignore: false
});

babel-register 与 babel-core 会同时安装,在 babel-core 中会有一个 register.js 文件,所以引入 babel-register 有两种方法:

require('babel-core/register');
require('babel-register');

但是官方不推荐第一种方法,因为 babel-register 已经独立成了一个模块,在 babel-core 的 register.js 文件中有如下注释。

TODO: eventually deprecate this console.trace (“use thebabel-registerpackage instead ofbabel-core/register“);

5、babel-polyfill

polyfill 这个单词翻译成中文是 垫片 的意思,详细点解释就是桌子的桌脚有一边矮一点,拿一个东西把桌子垫平。polyfill 在代码中的作用主要是用已经存在的语法和 api 实现一些浏览器还没有实现的 api,对浏览器的一些缺陷做一些修补。例如 Array 新增了 includes 方法,我想使用,但是低版本的浏览器上没有,我就得做兼容处理:

if (!Array.prototype.includes) {
Object.defineProperty (Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
var len = o.length >>> 0;
if (len === 0) {
return false;
}
var n = fromIndex | 0;
var k = Math.max (n >= 0 ? n : len - Math.abs (n), 0);
while (k < len) {
if (o [k] === searchElement) {
return true;
}
k++;
}
return false;
}
});
}

上面简单的提供了一个 includes 方法的 polyfill,代码来自 MDN

理解 polyfill 的意思之后,再来说说 babel 为什么存在 polyfill。因为 babel 的转译只是语法层次的转译,例如箭头函数、解构赋值、class,对一些新增 api 以及全局函数(例如:Promise)无法进行转译,这个时候就需要在代码中引入 babel-polyfill,让代码完美支持 ES6 + 环境。前面介绍的 babel-node 就会自动在代码中引入 babel-polyfill 包。

引入方法:

// 在代码的最顶部进行 require 或者 import

require("babel-polyfill");

import "babel-polyfill";

// 如果使用 webpack,也可以在文件入口数组引入
module.exports = {
entry: ["babel-polyfill", "./app/js"]
};

但很多时候我们并不会使用所有 ES6 + 语法,全局添加所有垫片肯定会让我们的代码量上升,之后会介绍其他添加垫片的方式。


.babelrc

前面已经介绍了 babel 常用的一些模块,接下来看看 babel 的配置文件 .babelrc

后面的后缀 rc 来自 linux 中,使用过 linux 就知道 linux 中很多 rc 结尾的文件,比如 .bashrc,rc 是 run command 的缩写,翻译成中文就是运行时的命令,表示程序执行时就会来调用这个文件。

babel 所有的操作基本都会来读取这个配置文件,除了一些在回调函数中设置 options 参数的,如果没有这个配置文件,会从 package.json 文件的 babel 属性中读取配置。

plugins

先简单介绍下 plugins ,babel 中的插件,通过配置不同的插件才能告诉 babel,我们的代码中有哪些是需要转译的。

这里有一个 babel 官网的 插件列表,里面有目前 babel 支持的全部插件。

举个例子:

{
"plugins": [
"transform-es2015-arrow-functions", // 转译箭头函数
"transform-es2015-classes", // 转译 class 语法
"transform-es2015-spread", // 转译数组解构
"transform-es2015-for-of" // 转译 for-of
]
}
// 如果要为某个插件添加配置项,按如下写法:
{
"plugins":[
// 改为数组,第二个元素为配置项
["transform-es2015-arrow-functions", { "spec": true }]
]
}

上面这些都只是语法层次的转译,前面说过有些 api 层次的东西需要引入 polyfill,同样 babel 也有一系列插件来支持这些。

{
"plugins":[
// 如果我们在代码中使用 Object.assign 方法,就用如下插件
"transform-object-assign"
]
}

// 写了一个使用 Object.assign 的代码如下:
const people = Object.assign ({}, {
name: 'shenfq'
});
// 经过 babel 转译后如下:
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call (source, key)) { target [key] = source [key]; } } } return target; };

const people = _extends ({}, {
name: 'shenfq'
});

这种通过 transform 添加的 polyfill 只会引入到当前模块中,试想实际开发中存在多个模块使用同一个 api,每个模块都引入相同的 polyfill,大量重复的代码出现在项目中,这肯定是一种灾难。另外一个个的引入需要 polyfill 的 transform 挺麻烦的,而且不能保证手动引入的 transform 一定正确,等会会提供一个解决方案:transform-runtime

除了添加 polyfill,babel 还有一个工具包 helpers,如果你有安装 babel-cli,你可以直接通过下面的命令把这个工具包输出:

./node_modules/.bin/babel-external-helpers > helpers.js

这个工具包类似于 babel 的 utils 模块,就像我们项目中的 utils 一样,很多地方都会用到,例如 babel 实现 Object.assign 就是使用的 helpers 中的_extend 方法。为了避免同一个文件多次引用 babel 的助手函数,通过 external-helpers 插件,能够把这些助手函数抽出放到文件顶部,避免多次引用。

// 安装: cnpm install --save-dev babel-plugin-external-helpers

// 配置
{
"plugins": ["external-helpers"]
}

虽然这个插件能避免一个文件多次引用助手函数,但是并不能直接避免多个文件内重复引用,这与前面说到的通过 transform 添加 polyfill 是一样的问题,这些引用都只是 module 级别的,在打包工具盛行的今天,需要考虑如何减少多个模块重复引用相同代码造成代码冗余。

当然也可以在每个需要使用 helpers 的 js 文件顶部直接引入之前生成的 helpers 文件既可,通过打包工具将这个公共模块进行抽离。

require('helpers');

在说完 babel 的 helpers 之后就到了插件系统的最后的一个插件:transform-runtime。前面在 transform-polyfill 的时候也有提到这个插件,之所以把它放到 helpers 后面是因为这个插件能自动为项目引入 polyfill 和 helpers。

cnpm install -D babel-plugin-transform-runtime babel-runtime

transform-runtime 这个插件依赖于 babel-runtime,所以安装 transform-runtime 的同时最好也安装 babel-runtime,为了防止一些不必要的错误。babel-runtime 由三个部分组成:

  1. core-js

    core-js 极其强悍,通过 ES3 实现了大部分的 ES5、6、7 的垫片,作者 zloirock 是来自战斗名族的程序员,一个人维护着 core-js,听说他最近还在找工作,上面是 core-js 的 github 地址,感兴趣可以去看看。

  2. regenerator

    regenerator 来自 facebook 的一个库,用于实现 generator functions。

  3. helpers

    babel 的一些工具函数,没错,这个 helpers 和前面使用 babel-external-helpers 生成的 helpers 是同一个东西

从 babel-runtime 的 package.json 文件中也能看出,runtime 依赖了哪些东西。

babel-runtime 的 package.json

安装有 babel-runtime 之后要引入 helpers 可以使用如下方式:

require('babel-runtime/helpers');

使用 runtime 的时候还有一些配置项:

{
"plugins": [
["transform-runtime", {
"helpers": false, // 自动引入 helpers
"polyfill": false, // 自动引入 polyfill(core-js 提供的 polyfill)
"regenerator": true, // 自动引入 regenerator
}]
]
}

比较 transform-runtime 与 babel-polyfill 引入垫片的差异:

  1. 使用 runtime 是按需引入,需要用到哪些 polyfill,runtime 就自动帮你引入哪些,不需要再手动一个个的去配置 plugins,只是引入的 polyfill 不是全局性的,有些局限性。而且 runtime 引入的 polyfill 不会改写一些实例方法,比如 Object 和 Array 原型链上的方法,像前面提到的 Array.protype.includes
  2. babel-polyfill 就能解决 runtime 的那些问题,它的垫片是全局的,而且全能,基本上 ES6 中要用到的 polyfill 在 babel-polyfill 中都有,它提供了一个完整的 ES6 + 的环境。babel 官方建议只要不在意 babel-polyfill 的体积,最好进行全局引入,因为这是最稳妥的方式。
  3. 一般的建议是开发一些框架或者库的时候使用不会污染全局作用域的 babel-runtime,而开发 web 应用的时候可以全局引入 babel-polyfill 避免一些不必要的错误,而且大型 web 应用中全局引入 babel-polyfill 可能还会减少你打包后的文件体积(相比起各个模块引入重复的 polyfill 来说)。

presets

显然这样一个一个配置插件会非常的麻烦,为了方便,babel 为我们提供了一个配置项叫做 persets(预设)。

预设就是一系列插件的集合,就好像修图一样,把上次修图的一些参数保存为一个预设,下次就能直接使用。

如果要转译 ES6 语法,只要按如下方式配置即可:

// 先安装 ES6 相关 preset: cnpm install -D babel-preset-es2015
{
"presets": ["es2015"]
}

// 如果要转译的语法不止 ES6,还有各个提案阶段的语法也想体验,可以按如下方式。
// 安装需要的 preset: cnpm install -D babel-preset-stage-0 babel-preset-stage-1 babel-preset-stage-2 babel-preset-stage-3
{
"presets": [
"es2015",
"stage-0",
"stage-1",
"stage-2",
"stage-3",
]
}

// 同样 babel 也能直接转译 jsx 语法,通过引入 react 的预设
//cnpm install -D babel-preset-react
{
"presets": [
"es2015",
"react"
]
}

不过上面这些 preset 官方现在都已经不推荐了,官方 唯一推荐 preset:babel-preset-env

这款 preset 能灵活决定加载哪些插件和 polyfill,不过还是得开发者手动进行一些配置。

//cnpm install -D babel-preset -env
{
"presets": [
["env", {
"targets": { // 指定要转译到哪个环境
// 浏览器环境
"browsers": ["last 2 versions", "safari >= 7"],
//node 环境
"node": "6.10", //"current" 使用当前版本的 node

},
// 是否将 ES6 的模块化语法转译成其他类型
// 参数:"amd" | "umd" | "systemjs" | "commonjs" | false,默认为 'commonjs'
"modules": 'commonjs',
// 是否进行 debug 操作,会在控制台打印出所有插件中的 log,已经插件的版本
"debug": false,
// 强制开启某些模块,默认为 []
"include": ["transform-es2015-arrow-functions"],
// 禁用某些模块,默认为 []
"exclude": ["transform-es2015-for-of"],
// 是否自动引入 polyfill,开启此选项必须保证已经安装了 babel-polyfill
// 参数:Boolean,默认为 false.
"useBuiltIns": false
}]
]
}

关于最后一个参数 useBuiltIns,有两点必须要注意:

  1. 如果 useBuiltIns 为 true,项目中必须引入 babel-polyfill。
  2. babel-polyfill 只能被引入一次,如果多次引入会造成全局作用域的冲突。

做了个实验,同样的代码,只是 .babelrc 配置中一个开启了 useBuiltIns,一个没有,两个 js 文件体积相差 70K,戳我看看

文件 大小
useBuiltIns.js 189kb
notUseBuiltIns.js 259kb

最后啰嗦一句

关于 polyfill 还有个叫做 polyfill.io 的神器,只要在浏览器引入

https://cdn.polyfill.io/v2/polyfill.js

服务器会更具浏览器的 UserAgent 返回对应的 polyfill 文件,很神奇,可以说这是目前最优雅的解决 polyfill 过大的方案。


前前后后写完这个差不多写了一个星期,查了很多资料(babel 的官网和 github 都看了好几遍),总算憋出来了。


参考

  1. ECMAScript 6 会重蹈 ECMAScript 4 的覆辙吗?
  2. Babel 手册
  3. Babel 官网
  4. babel-preset-env: a preset that configures Babel for you
支持一下
扫一扫,支持一下
  • 微信扫一扫
  • 支付宝扫一扫