Development模式是如何运做的?

原文:https://overreacted.io/how-do...
译者:前端技术小哥前端

图片描述
若是您的JavaScript代码库很是复杂,那么您可能会想办法在开发模式和生产模式中捆绑和运行不一样代码。react

在开发模式和生产模式中捆绑并运行不一样的代码是很是强大的。在开发模式中,React里有许多预警,能够帮助咱们在致使bug以前找到问题。然而,检测此类错误所需的代码一般会增长bundle文件的大小,并使应用程序运行得更慢。webpack

在开发模式中程序运行缓慢是能够接受的。事实上,在开发过程当中减慢代码的运行速度甚至多是有益的,由于它在必定程度上弥补了快速开发人员计算机和普通消费设备之间的差别。web

在生产中,咱们不想付出任何成本。所以,咱们在生产中省略了这些检查。这是怎么回事?让咱们来看看。npm

在开发中运行不一样代码的确切方法取决于JavaScript构建管道(以及是否有)。Facebook是这样的:编程

if (__DEV__) {
  doSomethingDev();
} else {
  doSomethingProd();
}

这里,__DEV__不是一个真正的变量。它是一个常量,当浏览器的模块被拼接在一块儿时这个常量就被替换掉了。结果是这样的:浏览器

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

// In production:
if (false) {
  doSomethingDev();
} else {
  doSomethingProd(); // ?
}

在生产中,咱们还须要在代码上运行一个压缩器(例如,terser)。大多数JavaScript微引擎都会进行部分的死代码消除,例如删除if(false)分支。因此在生产中你只会看到:服务器

// In production (after minification):
doSomethingProd();

(请注意,主流JavaScript工具如何有效地消除死码是有诸多限制的,但又这是一个单独的问题了。)框架

虽然您可能没有使用一个神奇的__DEV__常量,可是若是您使用一个流行的JavaScript捆绑器(如webpack),那么可能还有其余一些惯例能够遵循。例如,一般这样表示相同的模式:模块化

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

这正是使用捆绑器从NPM导入时React和Vue等库使用的模式。(单个文件(script)标签构建中将开发和生产版本做为单独的.js和.min.js文件提供。)

这一惯例最初来自Node.js。在Node.js中,有一个全局process变量将系统的环境变量做为process.env对象的属性公开。然而,当您在前端代码库中看到这个模式时,一般不涉及任何实际的process变量。

相反,整个process.env.NODE_ENV表达式在构建时被字符串文字替换,就像咱们的神奇的__DEV__变量:

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

// In production:
if ('production' !== 'production') { // false
  doSomethingDev();
} else {
  doSomethingProd(); // ?
}

由于整个表达式是常量('production' !== 'production'确保为false),因此压缩器也能够删除其余分支。

// In production (after minification):
doSomethingProd();

这个麻烦就解决啦

注意,这对更复杂的表达式没用:

let mode = 'production';
if (mode !== 'production') {
  // ? not guaranteed to be eliminated
}

因为JavaScript语言的动态特性,JavaScript静态分析工具不是很智能。当他们看到像mode这样的变量而不是像false或'production' !== 'production'这样的静态表达式时,他们一般会放弃。

一样,JavaScript在咱们使用顶级import语句时,死代码消除常常不能正常地跨模块边界运做:

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

if (false) {
  someFunc();
}

所以,咱们须要以一种很是机械的方式编写代码,使条件绝对静态,并确保要消除的全部代码都在其中。

要使全部这些正常运做,咱们的bundler须要执行process.env.NODE_ENV替换,并须要知道但愿在哪一种模式中构建项目。

几年前,咱们经常会忘记配置环境。因此咱们常常会看到一个处于开发模式的项目部署到生产模式中。这很糟糕,由于这会使网站加载和运行速度变慢。

在过去两年中,状况有了显著的改善。例如,webpack添加了一个简单的mode选项,而不是手动配置process.env.NODE_ENV替换。React DevTools如今还会在带有开发模式的站点上显示一个红色图标,这使得用户更容易发现以及报告。(此处需翻译图片中的文字)(此页面正在使用React开发构建模式。打开开发工具,React键将会出如今右侧。注意:发构建模式并不适用于生产模式。确保在部署前使用生产构建模式 )


像 Create React App、Next/Nuxt、Vue CLI、Gatsby和其余一些固定设置,将开发构建和生产构建分离成两个单独的指令,这样就更不容易产生问题。(例如,npm start和npm run build)通常状况下咱们只能部署生产构建,所以开发人员不会再犯这个错误。

老是有这样一种说法,即生产模式才应该被设置为默认的,而开发模式须要是手动切入。就我我的而言,我不认为这个论点有说服力。从开发模式的预警中获益最多的人一般是库的初学者。他们通常都不知道如何打开开发模式,而且会错过开发模式早就能给出的bug的高能预警。

是的,性能问题很糟糕。但向终端用户提供漏洞百出的体验也是如此。例如,React key预警有助于防止犯错,好比向错误的人发送消息或购买错误的产品。在禁用预警时进行开发对您和您的用户都会带来重大风险。若是默认状况下它是关闭的,那么当咱们找到切换键并打开它时,咱们将会面对过量的预警并须要清除。因此大多数人会把它切换回去。这就是为何须要从一开始就打开它,而不是稍后才启用它。

最后,即便选择切入开发预警,而且开发人员知道早早的时候就要打开它们,咱们又回到最初的问题。有人可能会在部署到生产环境中时忘记关闭它们!咱们又回到了出发点。

就我我的而言,我相信可以显示和使用正确模式的工具取决于咱们是在调试仍是部署。几十年来,除了Web浏览器以外,几乎全部其余环境(不管是移动、桌面仍是服务器)都有可以加载和区分开发和生产构建。也许是时候让JavaScript环境将这种区别视为头等需求了,而不是由库提出并依赖于临时约定。
说了这么多的理论知识!让咱们来看看实际操做:

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

你们可能会好奇:若是前端代码中没有真正的process对象,为何像React和Vue这样的库在npm构建中依赖它呢?(再次澄清一下:您能够在浏览器中加载的(script)标签,由React和Vue提供,不依赖于此。相反,咱们必须本身在开发.js和生产.min.js之间做选择。下面部分提到的只是关于经过从npm导入它们将React或Vue与捆绑器一块儿使用。)


像编程中的许多东西同样,这种特定的惯例主要是历史缘由。咱们如今仍在使用它,由于如今它被不一样的工具普遍采用。换成用其余东西是须要代价的,并且并无太多意义。那背后的历史缘由是什么呢?

在import和export语法标准化以前的不少年,存在着不止一种方式在竞争着来表达模块之间的关系。Node.js推广了require()和module.exports,称为CommonJS。早期在npm注册表上发布的代码是为Node.js编写的。Express是(而且可能如今仍然是?)Node.js最受欢迎的服务器端框架,它使用NODE_ENV环境变量来启用生产模式。其余一些npm包采用了相同的惯例。

像browserify这样的早期JavaScript捆绑包但愿可以在前端项目中使用来自npm的代码。(是的,当时几乎没有人使用npm做为前端!你们能想象吗?)所以他们将Node.js生态系统中已经存在的相同惯例扩展到前端代码。

最初的“envify”转换是在2013年发布的。React是在那个时候开源的,并且在那个时代使用browserify的npm彷佛是捆绑前端CommonJS代码的最佳解决方案。从一开始React就开始提供npm构建(包括(script)标记构建)。随着React的流行,使用CommonJS模块编写模块化JavaScript并经过npm发送前端代码的作法也开始流行。

React须要在生产模式中删除仅用于开发的代码。Browserify已经为这个问题提供了解决方案,所以React也采用了将process.env.NODE_ENV用于其npm构建的惯例。随着时间的推移,许多其余工具和库,包括webpack和Vue,都作了一样的事情。

到2019年,browserify已经失去了至关多的市场占有率。可是,在构建步骤中用'development'或'production'替换process.env.NODE_ENV还是一种流行的惯例。(看看如何采用ES模块做为分发格式,而不只仅是创做格式,会改变方程式,这颇有意思。)

还有一件事情可能仍然让你们感到困惑,在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年扩展到前端。也许这个解决方案并不完美,但对于每一个项目,采用它的成本要低于说服其余人作不一样的事情的成本。这教授了关于采用自上而下和自下而上的宝贵经验。理解这种动态的运行方式能够区分红功的标准化尝试和失败。

分离开发模式和生产模式是一种很是有用的技巧。我建议在您的库和应用程序代码中使用它,用于那些在生产环境中执行开销太大,但在开发中执行却颇有价值(并且经常很关键!)的检查。对于任何强大的特性,都有一些方法会误用它。

但愿本文能帮助到您!
看以后

点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
关注公众号「新前端社区」,享受文章首发体验!
每周重点攻克一个前端技术难点。

相关文章
相关标签/搜索