详解默认导出(export default)二三事

16,279 阅读4分钟

近期在做项目的时候,发现了自己的对默认导出的理解有偏差。首先,大家看看下面这段代码问题出现在哪里?

// test.js
const Test = {
  setName() => {
    console.log("setName run");
  },
  getName() => {
    console.log("getName run");
  }
}
export default Test;

// index.js
import { setName, getName } from './test';
setName();
getName();

我一直以来都是这么写的,没觉得有啥问题,代码也都是可以正常运行的,直到这次报错——Uncaught TypeError: setName is not a function。调试了一下发现,babel后的代码大概如下:

"use strict";

// _test = {default: {setName: xxx, getName: xxx}}
var _test = require("./test");

(0, _test.setName)();
(0, _test.getName)();

嗯?_test怎么多了一个default,导致我拿不到相应的方法了?我之前的代码怎么没有这个问题? 带着这些问题,我们进入今天的正文。

1. 默认导出的起源

大家都知道主流的模块规范有ES6 Module, CommonJS和UMD(之前还曾流行过AMD和CMD)。而默认导出就来自于CommonJS。在CommonJS的规范中,我们经常写这样的代码,

class Test {}
module.exports = Test;

规范变为ES6 Module之后,相应的代码就变成了

class Test {}
export default Test;

这种写法很方便,我们不用过多的关心我们要导出什么内容,只需要默认导出一次就好,因此,被广泛的应用在各处代码之中。但是这样的写法并不是一个银弹,会导致一些问题,下面我们就来盘点一下。

2. 默认导出的问题和起因

2.1 babel@5和babel@6的兼容性问题

先回到本文一开始的这种写法,看起来像是对象解构的语法,但是因为是和import/export一起使用的,所以这边其实是命名导出(named export)的语法,而命名导出的语法如下:

// test.js 
export let c1 = '我是c1'
export let c2 = '我是c2'
export default {
  c3'我是c3'
};

// index.js
import c from './test';
import { c1, c2 } from './test';
// c: { c3: '我是c3' }
// c1: 我是c1
// c2: 我是c2
console.log(c, c1, c2);

发现问题了吗?这种看起来的语法一致,导致了一直以来很多的错误使用。那为什么这种错误的使用法方式可以被正常执行呢?因为babel@5支持了这种错误的写法,帮忙做了抹平。所以如果你用babel进行转码,那么即使你是这么写的import { c3 } from './test';,也能正常输出,尽管它本应该输出的是undefined

但是这个特性会导致很多的混淆,所以在babel@6中已经干掉了这个特性,这就导致了兼容性问题。如果还想让babel继续有这种特性,可以使用babel-plugin-add-module-exports库。

2.2 tree shaking问题

即使用babel做转码,我们也不建议使用这种写法,原因是babel的处理方式其实是把整个代码转换为CommonJS——这样就导致我们把整个文件到倒入到import之中了,不利于webpack打包的时候进行静态分析,也就用不上ES6 Module只加载有用部分的特性。

2.3 其他打包工具不兼容

如rollup等,如果迁移了别的打包工具,会出现本来没有的很多问题。

3. 最佳实践

说了这么多,那么应该如何写呢?

3.1 改为命名导出(named export)

默认导出一个对象是一个深坑,要坚决避免,改为命名导出,例如上文的例子:

// test.js
export const Test = {
  setName() => {
    console.log("setName run");
  },
  getName() => {
    console.log("getName run");
  }
}

// index.js
import { Test } from './test';
const { setName, getName } = Test;
setName();
getName();

3.2 使用5to6-codemod

如果有多个项目,都使用了默认导出,改不动,可以使用这个包进行统一转换,它的转换规则如下:

// test.js
export default { a, b, c}
// test.js转换后的格式
const exported = { a, b, c}
export default exported;
export const { a, b, c} = exported;

这样就同时支持了错误和正确的写法。

4. 总结

本文阐述了默认导出的起源和使用它可能导致的坑,并给出了相应的解决方案,希望能对大家有所帮助。

参考文献:

深入解析ES Module(一):禁用export default object

深入解析ES Module(二):彻底禁用default export