[译]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咱们的人人贷大前端团队博客,全部的文章还会同步更新到知乎专栏掘金帐号,咱们每周都会分享几篇高质量的大前端技术文章。若是你喜欢这篇文章,但愿能动动小手给个赞。

相关文章
相关标签/搜索