[譯]JavaScript中的development模式怎麼實現

#[譯]JavaScript中的development模式怎麼實現javascript

原文連接overreacted.io/how-does-th…html

譯註: 忽略了一些內容,仍是強烈建議閱讀下原文吧。前端

隨着你的 JavaScript 應用愈來愈複雜,你極可能會在 developmentproduction 模式下,分別加載和執行不一樣的代碼邏輯。vue

可以在 developmentproduction 模式下,分別打包或執行不一樣的代碼,是一種很是強大的能力。在 development 模式下,React 會給出關於你代碼的一些警告,一般狀況下,這些代碼極可能會致使bug。可是,去提早監測並給出警告的這些代碼,也增大了打包以後的JS文件大小,甚至會下降應用的性能。java

在開發階段,咱們能夠接受由於額外的監測代碼而致使的應用性能損耗。事實上,在開發中,代碼運行比production模式更加緩慢,甚至可能帶來一些好處,這讓開發者能體會到在一些性能低下的設備上,咱們應用的運行效果,正如你知道的,一般開發者的機器性能是優於大部分的用戶設備。node

production 模式下,咱們不能忍受這些額外的監測代碼帶來的性能損耗。所以,在 production 模式下, React 不會包含這些監測代碼。下面讓咱們來看看具體是怎麼作到的。react


實現 developmentproduction 模式執行不一樣的代碼,這依賴於你的JS代碼編譯流程(固然前提是你有JS編譯流程……)。在Facebook,咱們大概這樣寫代碼:webpack

if (__DEV__) {
  doSomethingDev();
} else {
  doSomethingProd();
}
複製代碼

上面代碼裏,__DEV__ 不是一個真實存在的變量。它在JS代碼編譯階段,會被一個常量來替換,一般在 development 下是 true,在 production 模式下是 false。在不一樣模式下,編譯出來的最終代碼長這樣:git

// In development:
if (true) {
  doSomethingDev(); // 👈
} else {
  doSomethingProd();
}

// In production:
if (false) {
  doSomethingDev();
} else {
  doSomethingProd(); // 👈
}
複製代碼

production 模式下,你一般會使用一些壓縮工具(好比 terser )來壓縮JS代碼。同時,大多數的JS代碼壓縮工具,都會進行一些 死碼消除 ,好比在生成的代碼中,刪除掉 if(false){} 這樣的代碼分支。 所以,在 production 模式下,通過壓縮以後,最終產出的代碼長這樣:github

// In production (after minification):
doSomethingProd();
複製代碼

你在實際項目中,用到的可能並非 __DEV__ 這個標記常量,若是你用的是 webpack 這樣的JS編譯打包工具,那一般用的是另一種常量標記方式,來實現這個功能。好比,在 webpack 社區中,一般是這樣來區分 developmentproduction 的代碼分支:

if (process.env.NODE_ENV !== 'production') {
  doSomethingDev();
} else {
  doSomethingProd();
}
複製代碼

當你使用一些打包工具(好比webpack)來從 npm包的引入 ReactVue 這樣的類庫時,這些類庫裏區分 developmentproduction 的方式和上面這段代碼是同樣的(譯註: 經過 process.env.NODE_ENV 來區分不一樣模式,應該是 web前端開發中的一種約定了吧)。若是你是經過 <script>標籤的方式,來直接加載已經提早編譯好的版本,那麼一般是以JS代碼的文件後綴 .js.min.js 來區分 developmentproduction 模式的代碼。

經過 process.env.NODE_ENV 標記來區分 developmentproduction環境的約定,最初是來自於 Node.js。在 Node.js 中,有一個全局變量 process,而且能夠經過 process.env 這個對象來訪問到代碼執行時候的環境變量。可是,在前端代碼(譯註:指運行在瀏覽器裏的那種JS代碼)裏,並不存在全局的 process 變量🤯。

實際上,相似咱們前面提到的 __DEV__process.env.NODE_ENV 也是在代碼編譯階段會被 development 或者 production 常量替換掉。替換以後的代碼以下:

// In development:
if ('development' !== 'production') { // true
  doSomethingDev(); // 👈
} else {
  doSomethingProd();
}

// In production:
if ('production' !== 'production') { // false
  doSomethingDev();
} else {
  doSomethingProd(); // 👈
}
複製代碼

從上面能夠看出,替換以後, if 裏的表達式是不變的('production' !== 'production'的值永遠都是 false ),代碼壓縮工具可以移出掉對應的分支代碼。最終 production 模式下,壓縮以後的代碼是這樣的:

// In production (after minification):
doSomethingProd();
複製代碼

注意,若是表達式變得比上面複雜,代碼壓縮工具 不會 刪除掉那些死碼。看下面這個栗子:

let mode = 'production';
if (mode !== 'production') {
  // 🔴 not guaranteed to be eliminated
}
複製代碼

因爲JavaScript語言自己的動態特性,JS代碼的靜態分析工具不是太智能。當靜態分析工具在遇到一個變量(好比是 mode )時,他們一般什麼也不會作。只有當他們遇到明確的常量表達式(好比 false 或者 'production' !== 'production')時,他們能夠大膽的移出對應的代碼分支。

類似的,JavaScript壓縮工具的的死碼刪除功能,在面臨跨模塊引用時,也不會生效,好比下面的例子:

// 🔴 not guaranteed to be eliminated
import {someFunc} from 'some-module';

if (false) {
  someFunc();
}
複製代碼

所以,想要利用JavaScript壓縮工具的死碼刪除功能,你必須確保條件語句是常量,沒有包含變量,而且對應的分支代碼必須是在同一個 module 內部。


要實現這些功能,你的JavaScript打包工具須要根據你的編譯環境(development 或者 production)來替換 process.env.NODE_ENV

在幾年之前,開發者嚐嚐會忘記在編譯 production 包時設置對應的模式,這致使不少 development模式下編譯的 React 代碼在生產環境被使用。

在生產環境使用 development模式下的代碼,會增長咱們的JS文件大小,減慢頁面加載速度以及最終的代碼執行性能。

在過去的兩年,這種情形有所緩解。好比,webpack引入了一個簡單的 mode配置項,來避免開發者手動去設置 process.env.NODE_ENV的值。React開發者工具,在檢測到頁面使用了 development模式的React代碼時,會顯示一個紅色的圖標,這讓咱們很容易就察覺問題。

一些流行的代碼初始化工具,好比 Create React App,Next/Nuxt,Vue Cli,Gatsby等等,直接將developmentproduction模式下的打包命令分紅2個(好比開發模式是 npm start,生產環境使用 npm run build),進一步避免開發者去手動設置變量。一般來說,只有 production模式產出的代碼,會被部署到生產環境,所以,開發者不會再遇到之前的問題了。

一直以來都有一些爭論,認爲應該把 production 模式設置爲編譯工具的默認模式,development模式成爲須要開發者明確指定的。我我的認爲,這個觀點不是頗有說服力(譯註:我我的認爲默認是 production模式比較好😅)。那些可以從 development模式下的各類警告信息收益的開發者,一般是使用類庫的新手。他們可能並不知道怎麼開啓 development模式,由此可能會遇到不少原本能夠被那些警告信息所避免的bug。

誠然,development模式的代碼性能比較低,可能會讓一些開發者苦惱;但這也比發佈帶有bug的功能要強。舉個例子,React 列表的 key 警告 可以幫助開發者避免一些bug,好比發送了一條消息給錯誤的對象,或者購買了另外一個商品。若是在開發過程當中,沒有這些警告信息,開發者很難在開發階段就發現並解決一些潛在的bug。若是這些警告信息是默認關閉的(默認開啓 production模式的話),當你在某個時刻,切換到 development 模式時,你講看到太多的警告信息,這可能讓你不太想去修復這些問題。不修復問題,警告信息又太多,怎麼辦呢,大多數開發者可能會關閉警告,從新設置爲 production 模式。這就是爲何,development模式應該是默認開啓的,這讓開發者能夠一步步的發現並修復警告,而不是忽然面對一堆的警告信息變得無所適從。

若是development模式是可選的,而且開發者在初始時候就打開了 development模式,咱們就又回到了最初的那個問題:一些開發者可能會忘記在上線的時候,切換到 production模式,從而將 development模式的代碼發佈到了生產環境。


嗯,理論的東西說完啦(終於)

讓咱們再來看看這段代碼:

if (process.env.NODE_ENV !== 'production') {
  doSomethingDev();
} else {
  doSomethingProd();
}
複製代碼

你可能想知道:既然前端代碼中,根本就不存在全局的 process 對象,爲何經過 npm 包引入的 React/Vue 這些類庫,還要去依賴 process.env.NODE_ENV呢?

(再次說明下:若是你選擇的是直接經過 <script>標籤引入已經提早編譯好的代碼,你須要根據文件後綴 .js 仍是 .min.js 來手動選擇 development版本仍是production版本。後文要講的,都是從 npm 包通過打包工具來引入 React/Vue 的場景!)

和軟件編程領域的不少事情同樣,採用 process.env.NODE_ENV來標記不一樣環境,是歷史緣由。咱們繼續遵照這個約定,僅僅是由於它已經普遍地被大多數工具支持。換一個別的標記,沒有太大意義,也沒有社區工具的支持。

那麼這段背後的歷史是什麼呢?

importexport被加入JS語言標準以前的不少年,存在好幾個相互競爭的JS模塊化方案。Node.js 採用了來自 CommonJSrequire()module.exports

最初發布到 npm 上的包,都是給 Node.js 使用的,並非給前端代碼用的。做爲曾經是(或許如今也是?)最流行的 Node.js 服務端框架,Express 使用NODE_ENV這個環境變量 來開啓 production模式。在這以後,其餘的一些 npm包也採用了 process.env.NODE_ENV 來區分不一樣環境。

像 browserify 這樣的早期JavaScript代碼打包工具但願能在前端工程中,引入 npm 包提供的代碼。(是的,那時候 幾乎沒有前端開發者使用npm來發布本身的代碼,你能想象到嗎?) 。所以,這些早期的打包工具採納了 Node.js 生態中已經普遍採用的約定,使用 process.env.NODE_ENV 來區分不一樣環境。

從 React 發佈的那天起,在提供預編譯好的JS代碼以後,React 還提供了對應的 npm版本。伴隨着 React 的流行,愈來愈多的前端開發者也基於 CommonJS 的方式來發布前端代碼到 npm 倉庫。

React 須要在 production 模式中移出掉 development模式下的一些代碼。正好 Browserify 對這個問題有了解決方案,所以 React 也採用了這個方案,在 npm 包中經過 process.env.NODE_ENV來區分不一樣環境。到後來,愈來愈多的類庫和工具,好比 Vue 和 webpack,都採納了這個方案。

到2019年,browserify 已經再也不流行。可是,在JavaScript代碼編譯階段,將 process.env.NODE_ENV 替換成 developmentproduction 繼續像以前同樣被社區普遍採用。


還有一個事情,可能會讓你困惑。在 React 的 Github 源碼中,你會發現 __DEV__ 這樣的環境標記。可是在 npm 上的 React 代碼裏,使用的倒是 process.env.NODE_ENV。這是神馬狀況呢?

因爲歷史緣由,爲了和 Facebook 內部代碼保持一致,咱們使用了 __DEV__ 這個標記。在很長的一段時間裏,React 代碼都是直接被拷貝到 Facebook 的代碼倉庫,所以須要遵照相同的規則。所以,在發佈到 npm 以前,咱們會有一個替換步驟,將 __DEV__ 替換爲 process.env.NODE_ENV !== 'production'

在一些場景下,這會致使問題。一段基於 Node.js 約定的代碼,在 npm 下運行的很好,可是在 Facebook 內部卻出錯;或者是相反的問題。

所以,從 React 16 開始,咱們作出了一些調整。針對每一個環境(包括 <script>引入的預編譯代碼,npm,以及 Facebook內部倉庫),咱們都會 編譯對應的bundle

這意味着,針對 React 中的源碼 if (__DEV__) ,咱們實際上針對每一個包都會生成2個bundle:一個是設置了 __DEV__ = true 編譯出來的bundle;另外一個是設置了 __DEV__ = false 編譯的bundle。

舉個例子

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}
複製代碼

這個入口JS,是唯一一處你的打包工具須要替換 process.env.NODE_ENV 的地方,也是在這個入口JS,打包工具會忽略掉 developmentrequire

react.production.min.jsreact.development.js 兩個文件中,都 沒有 包含 process.env.NODE_ENV 這樣的檢查代碼。這很好,由於在 Node.js 裏訪問 process.env有些慢的。提早編譯好兩個模式下的 React 代碼,也能讓咱們的最初JS文件大小,無論咱們使用哪一個打包工具,都 更加的一致


根據運行環境執去執行不一樣的代碼,是一項很是強大的技術。我推薦你在你的類庫或者APP代碼中,也使用這個技術,只在 development模式下去作一些消耗性能的校驗和警告,在 production模式下移除這些代碼。

再次說明: 有一些片斷沒有搬過來,強烈推薦看看原文!

原文連接overreacted.io/how-does-th…

廣告時間

最後慣例,歡迎你們star咱們的人人貸大前端團隊博客,全部的文章還會同步更新到知乎專欄掘金帳號,咱們每週都會分享幾篇高質量的大前端技術文章。若是你喜歡這篇文章,但願能動動小手給個贊。

相關文章
相關標籤/搜索