若是你的 JavaScript 代碼庫已經達到的中等複雜度, 你也許能夠選擇在 development 和 production 打包和運行不一樣的代碼.
If your JavaScript codebase is even moderately complex, you probably have a way to bundle and run different code in development and production.node
在 development 和 production 打包和運行不一樣的代碼是強大的. 在 development 模式下, React 包含不少警告, 來幫助你在引入 bug 以前找到對應的問題. 可是, 探測這些錯誤的必要的代碼常常會增長包體積和使 app 運行比較緩慢. Bundling and running different code in development and production is powerful. In development mode, React includes many warnings that help you find problems before they lead to bugs. However, the code necessary to detect such mistakes often increases the bundle size and makes the app run slower.
在開發環境中, 這個緩慢是能夠接受的. 實際上, 在開發環境中運行代碼比較緩慢也許甚至是有裨益的, 由於其部分補償了在快速的開發機器和平均水平的消費者機器之間的差別. The slowdown is acceptable in development. In fact, running the code slower in development might even be beneficial because it partially compensates for the discrepancy between fast developer machines and an average consumer device.
生產環境中, 咱們不想要付出任何的這種消耗. 所以, 在生產環境中咱們省略了這些檢查. 這是如何工做的? 讓咱們來看一看. In production we don’t want to pay any of that cost. Hence, we omit these checks in production. How does that work? Let’s take a look.
在開發環境中運行不一樣的代碼的確切方式依賴你的 JavaScript 構建流程(以及是否有一個). 在 Facebook 它看起來像這樣: The exact way to run different code in development depends on your JavaScript build pipeline (and whether you have one). At Facebook it looks like this:
if (__DEV__) {
doSomethingDev();
} else {
doSomethingProd();
}
複製代碼
這裏, `__DEV__` 並非一個真正的變量. 當 modules 爲了瀏覽器被一塊兒拼接時, 其將會被替換爲一個常量, . 結果看起來像這樣: Here, `__DEV__` isn’t a real variable. It’s a constant that gets substituted when the modules are stitched together for the browser. The result looks like this:
在生產環境中, 你還要在代碼上運行一個壓縮器(minifier)(好比 terser). 大多數的 JavaScript 壓縮器(minifier) 會作一些有限形式的 死碼刪除, 好比移除 if (false) 分支, 所以在生產環境中你將只會看到: In production, you’d also run a minifier (for example, terser) on the code. Most JavaScript minifiers do a limited form of dead code elimination, such as removing if (false) branches. So in production you’d only see:
// In production (after minification)
doSomethingProd()
複製代碼
(注意, 主流的 JavaScript 工具對於有效的死碼移除是很是有限的, 不過這是另外一個話題了.)(Note that there are significant limits on how effective dead code elimination can be with mainstream JavaScript tools, but that’s a separate topic.) 雖然你可能沒有使用 `__DEV__` 魔法常量, 若是你使用像 webpack 之類的流行的 JavaScript 打包工具, 這裏或許有一些約定你能夠遵循. 例如, 像這樣的表達相同模式是常見的: While you might not be using a __DEV__ magic constant, if you use a popular JavaScript bundler like webpack, there’s probably some other convention you can follow. For example, it’s common to express the same pattern like this:
當你使用打包器從 npm 導入一些例如 React, Vue 的庫的時候, 它們正是使用了這種模式. (單文件的 <script> 標籤構建爲開發環境和生產環境提供了不一樣的 .js 和 .min.js 文件.) That’s exactly the pattern used by libraries like React and Vue when you import them from npm using a bundler. (Single-file
<script> tag builds offer development and production versions as separate
.js and
.min.js files.)
這個特殊的約定最初來源於 Node.js. 在 Node.js 中, 有一個全局的 process 變量, 其暴露系統的的環境變量做爲 process.env 對象的屬性. 可是, 當你在前端的代碼庫中看到這個模式, 這裏一般沒有任何真正的 process 變量介入. 🤯 This particular convention originally comes from Node.js. In Node.js, there is a global
process variable that exposes your system’s environment variables as properties on the
process.env object. However, when you see this pattern in a front-end codebase, there isn’t usually any real
process variable involved. 🤯
代替的是, 整個 process.env.NODE_ENV 表達式都會在構建時期被替換成一個字符串字面量, 就好像咱們的魔法 __ENV__ 變量: Instead, the whole
process.env.NODE_ENV expression gets substituted by a string literal at the build time, just like our magic
__ENV__ variable:
由於整個表達式是固定的 ('production' !== 'production' 保證爲 false), 壓縮器也能移除其餘的分支. Because the whole expression is constant (
'production' !== 'production' is guaranteed to be
false), a minifier can also remove the other branch.
// In production(after minification):
doSomethingProd();
複製代碼
惡做劇完成. Mischief managed.
注意, 帶有更復雜的表達式的這個將不會工做: Note that this wouldn’t work with more complex expressions:
let mode = 'production';
if (mode !== 'production') {
// 🔴 not guaranteed to be eliminated
}
複製代碼
JavaScript 靜態分析工具還不是太智能, 由於該門語言的動態天性.當它們看到諸如 mode 這種變量而不是像 false 或者是 'production' !== 'production' 這種靜態表達式的時候, 它們每每會放棄. JavaScript static analysis tools are not very smart due to the dynamic nature of the language. When they see variables like
mode rather than static expressions like
false or
'production' !== 'production', they often give up.
類似的, 當你使用頂層 import 語句的時候, 在穿越模塊邊界的死碼移除在 JavaScript 中一般也不會正常工做: Similarly, dead code elimination in JavaScript often doesn’t work well across the module boundaries when you use the top-level import statements:
import {someFunc} from'some-module';
if (false) {
someFunc()
}
複製代碼
所以你須要以很是機械的方式寫書寫代碼, 使得條件絕對靜態, 而且確保你想要移除的全部代碼都在其中. So you need to write code in a very mechanical way that makes the condition definitely static, and ensure that all code you want to eliminate is inside of it.
爲了讓這些工做, 你的打包器須要去作 process.env.NODE_ENV 的替換, 而且須要知道你要在哪一種模式下構建該項目. For all of this to work, your bundler needs to do the
process.env.NODE_ENV replacement, and needs to know in which mode you want to build the project in.
幾年之前, 忘了配置環境在過去很廣泛. 你會常常看到一個項目在 development 模式下部署到生產環境. A few years ago, it used to be common to forget to configure the environment. You’d often see a project in development mode deployed to production.
這很沮喪, 由於它使得網站加載和運行比較慢. That’s bad because it makes the website load and run slower.
在最近的兩年, 這個狀況已經很大的改善. 例如, webpack 增長了一個簡單的 mode 選項來替代手動的配置 process.env.NODE_ENV 的替換. 當站點在開發模式下的時候, React Devtools 如今也展現一個紅色的 icon, 使其容易發現和甚至是報告. In the last two years, the situation has significantly improved. For example, webpack added a simple
mode option instead of manually configuring the
process.env.NODE_ENV replacement. React DevTools also now displays a red icon on sites with development mode, making it easy to spot and even
report.
像 Create React App, Next/Nuxt, Vue CLI, Gatsby 這樣的觀點鮮明的設置工具經過分開開發打包和生產打包成爲兩個單獨的命令使其更難去弄亂.(例如, npm start 和 npm run build) 特別的是, 只有生產打包能夠部署, 因此開發者不再會犯這種錯誤了. Opinionated setups like Create React App, Next/Nuxt, Vue CLI, Gatsby, and others make it even harder to mess up by separating the development builds and production builds into two separate commands. (For example,
npm start and
npm run build.) Typically, only a production build can be deployed, so the developer can’t make this mistake anymore.
這裏常常有一個觀點, 也許 production 模式須要成爲默認, development 模式須要選擇性加入. 就我的而言, 我沒有發現這個觀點是有說服力的. 從 development 模式的警告中獲益不少的人通常是剛剛接觸庫. 他們大概是不知道如何開啓的, 而且經常會忽略一些能夠在早期就被感知到的一些 bug. There is always an argument that maybe the production mode needs to be the default, and the development mode needs to be opt-in. Personally, I don’t find this argument convincing. People who benefit most from the development mode warnings are often new to the library. They wouldn’t know to turn it on, and would miss the many bugs that the warnings would have detected early.
是的, 性能問題是很差的. 可是, 向終端用戶提供破損的, 古怪的體驗也是如此. 例如, React key warning 幫助避免如發送一個消息給錯誤的人或買了一個錯誤的產品這類 bug. 開發過程當中關掉該警告, 對你和你的用戶都會陷入重大的風險中.若是這個默認是關掉的, 隨後你發現了開關而且將其打開, 你將擁有太多的警告須要去清理. 所以大部分的人將關掉開關. 這就是爲何其須要從開始就是開啓的, 而不是在以後再開啓. Yes, performance issues are bad. But so is shipping broken buggy experiences to the end users. For example, the
React key warning helps prevent bugs like sending a message to a wrong person or buying a wrong product. Developing with this warning disabled is a significant risk for you and your users. If it’s off by default, then by the time you find the toggle and turn it on, you’ll have too many warnings to clean up. So most people would toggle it back off. This is why it needs to be on from the start, rather than enabled later.
最後, 即便開發警告是可選入的, 而且開發者知道如何在早起的開發中開啓它們, 咱們將回到最初的問題, 一些人將會意外的在部署生產的時候保持它們是開啓的! Finally, even if development warnings were opt-in, and developers knew to turn them on early in development, we’d just go back to the original problem. Someone would accidentally leave them on when deploying to production!
咱們又回到了原點. And we’re back to square one.
我的而言, 我相信, 工具顯示和使用正確的模式取決於你是在調試仍是在部署. 除了 web 瀏覽器, 幾乎全部其餘環境(不管 mobile, desktop, 或者是 server)都已經有一個方式去加載和區分開發和生產構建, 這已經存在了數十年了. Personally, I believe in tools that display and use the right mode depending on whether you’re debugging or deploying. Almost every other environment (whether mobile, desktop, or server) except the web browser has had a way to load and differentiate development and production builds for decades.
也許是時候 JavaScript 環境視該區別爲第一類需求, 而不是由庫提出並依賴特別的約定. Instead of libraries coming up with and relying on ad-hoc conventions, perhaps it’s time the JavaScript environments see this distinction as a first-class need.
足夠的哲學! Enough with the philosophy!
讓咱們再看看這段代碼: Let’s take another look at this code:
你也許會疑惑: 若是這裏在前端代碼中沒有真正的 process 對象, 爲何像 React 和 Vue 這樣的庫會在 npm 構建的時候依賴它? You might be wondering: if there’s no real
process object in front-end code, why do libraries like React and Vue rely on it in the npm builds?
再次闡明: 你能夠在 browser 中使用 <script> 標籤加載, React 和 Vue 都提供, 不依賴此(譯者注: 指的是上述的 process 對象). 可是你須要手動的在開發環境中的 .js 和 生產環境中的 .min.js 文件中選擇. 接下來的只是關於經過一個打包器從 npm 導入來使用 React 和 Vue. (To clarify this again: the <script> tags you can load in the browser, offered by both React and Vue, don’t rely on this. Instead you have to manually pick between the development .js and the production .min.js files. The section below is only about using React or Vue with a bundler by importing them from npm.)
就像在程序中的許多東西, 該特殊的約定也多數是歷史緣由. 咱們繼續使用它是由於如今他已經普遍的被不一樣的工具適配. 切換到其餘是昂貴的而且沒有太多收益. Like many things in programming, this particular convention has mostly historical reasons. We are still using it because now it’s widely adopted by different tools. Switching to something else is costly and doesn’t buy much.
那麼其背後的歷史是什麼? So what’s the history behind it?
在 import 和 export 語法被標準化以前的許多年, 有幾種競爭的方式去傳達在模塊之間的關係. node.js 使 require() 和 module.export 流行起來, 被稱爲 CommonJS. Many years before the import and export syntax was standardized, there were several competing ways to express relationships between modules. Node.js popularized require() and module.exports, known as
CommonJS.
早期在 npm 發佈的代碼是爲了 Node.js 寫的. Express 曾(也許如今也是?)是 Node.js 最流程的服務端框架, 而且其 使用 NODE_ENV 環境變量 去開啓 production 模式. 一些其餘包也採用了該約定. Code published on the npm registry early on was written for Node.js.
Express was (and probably still is?) the most popular server-side framework for Node.js, and it
used the NODE_ENV environment variable to enable production mode. Some other npm packages adopted the same convention.
像 browserify 之類的早期 JavaScript 打包器想要在前端項目之上使用 npm 的代碼成爲可能. (是的, 回到 了幾乎沒有人在前端中使用 npm的時期! 你能想象嗎?) 因此他們將已經存在於 Node.js 生態的相同約定擴展進了前端的代碼中. Early JavaScript bundlers like browserify wanted to make it possible to use code from npm in front-end projects. (Yes,
back then almost nobody used npm for front-end! Can you imagine?) So they extended the same convention already present in the Node.js ecosystem to the front-end code.
最先的 "envify" 轉換器在 2013 被髮布. React 差很少也是在同時期開源, 而且在那個時期, npm 和 browserify 彷佛是打包前端的 CommonJS 代碼最好的解決方案. The original 「envify」 transform was released in 2013. React was open sourced around that time, and npm with browserify seemed like the best solution for bundling front-end CommonJS code during that era.
React 從一開始就提供 npm 構建(除 <script> 標籤構建外). 隨着 React 流行, 使用 CommonJS 模塊編寫模塊化 JavaScript 並經過 npm 發送前端代碼也流行起來. React started providing npm builds (in addition to
<script> tag builds) from the very beginning. As React got popular, so did the practice of writing modular JavaScript with CommonJS modules and shipping front-end code via npm.
React 須要移除在 production 模式中移除 development-only 的代碼. Browserify 已經給這個問題提供瞭解決方案, 因此 React 也採用了在 npm 的構建中使用 process.env.NODE_ENV 的約定. 通過時間的推移, 許多其餘的工具和庫, 包括 webpack 和 Vue, 也是這樣作的. React needed to remove development-only code in the production mode. Browserify already offered a solution to this problem, so React also adopted the convention of using
process.env.NODE_ENV for its npm builds. With time, many other tools and libraries, including webpack and Vue, did the same.
到了 2019 年, browserify 已經失去了大量的市場佔有率. 可是, 在構建步驟將 process.env.NODE_ENV 替換成 'development' 或者是 'production' 是一種很是流行的約定. By 2019, browserify has lost quite a bit of mindshare. However, replacing
process.env.NODE_ENV with 'development' or 'production' during a build step is a convention that is as popular as ever.
改變現狀, 看看如何採用 ES 模塊做爲分發格式將是頗有趣的, 而不是一個創做格式. 在 Twitter 上面告訴我?(It would be interesting to see how adoption of ES Modules as a distribution format, rather than just the authoring format, changes the equation. Tell me on Twitter?) 有一件事情有可能依然在迷惑你, 在Github 上面的 React 源碼, 你將看到 __DEV__ 還在做爲魔術變量來使用. 可是在 npm 的 React 代碼, 其使用 process.env.NODE_ENV. 這是怎麼工做的? One thing that might still confuse you is that in React source code on GitHub, you’ll see __DEV__ being used as a magic variable. But in the React code on npm, it uses
process.env.NODE_ENV. How does that work?
在歷史上, 咱們在源碼中使用 __DEV__ 來知足 Facebook 的源碼. 長期以來, React 是直接 copy 進入 Facebook 的代碼庫, 因此它須要遵循相同的規則. 對於 npm, 咱們有一個構建步驟, 其按字面的在發佈以前將 __DEV__ 檢查替換成 process.env.NODE_ENV !== 'production'. Historically, we’ve used __DEV__ in the source code to match the Facebook source code. For a long time, React was directly copied into the Facebook codebase, so it needed to follow the same rules. For npm, we had a build step that literally replaced the __DEV__ checks with process.env.NODE_ENV !== 'production' right before publishing.
這有時候是一個問題. 有時, npm 上工做正常的一個代碼模式依賴一些 Node.js 的約定, 可是破壞了 Facebook, 反之亦然. This was sometimes a problem. Sometimes, a code pattern relying on some Node.js convention worked well on npm, but broke Facebook, or vice versa.
這意味着, 在 React 源碼使用 if (__DEV__) 的時候, 咱們實際上對每個包生成兩個 bundle. 一個已經用 __DEV__ = true 預編譯, 另外一個用 __DEV__ = false 預編譯. 在 npm 中的每個包的入口點決定導出哪個. This means that while the React source code says if (__DEV__), we actually produce two bundles for every package. One is already precompiled with __DEV__ = true and another is precompiled with __DEV__ = false. The entry point for each package on npm 「decides」 which one to export.
例如: For Example:
而且這裏是你的打包器將 'development' 或者是 'production' 做爲字符串插入的惟一地方, 而且這裏是你的壓縮器移除 development-only require 的地方. And that’s the only place where your bundler will interpolate either
'development' or
'production' as a string, and where your minifier will get rid of the development-only
require.
react.production.min.js 和 react.development.js 二者都不在有任何的 process.env.NODE_ENV 檢查. 這是很棒的, 由於當實際在 Node.js 中運行的時候, 訪問 process.env 稍許緩慢. 提早在兩個模式下編譯 bundle 也讓咱們優化文件體積更加一致, 不管你使用的是哪一種打包器和壓縮器. Both react.production.min.js and react.development.js don’t have any
process.env.NODE_ENV checks anymore. This is great because when actually running on Node.js, accessing
process.env is somewhat slow. Compiling bundles in both modes ahead of time also lets us optimize the file size much more consistently, regardless of which bundler or minifier you are using.
這纔是真正有效的. And that’s how it really works!
我但願, 這裏會有一個更一流的方式來作, 而不是依賴約定, 可是咱們已是這樣了. 若是模式在全部的 JavaScript 環境中是一流概念, 而且當某些代碼沒有被認爲應該是運行在 development 模式下的時候, 若是瀏覽器中有一些方式來揭露, 這應該是很棒的. I wish there was a more first-class way to do it without relying on conventions, but here we are. It would be great if modes were a first-class concept in all JavaScript environments, and if there was some way for a browser to surface that some code is running in a development mode when it’s not supposed to.
另外一方面, 一個單一的項目中一個約定能夠傳播至生態是很是迷人的. 在 2010 年 EXPRESS_ENV 成爲 NODE_ENV, 而且在 2013 年蔓延至前端. 也許該解決方案不是最好的, 可是對於每個項目, 採用這個的成本是低於說服其餘人作一些不一樣的事情的成本. 這教授了關於採用的自上而下與自下而上的有價值的經驗. 瞭解這種動態如何發揮做用將成功的標準化嘗試與失敗區分開來. On the other hand, it is fascinating how a convention in a single project can propagate through the ecosystem. EXPRESS_ENV became NODE_ENV in 2010 and spread to front-end in 2013. Maybe the solution isn’t perfect, but for each project the cost of adopting it was lower than the cost of convincing everyone else to do something different. This teaches a valuable lesson about the top-down versus bottom-up adoption. Understanding how this dynamic plays out distinguishes successful standardization attempts from failures.
區分 development 和 production 模式是很是有用的技術. 我建議在你的庫或者是應用程序中使用它, 作一些在生產環境中作起來太昂貴的這類檢查, 可是在開發中去作是很是有價值的(而且每每是極重要的). Separating development and production modes is a very useful technique. I recommend using it in your libraries and the application code for the kinds of checks that are too expensive to do in production, but are valuable (and often critical!) to do in development.
正如其餘的強大的特性同樣, 有一些方法能夠濫用它, 這將是我下一篇文章的主題! As with any powerful feature, there are some ways you can misuse it. This will be the topic of my next post!