近期在做项目的时候,发现了自己的对默认导出的理解有偏差。首先,大家看看下面这段代码问题出现在哪里?
// 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. 总结
本文阐述了默认导出的起源和使用它可能导致的坑,并给出了相应的解决方案,希望能对大家有所帮助。