最近尝试用了下react-starter-kit,其中进行预编译的方式是直接使用babel-node来执行Node.js脚本,比如npm start对应的命令为:

babel-node tools/run start

这个命令本身并没有什么问题,问题在于使用babel-node的方式无法用Webstorm进行debug,作为替代,官方建议使用如下的方式来执行脚本(见How to make Webstorm 2016.2 debug work with ES6 and babel):

node -r babel-register tools/run start

然后问题就来了,在我尝试用后者来执行同样的脚本时冒出了下面的错误:

ReferenceError: regeneratorRuntime is not defined

这个错误的原因是有用到生成器的语句没有被正确polyfill,因而编译器执行时不认识这些语句了。但为什么后者会出现这样的问题呢?以及究竟要怎样做才能成功地使用Webstorm结合Babel进行debug呢?

babel-node VS babel-register

首先来解释为什么。

node -r babel-register其实相当于在执行后面指定的JS脚本之前先加载了babel-register-r等同于--require)。因此上面发生错误的命令类似于使用node执行了下面的代码:

require('babel-register');
require('./run.js');

加载了babel-register之后,所有的JS代码就会在run time时先预编译(默认根据项目中的.babelrc配置)再执行(其实现的原理是它对require函数添加了一个hook使得每次加载模块时会先预编译)。由于预编译是run time的,因此会比先预编译好所有代码在执行要效率低些,因此不建议在生产环境中使用这种方式(babel-node也同理)。

另外,babel-register可以显式指定Babel配置:

var babelConfig = require('../.babelrc.js');
require('babel-register')(babelConfig);

babel-node其实内部的实现也是用到了babel-register的,参考stackoverflow

babel-node basically calls babel-register internally. see source. The differences are

  1. when using babel-node the entry-point itself will also run through babel vs. babel-registeronly files required after babel-register is required will be run through babel.
  2. if you need babel-polyfill (for eg. generators) babel-node will pull it in automatically, vs. babel-register you’d need to do that yourself. This is something to keep in mind when building for production. If you need babel-polyfill and you are using babel-node in development, you’d need to make sure you are building w/ babel-polyfill when building for production.

所以,主要的区别在于,babel-node默认就用了babel-polyfill!这也是为什么前面使用babel-register的命令会报错的原因,并且错误恰好也是缺少了babel-polyfill导致的。

Why babel-polyfill?

这里顺便解释下为什么Babel都已经根据配置进行预编译了,却还是需要babel-polyfill。原因其实很简单,(以ES6代码预编译为ES5环境可运行为例)因为Babel只是把之前不支持的语法进行了转换,比如ES5中并没有生成器的概念,以下面的代码为例:

function* foo() {
    yield 1;
}

使用Babel设置为(当然现在建议使用babel-preset-env,更加省心一些):

{
  "presets": ["es2015"]
}

就得到了:

"use strict";

var _marked = /*#__PURE__*/regeneratorRuntime.mark(foo);

function foo() {
    return regeneratorRuntime.wrap(function foo$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    _context.next = 2;
                    return 1;

                case 2:
                case "end":
                    return _context.stop();
            }
        }
    }, _marked, this);
}

在ES5的运行环境中运行上述脚本,会发现报错了,且错误和开头的时候一样:

ReferenceError: regeneratorRuntime is not defined

也就是说babel-preset-es2015等只是对语法做了转换,而并不会管转换后用到的函数是否在指定的环境被标准库所支持,因此这里就需要babel-polyfill来将之前不支持的函数进行注入。又比如说Object.assign这个函数是ES6中新添加进标准库的一个函数,经过babel-preset-es2015转换后,Babel并没有去理这个函数,因为它并不是什么全新的语法,看上去就是一个正常的函数,而使用了babel-polyfill之后,它会对Object的原型链进行注入,添加一个assign属性的函数,从而在ES5的环境中也可以正常执行Object.assign函数了。

Why not babel-polyfill?

从上面的表述也能知道babel-polyfill会把所有之前不存在的函数通过原型链注入的方式注入到运行环境中,这样做看上去并没有什么不妥,但实际上可能会产生一些问题:

  • 它污染了标准库,如果进行库开发的话必须要考虑到这一点;
  • 因为对所有不支持的函数都进行了注入,polyfill之后的文件可能会包含一部分没用上的代码,导致文件体积较大。

但对于大部分的前端应用来说,上面两个缺点影响其实不大,所以通常就直接用babel-polyfill解决了事,很方便。

以下介绍一个可能的替代方案:babel-plugin-transform-runtime

安装好该插件并把之前的.babelrc改为:

{
  "presets": ["es2015"],
  "plugins": ["transform-runtime"]
}

上面同样的代码就转换为了:

"use strict";

var _regenerator = require("babel-runtime/regenerator");

var _regenerator2 = _interopRequireDefault(_regenerator);

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

var _marked = /*#__PURE__*/_regenerator2.default.mark(foo);

function foo() {
    return _regenerator2.default.wrap(function foo$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    _context.next = 2;
                    return 1;

                case 2:
                case "end":
                    return _context.stop();
            }
        }
    }, _marked, this);
}

可以看到之前通过注入的regeneratorRuntime现在变成了从babel-runtime/regenerator中import进来了,也就没有污染原先的全局空间。但同时一些标准库中原型链上的函数就没办法转换了,比如Array.prototype.includes函数,我在代码中创建了一个列表,它并不会转换a.includes(1)中的includes函数(毕竟用import的方式管不了原型链啊)。

Solve the problem

回到开始的那个问题,解决的方法就是引入babel-polyfill,即在.babelrc中加入这个plugin即可:

{
  "presets": ["es2015"],
  "plugins": ["module:babel-polyfill"]
}

然后就可以愉快的执行命令了:

node -r babel-register tools/run start

当然,这样用Webstorm来debug也不是事儿了。

Reference

  1. http://guoyongfeng.github.io/my-gitbook/03/toc-babel-polyfill.html
  2. Babel polyfill 知多少
  3. How are generators transpiled to ES5