[译]JavaScript中的development模式怎么实现

619 阅读11分钟

#[译]JavaScript中的development模式怎么实现

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

译注: 忽略了一些内容,还是强烈建议阅读下原文吧。

随着你的 JavaScript 应用越来越复杂,你很可能会在 developmentproduction 模式下,分别加载和执行不同的代码逻辑。

能够在 developmentproduction 模式下,分别打包或执行不同的代码,是一种非常强大的能力。在 development 模式下,React 会给出关于你代码的一些警告,通常情况下,这些代码很可能会导致bug。但是,去提前监测并给出警告的这些代码,也增大了打包之后的JS文件大小,甚至会降低应用的性能。

在开发阶段,我们可以接受因为额外的监测代码而导致的应用性能损耗。事实上,在开发中,代码运行比production模式更加缓慢,甚至可能带来一些好处,这让开发者能体会到在一些性能低下的设备上,我们应用的运行效果,正如你知道的,通常开发者的机器性能是优于大部分的用户设备。

production 模式下,我们不能忍受这些额外的监测代码带来的性能损耗。因此,在 production 模式下, React 不会包含这些监测代码。下面让我们来看看具体是怎么做到的。


实现 developmentproduction 模式执行不同的代码,这依赖于你的JS代码编译流程(当然前提是你有JS编译流程……)。在Facebook,我们大概这样写代码:

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

上面代码里,__DEV__ 不是一个真实存在的变量。它在JS代码编译阶段,会被一个常量来替换,通常在 development 下是 true,在 production 模式下是 false。在不同模式下,编译出来的最终代码长这样:

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

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

production 模式下,你通常会使用一些压缩工具(比如 terser )来压缩JS代码。同时,大多数的JS代码压缩工具,都会进行一些 死码消除 ,比如在生成的代码中,删除掉 if(false){} 这样的代码分支。 因此,在 production 模式下,经过压缩之后,最终产出的代码长这样:

// 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我们的人人贷大前端团队博客,所有的文章还会同步更新到知乎专栏掘金账号,我们每周都会分享几篇高质量的大前端技术文章。如果你喜欢这篇文章,希望能动动小手给个赞。