[译] 为何我再也不使用 export default 来导出模块

在与默认导出(export default)死缠烂打了这么多年后,我改变了主意。javascript

上个星期,我发了条推特,收到了很多出人意料的回复:html

2019年,我要作的其中一件事就是再也不从个人 CommonJS/ES6 模块中导出默认值。前端

导入一个默认值感受上就像抛硬币同样,有一半的几率会猜错。好比我有时就会搞不清楚导入的究竟是 class 仍是 function。java

— Nicholas C. Zakas (@slicknet) January 12, 2019android

我意识到我所遇到的大多数与 JavaScript 模块有关的问题均可以归咎于默认导出,因而就发了这条推特。无论我用的是 JavaScript 模块(或者 ECMAScript 模块,不少人喜欢这么叫它)仍是 CommonJS,都会深陷于默认导出的泥潭。那条推特收到了各类各样的评论,不少人都在问我我是如何得出这个结论的。在这篇文章中,我将尽量地解释个人思考历程。ios

一些澄清

正如全部的推文同样,个人推文不过是个人见解的一个缩影,而不是我完整见解的规范性参考。首先我要澄清推文里让人困惑的几点:git

  • 关于不知道导出的是 function 仍是 class 这一点,它只是我在使用中所遇到的诸多问题中的一个例子。这不是命名导出为我解决的惟一的一个问题。
  • 我所遇到的问题不仅出如今我本身的项目中,当引入某些第三方库和工具模块时,也会出现这些问题。这意味着文件名的命名约定并不能解决全部问题。
  • 我并非要全部人都放弃默认导出。我只是说在我写的模块中,我会选择不去用默认导出。固然你能够有你本身见解。

但愿以上澄清能够避免后文可能产生的一些误会。程序员

默认导出:最初的选择

据我所知,默认导出是最早从 CommonJS 流行开来的。模块能够经过以下方式导出某个默认值:github

class LinkedList {}
module.exports = LinkedList;
复制代码

这段代码导出了 LinkedList 类,可是并无规定它被引用时应该使用的名称。假设该文件名为 linked-list.js,你能够经过以下方式在其它模块中导入它:web

const LinkedList = require("./linked-list");
复制代码

我只是碰巧把 require() 仍是返回的值命名为 LinkedList,以匹配文件名 linked-list.js,可是我也彻底能够叫它 fooMountain 或者其它随便什么名称。

默认模块导出在 CommonJS 中的流行,说明 JavaScript 模块生来就支持这种模式:

ES6 偏好单一/默认导出的风格,并且为默认导入提供了甜蜜的语法糖。

— David Herman June 19, 2014

所以,在 JavaScript 模块中,你能够经过以下方式导出默认值:

export default class LinkedList {}
复制代码

而后,你能够这样来导入它:

import LinkedList from "./linked-list.js";
复制代码

再次说明,这里的 LinkedList 这是个随意的选择(若是不是特别合理的话),并无特殊含义,也能够是 Dog 或者 symphony 诸如此类。

另外一个选择:命名导出

除了默认导出之外,CommonJS 和 JavaScript 模块都支持命名导出。在导入时,命名导出容许保留被导出的函数、类或者变量的名称。

在 CommonJS 中,你能够经过在 exports 对象上添加某对键值来建立命名导出:

exports.LinkedList = class LinkedList {};
复制代码

而后,你能够在另外一个文件中使用以下方法来导入它们:

const LinkedList = require("./linked-list").LinkedList;
复制代码

再次说明,const 以后的名字是任取的,可是为了导出时的名称一致,这里我选择使用 LinkedList

在 JavaScript 模块中,命名导出看上去像这样:

export class LinkedList {}
复制代码

而后你能够这样来导入它:

import { LinkedList } from "./linked-list.js";
复制代码

这里,LinkedList 不能够取任意的标识符,必须与命名导出使用的名称一致。对于这篇文章要讲的东西而言,这是与 CommonJS 惟一的重要区别。

因此说,这两种模块化方案都支持默认导出和命名导出。

我的偏好

在进一步深刻以前,我须要说明一下我本身在写代码时的一些我的偏好。这是我写代码的整体原则,与语言自己无关。

  1. 明了胜于晦涩。我不喜欢有秘密的代码。某个东西是干吗的,应该如何调用,诸如此类,在任何可能的状况下,都应该明确且清晰。

  2. 名称应该在全部文件中保持一致。若是某样东西在这个文件里叫 Apple,那么在另外一个文件里就不应叫 OrangeApple 永远都是 Apple

  3. 尽早并常常抛出错误。若是某样东西有可能缺失,那么最好就尽早检查它,接着,在最理想的状况下,抛出一个错误,让我知道问题在哪儿。我不想等着代码所有执行完后才发现出了问题,而后再去搜查问题出在哪儿。

  4. 更少地抉择意味着更快地开发速度。个人不少编程偏好都是为了减小编码过程当中的抉择。每作一个决定,你都会慢上一点。这就是为何代码规范能够提升开发速度的缘由。我喜欢预先决定好全部事情,而后直接放手去作。

  5. 中途打断会拖慢开发速度。当你在编码过程当中不得不停下来查找一些东西时,这就是我所说的『中途打断』。打断有时候是必要的,可是过多没必要要的打断则会拖你的后腿。我想写出尽量不须要『中途打断』的代码。

  6. 认知负荷会拖慢开发速度。简单来讲,编码时,你须要记忆的用来保证效率的细节越多,你的开发速度越慢。

对开发速度的关注对我而言是个很现实的问题。多年来,我一直为本身的健康所困扰,我能用于写代码的精力愈来愈少。任何能帮我在保证完成度的前提下,减小编码时间的操做都很关键。

我遇到的那些问题

在上述前提下,这里是我在使用默认导出时遇到的主要问题,以及为何我相信在大多数状况下命名导出都是更好的选择。

那到底是啥?

正如我在以前那条推文上说的,若是模块只有一个默认导出,我很难弄清楚我导入的是什么。若是你正在用一个不熟悉的模块或文件,你很难弄清楚返回的是什么。举个例子:

const list = require("./list");
复制代码

这里,你预想中 list 应该是什么?虽然不太多是基本类型数据,但从逻辑上讲能够是函数、类或者其它类型的对象。我怎么才能肯定呢?我须要中途打断一下。当前状况下,这可能意味着:

  • 若是我有 list.js 这个文件,我也许会打开它,看看它导出了什么。
  • 若是我没有 list.js 这个文件,那么我或许会打开某个文档。

无论是那种状况,你不得不把这段额外的信息记在脑海里,以免当你须要再次从 list.js 导入时发生打断。若是你从各类模块中引入了不少默认值,要么你的认知负荷会增长,要么你不得不中途打断屡次。二者都不理想,并且很叫人沮丧。

有人可能会说,IDE 能够解决这些问题。那么 IDE 应该足够聪明,聪明到能够弄明白正在导入的是什么,而后告诉你。固然我是支持使用聪明的 IDE 来帮助开发者的,不过我以为要求 IDE 来有效地使用语言特性是会有问题的。

名称匹配问题

命名导出要求模块的消费者至少得指定导入东西的名称。这有个好处,我能够方便地在代码库中查找全部用到 LinkedList 的地方,知道它们都指代的同一个 LinkedList。由于默认导出并不能限定导入时使用的名称,给导入命名会为每一个开发者带来更多的认知负荷。你须要决定正确的命名规范,另外,你还得确保团队中的每一个开发者对同一个事物使用相同的名称。(固然你也能够容许每一位开发者使用不一样的命名,可是这会为整个团队带来更多的认知负荷。)

使用命名导出意味着至少在它被用到的地方引用的都是定好的名称。就算你选择重命名某个导入,你也得显示说明出来,不可能在不引用规定名称的状况下实现。在 CommonJS 中:

const MyList = require("./list").LinkedList;
复制代码

在 JavaScript 模块中:

import { LinkedList as MyList } from "./list.js";
复制代码

在这两种状况下,你都得显示地声明 LinkedList 被改成 MyList

若是名称在代码库中保持一致,你就能够作到如下事情:

  1. 查找代码库,了解使用状况。
  2. 在整个代码库的范围内,重命名某个东西。

若是使用默认导出和特定命名的话,这些操做能够实现吗?我猜是能够的,可是会复杂得多,也容易出现错误。

导入错误的东西

相对于默认导出,命名导出有个明显的好处。那就是,当试图导入模块中不存在的东西时,命名导入会抛出错误。考虑如下代码:

import { LinkedList } from "./list.js";
复制代码

若是 list.js 中不存在 LinkedList,则会报错。另外,也方便像 IDE 和 ESLint1 这样的工具在代码执行以前检测不存在的引用。

糟糕的工具支持

提到 IDE,WebStorm 能够帮你书写 import 语句。2 当你在打完一个当前文件内未定义的标识符后,WebStorm 会在项目内查找模块,检查该标识符是不是某一个文件的命名导出。这时,它会作以下事情:

  1. 在缺失定义的标识符下加上下划线,显示能够修复这个问题的 import 语句。
  2. 根据你打出的标识符,自动导入正确的 import 语句(若是打开了自动导入功能)。事实上,当使用命名导入时,WebStorm 能够帮上不少忙。

Visual Studio Code3 有一个插件能够实现相似的功能。这种功能没法经过默认导出实现,由于你想导入的东西没有肯定的名称。

结论

当我在项目中使用默认导出时,我遇到严重的工做效率问题。然而这些问题并非无解的,使用命名导出和导入能够更好地配合个人编程习惯。清晰明确的代码和对工具的重度依赖使我成为高效的程序员。只要命名导出能够帮我作到这些,在可预见的将来内,我都会支持它们。固然,我没法决定我用的第三方模块如何导出,但我能够控制我本身写的模块如何导出,我会选择命名导出。

正如前文说的,得提醒一下,这只是我我的的见解,你也许以为个人论证没有足够的说服力。这篇文章并非想劝阻任何使用默认导出,而是做为对那些询问我为何中止使用默认导出的一个更好的回答。

References

  1. esling-plugin-import import/named rule

  2. WebStorm: Auto Import in JavaScript

  3. Visual Studio Extension: Auto Import

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索