记一次 ES6 modules export default bug

一个错误的export写法

最近发现以前写的代码中有一个bug:
我在export的时候这样写的:

1
2
3
4
import SimpleHeader from ‘./simple-header'
export default {
SimpleHeader
}

然后这样 import:

1
2
import { SimpleHeader } from ‘./header'
console.log(SimpleHeader)

其实这样写是错的,因为ES6的import并不是对象解构语法,只是看起来比较像,可以参考MDN对import的描述 MDN import。所以import并不能解构一个default对象。

由于当时写代码的时候也被这个语法迷惑,导致以为是解构语法,所以直接导出了一个默认的对象。但是为什么我没有发现错误呢而且编译正常呢?这是非常让人费解的。

webpack 和 babel 的编译结果

于是我决定从编译后的代码着手,对webpack来说,从 webpack2 开始一般都不启用 babel 的modules,而是让webpack进行模块的编译,因为只有这样才能用到webpack的 tree shaking 优化。

但是我的老项目中其实并没有关掉 babel 的modules,所以其实 ES6的模块语法是先被babel编译成了 nodejs 的语法,然后又被webpack进行了一次编译和打包。这里对比下启用 babel modules 和关闭babel modules 的结果:

启用 Babel modules 的编译结果:

如果是:

1
2
import {SimpleHeader} from '../components/header'
console.log(SimpleHeader)

编译出来是:

1
2
3
4
5
6
7
8
var _header = __webpack_require__(6);
console.log(_header.SimpleHeader);
```

如果是:
```js
import Header from '../components/header'
console.log(Header)

编译出来是:

1
2
3
4
var _header = __webpack_require__(6);
var _header2 = _interopRequireDefault(_header);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_header2.default);

不启用babel modules中的编译结果:

如果是:

1
2
import Page from './page'
console.log(Page)

编译出来是:

1
2
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__page__ = __webpack_require__(34)
console.log(__WEBPACK_IMPORTED_MODULE_0__page__["a" /* default */]);

如果是:

1
2
import { Header } from './page'
console.log(Header)

编译出来是:

1
2
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__page__ = __webpack_require__(34);
console.log(__WEBPACK_IMPORTED_MODULE_0__page__["Header"]);

可以看到 webpack 和 babel 对modules的编译其实是有一些区别的:

  • babel 会把default 放到 exports.default 上,而 webpack 会把 default 放到 exports.a 上。
  • babel 有一个 _interopRequireDefault 方法,会判断 __esModule 标记而决定如何导出 default

出错原因

我们回到错误的代码,export 被编译后变成这样:

1
2
3
4
5
6
7
8
9
Object.defineProperty(exports, "__esModule", {
value: true
});
var _header = __webpack_require__(1);
var _header2 = _interopRequireDefault(_header);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = {
Header: _header2
};

而import代码被编译成这样:

1
2
var _header = __webpack_require__(4);
console.log(_header.SimpleHeader);

这里其实 _header 是这样的:

1
2
3
{
default: { // header is here}
}

因此 _header.SimpleHeader 是不存在的, 应该是 _header.default.SimpleHeader 才对。

那么为什么在很久以前是可以正常运行的呢?我找到了 Babel 的这个Issue Kill CommonJS default export behaviour。Babel5 是支持export 一个对象,但是到 Babel6 的时候去掉了这个特性。因为我的老项目中用的Babel比较久,所以可以正常工作,而现在重新引用的时候就会发现编译报错了。

Babel5 编译后 var _header = __webpack_require__(4) 中的 _header 就是 Header 对象,而不是被放在 _header.default,所以 _header.SimpleHeader 代码就对了。

这个Bug只有在使用 Babel5 并且启用了 Babel modules 时候才能重现出来。如果有人就想用不规范的export语法怎么办,这里有一个插件可以支持: https://github.com/59naga/babel-plugin-add-module-exports

当然除非是为了兼容老代码,否则强烈按ES6的规范,老老实实用 export const 之类的标准语法来导出。