- 原文地址:开发模式的工做原理是什么?
- 原文做者: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。