- 原文地址:開發模式的工做原理是什麼?
- 原文做者:Dan Abramov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Jerry-FD
- 校對者:TokenJan、hanxiaosss
若是你的 JavaScript 代碼庫已經有些複雜了,你可能須要一個解決方案來針對線上和開發環境區分打包和運行不一樣代碼。html
針對開發環境和線上環境,來區分打包和運行不一樣的代碼很是有用。在開發模式中,React 會包含不少告警來幫助你及時發現問題,而不至於形成線上 bug。然而,這些幫助發現問題的必要代碼,每每會形成代碼包大小增長以及應用運行變慢。前端
這種降速在開發環境下是能夠接受的。事實上,在開發環境下運行代碼的速度更慢可能更有幫助,由於這能夠必定程度上消除高性能的開發機器與平均速度的用戶設備而帶來的差別。vue
在線上環境咱們不想要任何的性能損耗。所以,咱們在線上環境刪除了這些校驗。那麼它的工做原理是什麼?讓咱們來康康。node
想要在開發環境運行下不一樣代碼關鍵在於你的 JavaScript 構建工具(不管你用的是哪個)。在 Facebook 中它長這個樣子:react
if (__DEV__) {
doSomethingDev();
} else {
doSomethingProd();
}
複製代碼
在這裏,__DEV__
不是一個真正的變量。當瀏覽器把模塊之間的依賴加載完畢的時候,它會被替換成常量。結果是這個樣子:android
// 在開發環境下:
if (true) {
doSomethingDev(); // 👈
} else {
doSomethingProd();
}
// 在線上環境:
if (false) {
doSomethingDev();
} else {
doSomethingProd(); // 👈
}
複製代碼
在線上環境,你可能會在代碼中會啓用壓縮工具(好比, terser)。大多 JavaScript 壓縮工具會針對無效代碼作一些限制,好比刪除 if (false)
的邏輯分支。因此在線上環境中,你可能只會看到:webpack
// 在線上環境(壓縮後):
doSomethingProd();
複製代碼
(注意,針對目前主流的 JavaScript 工具備一些重要的規範,這些規範能夠指導怎樣纔能有效的移除無效代碼,但這是另外一個的話題了。)ios
可能你使用的不是 __DEV__
這個神奇的變量,若是你是用的是流行的 JavaScript 打包工具,好比 webpack,那麼這有一些你須要遵照的約定。好比,像這樣的一種很是常見的表達式:git
if (process.env.NODE_ENV !== 'production') {
doSomethingDev();
} else {
doSomethingProd();
}
複製代碼
一些框架好比 React 和 Vue 就是使用的這種形式。當你使用 npm 來打包載入它們的時候。 (單個的 <script>
標籤會提供開發和線上版本的獨立文件,而且使用 .js
和 .min.js
的結尾來做爲區分。)github
這個特殊的約定最先來自於 Node.js。在 Node.js 中,會有一個全局的 process
變量用來表明你當前系統的環境變量,它屬於 process.env
object 的一個屬性。然而,若是你在前端的代碼庫裏看到這種語法,實際上是並不存在真正的 process
變量的。🤯
取而代之的是,整個 process.env.NODE_ENV
表達式在打包的時候會被替換成一個字面量的字符串,就像神奇的 __DEV__
變量同樣:
// 在開發環境中:
if ('development' !== 'production') { // true
doSomethingDev(); // 👈
} else {
doSomethingProd();
}
// 在線上環境中:
if ('production' !== 'production') { // false
doSomethingDev();
} else {
doSomethingProd(); // 👈
}
複製代碼
由於整個表達式是常量('production' !== 'production'
恆爲 false
)打包壓縮工具也能夠藉此刪除其餘的邏輯分支代碼。
// 在線上環境(打包壓縮後):
doSomethingProd();
複製代碼
惡做劇到此結束~
注意這個特性若是面對更復雜的表達式將不會工做:
let mode = 'production';
if (mode !== 'production') {
// 🔴 不能保證會被移除
}
複製代碼
JavaScript 靜態分析工具不是特別智能,這是由於語言的動態特性所決定的。當它們發現像 mode
這樣的變量,而不是像 false
或者 'production' !== 'production'
這樣的靜態表達式時,它們大機率會失效。
相似地,在 JavaScript 中若是你使用頂層的 import
聲明,自動移除無用代碼的邏輯會由於不能跨越模塊邊界而沒法生效。
// 🔴 不能保證會被移除
import {someFunc} from 'some-module';
if (false) {
someFunc();
}
複製代碼
因此你的代碼須要寫的很是嚴格,來確保條件的絕對靜態,而且確保全部你想要移除的代碼都包含在條件內部。
爲了保證一切按計劃運行,你的打包工具須要替換 process.env.NODE_ENV
,並且它須要知道你想要在哪一種模式下構建項目。
在幾年前,忘記配置環境變量很是常見。你會常常發如今開發模式下的項目被部署到了線上。
那很糟糕,由於這會使網站加載運行的速度很慢。
在過去的兩年裏,這種狀況有了顯著的改善。例如,webpack 增長了一個簡單的 mode
選項,替換了原先手動更改 process.env.NODE_ENV
。 React DevTools 如今也會針對開發模式下的站點展現一個紅色的 icon,來使得它容易被察覺。
一些會幫你作預設置的安裝工具好比 Create React App、Next/Nuxt、Vue CLI、Gatsby 等等,會把開發和線上構建分紅兩個獨立的命令,來使得犯錯的概率更小。(例如,npm start
和 npm run build
。)也就是說,只有線上的構建代碼才能被部署,因此開發者不再可能犯這種錯誤了。
一直有一個在討論的點是,把線上模式置爲默認,開發模式變爲可選項。我的來講,我認爲這樣作不是很好。從開發模式的警告中受益的人大可能是剛剛接觸這個框架的開發者。 他們不會意識到要打開開發模式的開關,這樣就會錯過不少應該被警告提早發現的 bug。
是的,性能問題很是糟糕,但充斥着 bug 的用戶體驗也是同樣。例如,React key 警告 幫助防止發生像發錯了消息或者買錯了產品這樣的 bug。若是在開發中禁用這個警告,對你和你的用戶來講都是很是冒險的。由於若是它默認是關閉狀態,而以後你發現了這個開關並把它打開了,你會發現有太多的警告須要清理。因此大多數人會再把它關上。因此這就是爲何它須要在開始時候就是打開狀態,而不是以後才讓它生效的緣由。
最後,就算在開發中這些警告是可選項,而且開發者們也知道須要在開發的早期就把它們打開,咱們仍是要回到最開始的問題。仍是會有一些開發者不當心把他們部署到線上環境中!
咱們回到這一點來。
我的認爲,我堅信工具展現和使用的正確模式取決於你是在調試仍是在部署。幾乎全部其餘環境(不管是手機、桌面仍是服務端)除了頁面瀏覽器以外都已經有區分和加載不一樣的開發和線上環境的方法存在長達數十年了。
不能僅依靠框架提出或者依賴臨時公約,可能 JavaScript 的環境是時候把這種區別做爲一個很重要的需求來看待了。
大道理已經夠了!
讓咱們再來看一眼代碼:
if (process.env.NODE_ENV !== 'production') {
doSomethingDev();
} else {
doSomethingProd();
}
複製代碼
你可能想知道:若是在前端代碼中不存在 process
對象,爲何像 React 和 Vue 這樣的框架會在 npm 包中依賴它?
(再次聲明:用 <script>
標籤可使用 React 和 Vue 提供的方式把它們加載到瀏覽器中,這不會依賴 process。取而代之的是,你必需要手動選擇,在開發模式下的 .js
仍是線上環境中的 .min.js
文件。下面的部分只是關於使用打包工具把 React 或者 Vue 從 npm 中 import
進來而使用它們。)
像編程中的不少問題同樣,這種特殊的約定大可能是歷史緣由。咱們還在使用它的緣由是由於,它如今已經被不少其餘的工具所接受並適應了。換成其餘的會有很大的代價,而且不是特別值得這麼作。
因此背後的歷史緣由到底是什麼?
在 import
和 export
的語法被標準化的不少年前,有不少方式來表達模塊之間的關係。好比 Node.js 中所受歡迎的 require()
和 module.exports
,也就是著名的 CommonJS。
在 npm 上註冊發佈的代碼早期多數是針對 Node.js 寫的 Express 曾是(可能如今仍是?)最受歡迎的服務端 Node.js 框架,它使用 NODE_ENV
這個環境變量 來使線上模式生效。 一些其餘的 npm 包也採用了一樣的約定。
早期的 JavaScript 打包工具好比 browserify 想要在前端工程中使用 npm 中的代碼。(是的,那時候 在前端中幾乎沒人使用 npm!你能夠想象嗎?)因此它們拓展了當時在 Node.js 生態系統中的約定,將之應用於前端代碼中。
最初的 「envify」 變革是在 2013 正式版。React 就是在差很少那個時候開源的,而且在那個時代 npm 和 browserify 看起來是是打包前端 CommonJS 代碼的最佳解決方案。
React 在很早的時候就提供 npm 版本(還有 <script>
標籤版本)。隨着 React 變得流行起來,使用 CommonJS 模塊來寫 JavaScript 的模塊化代碼、並使用 npm 來管理髮布代碼也變成了最佳實踐。
React 須要在線上環境移除只應該出如今開發模式中的代碼。恰好 Browserify 已經針對這個問題提供瞭解決方案,因此 React 針對 npm 版本也接受了使用 process.env.NODE_ENV
的這個約定,隨着時間的流逝,一些其餘的工具和框架,包括 webpack 和 Vue,也採起了相同的措施。
到了 2019 年時,browserify 已經失去了很大一部分的市場佔有率。然而,在構建的階段把 process.env.NODE_ENV
替換成 'development'
或者 'production'
的這項約定,卻一如既往的流行。
(一樣有趣的是,瞭解 ES 模塊的方式是如何一步步發展成做爲線上的分發引用模式,而不只僅只是在開發時使用的發展歷史,它是如何慢慢改變天平的?在 Twitter 上告訴我)
另外一件你可能會感到迷惑的事是,在 GitHub 上 React 源碼中,你會看到 __DEV__
被做爲一個神奇的變量來使用。可是在 npm 上的 React 代碼裏,使用的倒是 process.env.NODE_ENV
。這是怎麼作到的?
從歷史上說,咱們在源碼中使用 __DEV__
來匹配 Facebook 的源碼。在很長一段時間裏,React 被直接複製進 Facebook 的代碼倉庫裏,因此它須要遵照相同的規則。對於 npm 的代碼,咱們有一個構建階段,在發佈代碼以前會檢查並使用 process.env.NODE_ENV !== 'production'
來字面地替換 __DEV__
。
這有時會有一個問題。某些時候,遵循 Node.js 約定的代碼在 npm 上運行的很好,可是會破壞 Facebook,反之亦然。
從 React 16 起,咱們改變了這種方式。取而代之,如今咱們會針對每個環境編譯一個包(包括 <script>
標籤、npm 和 Facebook 內部的代碼倉庫)。因此甚至是 npm 的 CommonJS 代碼也被提早編譯成獨立的開發和線上包。
這意味着當 React 源碼中出現 if (__DEV__)
的時候,事實上咱們會對每個包產出兩個代碼塊。一個被預編譯爲 __DEV__ = true
另外一個是 __DEV__ = false
。每個 npm 包的入口來「決定」該導出哪個。
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
複製代碼
這是你的打包工具把 'development'
或者 'production'
替換爲字符串的惟一地方。也是你的壓縮工具除去只應在開發環境中 require
代碼的惟一地方。
react.production.min.js
和 react.development.js
再也不有任何 process.env.NODE_ENV
檢查了。這頗有意義,由於當代碼真正運行在 Node.js 中的時候, 訪問 process.env
有可能會很慢。提早編譯兩個模式下的代碼包也能夠幫助咱們優化文件的大小變得更加一致,不管你使用的是哪一個打包壓縮工具。
這就是它的工做原理!
我但願有一個更好的方法而不是依賴約定,可是咱們已經到這了。若是在全部的 JavaScript 環境中,模式是一個很是重要的概念,而且若是有什麼方法可以在瀏覽器層面來展現這些本不應出現的運行在開發環境下的代碼,那就很是棒了。
另外一方面,在單個項目中的約定能夠傳播到整個生態系統,這點很是神奇。2010年 EXPRESS_ENV
變成了 NODE_ENV
並在 2013 年蔓延到前端。可能這個解決方案並不完美,可是對每個項目來講,接受它的成本遠比說服其餘每個人去作一些改變的成本要低得多。這教會了咱們寶貴的一課,關於自上而下與自下而上的方案接受。理解了相比於那些失敗的標準來講它是如何一步步地轉變成功的標準的。
隔離開發和線上模式是一個很是有用的技術。我建議你在你的庫和應用中使用這項技術,來作一些在線上環境很重,可是在開發環境中卻很是有用(一般是嚴格的)的校驗和檢查。
和任何功能強大的特性同樣,有些狀況下你可能也會濫用它。這是我下一篇文章的話題!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。