export default 为何突然没用了?

前言

前几天团队小伙伴问我一个问题,我在ts文件中使用解构导出了一个变量,在其他地方 import 进来发现是 undefined,类似这样

//a.ts
export const a = {
   a1: 1,
   a2: 2
}

export const b = {
    b1: 1
}

export default {
 ...a,
 b
}
// b.ts
import { a1 } from 'a';
console.log(a1): // undefined

这里抛出一个疑问?

明明使用了 babel-plugin-add-module-exports 兼容了 export default,但是就是取不到?

接下来我们从 export defalut -> babel -> add-module-exports来逐步的了解下为什么

export default 作用是什么

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。

类似这样
导出函数

//a.js
export default funcion() {
   //xxx
}
//b.js
import foo from 'a';

导出对象

//c.js
const c = { c1:1, c2:2 }
const d = { d1:1, d2:2 }
export default {
    c,d
}
//d.js
import obj from 'c'
console.log(obj); // {c:{c1:1,c2:2},d:{d1:1,d2:2}}

导出default

//a.js
function foo(){}
export { foo as default};
// 等同于
// export default foo;

// b.js
import { default as foo } from 'a';
// 等同于
// import foo from 'a';

到这里看起来一切很美好,有一个新问题:在d.js里,我想直接拿到 obj 里的 c 属性,可以吗?

const c = { c1:1, c2:2 }
const d = { d1:1, d2:2 }
export default {
    c,d
}
//d.mjs
import {c} from 'c'
console.log(c); // 报错了

// terminal
node --experimental-modules d.js
/*
import {c} from 'c';
        ^
SyntaxError: The requested module 'c' does not provide an export named 'c'
*/

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

既然import不支持解构一个default对象,那么我们心中又有一个疑问,为什么我们在项目中能够随意的去写 export default, 并且通过解构可以取的到呢?

export default 编译结果

export default 属于 ES6 的语法,为了兼容不支持 ES6 的浏览器,所以需要 babel 编译。接下来我们看看经过babel编译之后,export default变成了什么。

babel 5 时代

在使用babel5的时候,下面代码

//a.js
const a = {};
const b = {};
export default {a,b}
//b.js
import {a} from 'b'
console.log(a)

会被打包为

//a.js
...
let _default = _objectSpread({}, {a, b});
exports.default = _default;
module.exports = exports.default;

//b.js
"use strict";
var _const = require("./a");
console.log(_const.a);

babel 把 esm 解析成了cjs,我们执行 b.js,发现可以取到值,但是在浏览器环境require语法,我们还需要webpack,因为webpack 简单来说是对babel转换后的文件做了一层 require 的包装,所以这里具体不谈webpack做了什么,只讨论babel, webpack具体做了什么可以戳这里查看

webpack启动代码解读
webpack模块化原理

babel 6 时代

项目升级babel 6 之后,发现之前写法取不到值了,上面的 a.js 和 b.js 打包后变为

//a.js
...
let _default = _objectSpread({}, {a, b});
exports.default = _default;
// babel6 去掉了 module.exports = exports.default;

//b.js
"use strict";
var _const = require("./a");
console.log(_const.a);

这个时候 _const 的值为 {default: {a:{},b{}}}
出现这个的原因是因为 Babel 的这个Issue Kill CommonJS default export behaviour,所以 Babel 6通过不再执行module.exports = exports['default']模块转换来更改某些行为。Babel5 是支持export 一个对象,但是到 Babel6 的时候去掉了这个特性。这个时候我们为了兼容老代码,需要一个解决方案,这个时候 babel-plugin-add-module-exports 入场了。

babel-plugin-add-module-exports 入场

babel-plugin-add-module-exports 主要作用是补全 Babel6 去掉的 module.exports = exports.default;
问题来了,项目中配置了babel-plugin-add-module-exports为什么前沿中的代码会有问题呢

babel-plugin-add-module-exports 失效原因

答案很简单,我们发现 babel-plugin-add-module-exports 失效了,深入源码打个log发现会判断是否有 name export,如果有 name export,就不会补上 babel5 export default object的特性。

// hasExportNamed 一直是 true
...
if (hasExportDefault &&  !hasExportNamed) {

path.pushContainer('body', \[types.expressionStatement(types.assignmentExpression('=', types.memberExpression(types.identifier('module'), types.identifier('exports')), types.memberExpression(types.identifier('exports'), types.stringLiteral('default'), true)))\]);

}
...

解决方案:
我们只需要改动代码为

//a.ts
const a = {
   a1: 1,
   a2: 2
}
const b = {
    b1: 1
}
export default {
 ...a,
 b
}
// b.ts
import { a1 } from 'a';
console.log(a1): // 1

我们只需要去掉 export const, 只保留 export default 即可解决这个问题。

有好奇的同学问,为什么有了name export,就不会去补全module.exports = exports.default。看到下面的例子你就明白了

//a.ts
export const a = {
   a1: 1,
   a2: 2
}
const b = {
    b1: 1
}
export default {
 b
}
// b.ts
import { a } from 'a';
console.log(a);

打包后手动加一个 module.exports = exports.default

//a.js
...
let _default = _objectSpread({}, {b});
exports.default = _default;
module.exports = exports.default;

结果可想而知,在b文件require进来的时候,a 找不到了

//b.js
"use strict";
var _const = require("./a");
console.log(_const.a); // undefined

结语

export default配合babel-plugin-add-module-exports给了我们很好的开发体验,但是还是要遵守export default 的基本规则:
虽然es6 export default 导出的内容有工具帮你处理,但是 es6 import 不是解构语法。需要注意的是,在引入一个有默认输出的模块时,这时import命令后面,不使用大括号,不要对引入的内容进行解构。

帮助链接:

关于 import、require、export、module.exports

最担心的事不是写出ipcode,而是从不开始

229 声望
7 粉丝
0 条评论
推荐阅读
webpack 流程解析 (5) module reslove
上文说道我们拿到了构建modlule的factory,和依赖等关键数据,通过addModuleTree经过factorizeQueue的控制走到了factory.create。这个时候就开始了reslove过程。本文主要分析,NormalModuleFactory 内部 beforeRe...

csywweb2阅读 2.2k

安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城31阅读 7.2k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco20阅读 2.1k评论 2

在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
【已结束】SegmentFault 思否写作挑战赛!
SegmentFault 思否写作挑战赛 是思否社区新上线的系列社区活动在 2 月 8 日 正式面向社区所有用户开启;挑战赛中包含多个可供作者选择的热门技术方向,根据挑战难度分为多个等级,快来参与挑战,向更好的自己前进!

SegmentFault思否20阅读 5.6k评论 10

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.7k评论 3

封面图
你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan21阅读 1.6k评论 1

封面图

最担心的事不是写出ipcode,而是从不开始

229 声望
7 粉丝
宣传栏