[譯] 爲何我再也不使用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索