React 源代码中的 invariant() 方法为啥构建后没有了?

2,831 阅读3分钟

React 源代码中大量使用 invariant() 展示警告信息,然而在构建版本中这类代码自动被转换成了抛出错误的形式,这其中用到了自定义 babel 插件。本文将探讨 React 源代码如何运用自定义插件,以及如何自己写一个 babel 插件。

Warnings and Invariants

React 源码采用 warning 模块展示警告。 warning 仅在开发环境中启用。在生产环境中,他们会被完全剔除掉。如果你需要在生产环境禁止执行某些代码,请使用 invariant 模块代替 warning:

var invariant = require('invariant');

invariant(
  2 + 2 === 4,
  'You shall not pass!'
);

当 invariant 判别条件为 false 时,会将 invariant 的信息作为错误抛出

“Invariant” 用于声明 “这个条件应总为 true”。你可以把它当成一种断言。

保持开发和生产环境的行为相似是十分重要的,因此 invariant 在开发和生产环境下都会抛出错误。不同点在于在生产环境中这些错误信息会被自动替换成错误代码,这样可以让输出库文件变得更小。

react-dom.development.js 文件会抛出这样的错:

react-dom.development.js:26509 Uncaught Error: Target container is not a DOM element.
    at Object.render (react-dom.development.js:26509)
    at <anonymous>:49:10
    at run (babel.js:61531)
    at check (babel.js:61597)
    at loadScripts (babel.js:61638)
    at runScripts (babel.js:61668)
    at transformScriptTags (babel.js:336)
    at babel.js:327

react-dom.production.min.js 文件直接使用时 invariant 内容会被转换成抛出错误代码:

react-dom.production.min.js:256 Uncaught Error: Minified React error #200; visit https://reactjs.org/docs/error-decoder.html?invariant=200 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at Object.v.render (react-dom.production.min.js:256)
    at dev.html:31
v.render @ react-dom.production.min.js:256
(anonymous) @ dev.html:31

如何开发一个 babel 插件

官网开发指南 Plugin Development handle-book

使用 rollup、rollup-plugin-babel 实现一个自定义 babel 插件的 demo: babel-plugin-development

React 中如何去除 invariant 的?

使用 rollup-plugin-babel 在 rollup 中使用 babel 的插件。

createBundle 函数调用 getPlugins 函数,其中继续调用 getBabelConfig 函数,该函数中判断到是 UMD 版本后会增加一个 transform-error-messages 插件

case UMD_DEV:
case UMD_PROD:
case UMD_PROFILING:
case NODE_DEV:
case NODE_PROD:
case NODE_PROFILING:
  return Object.assign({}, options, {
    plugins: options.plugins.concat([
      // Use object-assign polyfill in open source
      path.resolve('./scripts/babel/transform-object-assign-require'),
      // Minify invariant messages
      require('../error-codes/transform-error-messages'),
    ]),
  });

transform-error-messages 即为一个 babel 插件。

说明:

const DEV_EXPRESSION = t.identifier('__DEV__');

表示创建一个标示符,用于区分是 prod 还是 dev 版本,会直接出现在转译后的代码中。其他类似的 t.xxxXXX() 表示创建一个对应的 babel 节点,节点类型和接受的参数由 xxxXXX 决定。

插件为一个函数,接受一个参数,通过这个参数可以 babel.types. 该函数会返回一个对象,这个对象最重要的参数就是 visitor. visitor 使用了访客设计模式,具体可参考 Visitors.

该插件的 visitor 与 CallExpression 绑定,就是说在 babel 遍历 tree 时,遇到函数调用就会进入该方法。在该方法中使用 path.get('callee').isIdentifier({name: 'invariant'}) 判断是否是调用了 invariant 函数。是则进一步获取函数的参数,第一个参数为判断条件,为 false 则会在代码中抛出错误。第二个参数是字符串,描述了错误原因。

代码中使用 invertObject 通过第二个参数取到对应的 key, 并在 prod 版本使用 key 做为错误信息的参数,进一步压缩构建包的最终体积。

使用 replaceWith 将对源码进行了转译,核心代码如下

// Outputs:
// if (!condition) {
//   throw Error(
//     __DEV__
//       ? `A ${adj} message that contains ${noun}`
//       : formatProdErrorMessage(ERR_CODE, adj, noun)
//   );
// }
parentStatementPath.replaceWith(
  t.ifStatement(
    t.unaryExpression('!', condition),
    t.blockStatement([
      t.blockStatement([
        t.throwStatement(
          t.callExpression(t.identifier('Error'), [
            t.conditionalExpression(
              DEV_EXPRESSION,
              devMessage,
              prodMessage
            ),
          ])
        ),
      ]),
    ])
  )
);

使用父节点将这一段节点替换成了 if 语句,参数为 !condition, if 语句的内容为抛出错误,其中使用了 conditionalExpression 生成了三目表达式。

本系列文件开源在 read-source-code