- 原文地址:Why I've stopped exporting defaults from my JavaScript modules
- 原文作者:Nicholas C. Zakas
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Hopsken
- 校對者:Fengziyin1234,SHERlocked93
在與預設匯出(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
,但是我也完全可以叫它 foo
、Mountain
或者其它隨便什麼名稱。
預設模組匯出在 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 唯一的重要區別。
所以說,這兩種模組化方案都支援預設匯出和命名匯出。
個人偏好
在進一步深入之前,我需要說明一下我自己在寫程式碼時的一些個人偏好。這是我寫程式碼的總體原則,與語言本身無關。
-
明瞭勝於晦澀。我不喜歡有祕密的程式碼。某個東西是幹嘛的,應該如何呼叫,諸如此類,在任何可能的情況下,都應該明確且清晰。
-
名稱應該在所有檔案中保持一致。如果某樣東西在這個檔案裡叫
Apple
,那麼在另一個檔案裡就不該叫Orange
。Apple
永遠都是Apple
。 -
儘早並經常丟擲錯誤。如果某樣東西有可能缺失,那麼最好就儘早檢查它,接著,在最理想的情況下,丟擲一個錯誤,讓我知道問題在哪兒。我不想等著程式碼全部執行完後才發現出了問題,然後再去搜查問題出在哪兒。
-
更少地抉擇意味著更快地開發速度。我的很多程式設計偏好都是為了減少編碼過程中的抉擇。每做一個決定,你都會慢上一點。這就是為什麼程式碼規範可以提高開發速度的原因。我喜歡預先決定好所有事情,然後直接放手去做。
-
中途打斷會拖慢開發速度。當你在編碼過程中不得不停下來查詢一些東西時,這就是我所說的『中途打斷』。打斷有時候是必要的,但是過多不必要的打斷則會拖你的後腿。我想寫出儘可能不需要『中途打斷』的程式碼。
-
認知負荷會拖慢開發速度。簡單來說,編碼時,你需要記憶的用來保證效率的細節越多,你的開發速度越慢。
對開發速度的關注對我而言是個很現實的問題。多年來,我一直為自己的健康所困擾,我能用於寫程式碼的精力越來越少。任何能幫我在保證完成度的前提下,減少編碼時間的操作都很關鍵。
我遇到的那些問題
在上述前提下,這裡是我在使用預設匯出時遇到的主要問題,以及為什麼我相信在大多數情況下命名匯出都是更好的選擇。
那究竟是啥?
正如我在之前那條推文上說的,如果模組只有一個預設匯出,我很難弄清楚我匯入的是什麼。如果你正在用一個不熟悉的模組或檔案,你很難弄清楚返回的是什麼。舉個例子:
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
。
如果名稱在程式碼庫中保持一致,你就可以做到以下事情:
- 查詢程式碼庫,瞭解使用情況。
- 在整個程式碼庫的範圍內,重新命名某個東西。
如果使用預設匯出和特定命名的話,這些操作可以實現嗎?我猜是可以的,但是會複雜得多,也容易出現錯誤。
匯入錯誤的東西
相對於預設匯出,命名匯出有個明顯的好處。那就是,當試圖匯入模組中不存在的東西時,命名匯入會丟擲錯誤。考慮以下程式碼:
import { LinkedList } from "./list.js";
複製程式碼
如果 list.js
中不存在 LinkedList
,則會報錯。另外,也方便像 IDE 和 ESLint1 這樣的工具在程式碼執行之前檢測不存在的引用。
糟糕的工具支援
提到 IDE,WebStorm 可以幫你書寫 import
語句。2 當你在打完一個當前檔案內未定義的識別符號後,WebStorm 會在專案內查詢模組,檢查該識別符號是否是某一個檔案的命名匯出。這時,它會做如下事情:
- 在缺失定義的識別符號下加上下劃線,顯示可以修復這個問題的
import
語句。 - 根據你打出的識別符號,自動匯入正確的
import
語句(如果開啟了自動匯入功能)。事實上,當使用命名匯入時,WebStorm 可以幫上很多忙。
Visual Studio Code3 有一個外掛可以實現類似的功能。這種功能無法通過預設匯出實現,因為你想匯入的東西沒有確定的名稱。
結論
當我在專案中使用預設匯出時,我遇到嚴重的工作效率問題。然而這些問題並不是無解的,使用命名匯出和匯入可以更好地配合我的程式設計習慣。清晰明確的程式碼和對工具的重度依賴使我成為高效的程式設計師。只要命名匯出可以幫我做到這些,在可預見的未來內,我都會支援它們。當然,我無法決定我用的第三方模組如何匯出,但我可以控制我自己寫的模組如何匯出,我會選擇命名匯出。
正如前文說的,得提醒一下,這只是我個人的看法,你也許覺得我的論證沒有足夠的說服力。這篇文章並不是想勸阻任何使用預設匯出,而是作為對那些詢問我為什麼停止使用預設匯出的一個更好的回答。
References
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。