[譯] 為什麼我不再使用 export default 來匯出模組

Hopsken發表於2019-01-25

在與預設匯出(export default)死纏爛打了這麼多年後,我改變了主意。

上個星期,我發了條推特,收到了不少出人意料的回覆:

2019年,我要做的其中一件事就是不再從我的 CommonJS/ES6 模組中匯出預設值。

匯入一個預設值感覺上就像拋硬幣一樣,有一半的概率會猜錯。比如我有時就會搞不清楚匯入的到底是 class 還是 function。

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

我意識到我所遇到的大多數與 JavaScript 模組有關的問題都可以歸咎於預設匯出,於是就發了這條推特。不管我用的是 JavaScript 模組(或者 ECMAScript 模組,很多人喜歡這麼叫它)還是 CommonJS,都會深陷於預設匯出的泥潭。那條推特收到了各種各樣的評論,很多人都在問我我是如何得出這個結論的。在這篇文章中,我將盡可能地解釋我的思考歷程。

一些澄清

正如所有的推文一樣,我的推文不過是我的看法的一個縮影,而不是我完整看法的規範性參考。首先我要澄清推文裡讓人困惑的幾點:

  • 關於不知道匯出的是 function 還是 class 這一點,它只是我在使用中所遇到的諸多問題中的一個例子。這不是命名匯出為我解決的唯一的一個問題。
  • 我所遇到的問題不只出現在我自己的專案中,當引入某些第三方庫和工具模組時,也會出現這些問題。這意味著檔名的命名約定並不能解決所有問題。
  • 我並不是要所有人都放棄預設匯出。我只是說在我寫的模組中,我會選擇不去用預設匯出。當然你可以有你自己看法。

希望以上澄清可以避免後文可能產生的一些誤會。

預設匯出:最初的選擇

據我所知,預設匯出是最先從 CommonJS 流行開來的。模組可以通過如下方式匯出某個預設值:

class LinkedList {}
module.exports = LinkedList;
複製程式碼

這段程式碼匯出了 LinkedList 類,但是並沒有規定它被引用時應該使用的名稱。假設該檔名為 linked-list.js,你可以通過如下方式在其它模組中匯入它:

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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章