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