如何在 React Native 中实现条件编译

何为条件编译,有什么应用场景

以下面的 JAVA 代码为例:

#IFDEF DEBUG
/*
code block 1
*/
#ELSE
/*
code block 2
*/
#ENDIF

DEBUG 环境下,编译出来的源码只会包含 code block 1,其他环境编译打包出来的源码只会包含 code block 2,条件编译在 C++、Java、Objective-C 这样的编译语言中默认支持,但是在 RN 中却是没有提供这个功能。

这种按照特定条件编译的功能有哪些应用场景呢?我们为什么需要呢?

我们可能经常会遇到类似这样的需求:

  • 代码需要根据运行环境,运行不同的代码。比如,测试环境可以在页面上浮层显示调试信息,生产环境则不提示;同时又不希望输出的代码中存在判断环境的 if-else 这样的判断代码使得程序包体积增大
  • 项目交付给多个客户使用,而某些客户会有一些定制模块。这些定制模块只给特定用户使用,不希望也一起打包在不相干的程序包中,但也不希望给定制客户单独维护一个特殊项目而增加维护成本。然后使用参数构建不同程序:如 npm run build --a 构建 a 用的程序包,npm run build -b 构建 b 用的程序包。
  • 我们的代码通常要兼容 iOS 和 Android,如果不用平台文件来隔离这两端的代码,那么打包的时候 iOS 的 bundle 包中会包含 Android 的代码Android 的 bundle 包中会包含 iOS 的代码,这是我们不希望看到的。

使用条件编译的方法,可以优雅的解决上面提到的问题,发布的程序包中不会有多余的代码存在,同时维护也方便。

在 RN 中实现条件编译

我们知道 Java 和 Objective-C 这类编译语言,有提供预编译的功能,天然支持了条件编译的能力。JavaScript 作为脚本语言,实际上是没有编译过程的,代码编写完之后能够直接运行。那我们在 JavaScript 中如何实现条件编译呢?

事实上由于 JavaScript 最初的设计缺陷,导致支持 JavaScript 的团队和社区不断对其进行完善,也就是我们熟知的 ES5、ES6、ES7 等的演进,包括 React.js、Vue.js 等构建于 JavaScript 语言之上的框架出现,使的 JavaScript 的呈现形态多种多样,但是浏览器内核的变化却是异常的缓慢,只能运行 ES5 的 JavaScript 代码,这个问题催生了 JavaScript 编译器 Babel,最后衍生出了很多的 JavaScript 打包工具,比如:grunt , gulp,webpack, rollup 等。

RN 使用的是自定义的打包工具 metro,底层仍然会调用 Babel 将 ES6、React 转成 ES5 的代码,所以这是我们的突破口,可以通过自定义 Babel 插件来完成这项工作!

Babel 编译代码的过程:

总结就是:先将代码转换为AST(Parse) → 对AST进行编辑生成新的AST(Transform) → 将转换之后的AST生成新的代码(Generate)

原理:可以发现,代码的转换处理都是在 Transform 环节进行的,我们需要在 Babel 将源码转换为 AST 之后、处理各种代码文件之前,将代码内容根据设置的条件进行修改,去掉当前条件下不需要的代码,保留需要的代码,从而实现条件编译的功能。

核心代码如下:

/**
 * 条件变量名称以及当前值,通过 babel 配置传递过来
 * {
 * __ENV__: 'debug'
 * }
 */
let conditionEnvs = {};

/**
 * 判断是否有效的二进制表达式,
 * 二进制表达式的操作符包含:"+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" (required)
 * 符合条件的表达式操作符为:"===", "==", "!==", "!="
 * @param {*} binaryExpression
 */
function checkValidBinaryExpression(t, binaryExpression) {
  const validOperator = ['===', '==', '!==', '!='];
  if (
    binaryExpression &&
    t.isBinaryExpression(binaryExpression) &&
    validOperator.indexOf(binaryExpression.operator) !== -1 &&
    t.isIdentifier(binaryExpression.left) &&
    conditionEnvs.hasOwnProperty(binaryExpression.left.name) &&
    t.isStringLiteral(binaryExpression.right)
  ) {
    return true;
  } else {
    return false;
  }
}

module.exports = function (babel, options) {
  const t = babel.types;
  conditionEnvs = options;
  return {
    visitor: {
      /**
       * AST:if else 条件表达式
        interface IfStatement extends BaseNode {
            type: "IfStatement";
            test: Expression;
            consequent: Statement;
            alternate?: Statement | null;
        }
       * 示例:if (__ENV__ === "debug") { return "debug" } else { return "release" }
       * 替换为:if (__ENV__ === "debug") {} else { return "release" } 或者 if (__ENV__ === "debug") { return "debug" } else {}
       * @param {*} path
       */
      IfStatement(path) {
        if (checkValidBinaryExpression(t, path.node.test)) {
          let node = path.node.test;
          let conditionEnvValue = conditionEnvs[node.left.name];
          let operator = String(node.operator);
          let right = node.right;

          let rightValue = String(right.value);

          // 找出要移除的条件分支节点
          let removeNodePath = null;
          if (operator.indexOf('!=') !== -1) {
            // !=/!===
            removeNodePath =
              conditionEnvValue !== rightValue
                ? path.get('alternate')
                : path.get('consequent');
          } else {
            // ===
            removeNodePath =
              conditionEnvValue === rightValue
                ? path.get('alternate')
                : path.get('consequent');
          }

          // 将要移除的条件分支替换为空实现:{},并跳过子节点:由于替换了对应的节点,如果不跳过子节点,会报错
          if (removeNodePath.node && !t.isIfStatement(removeNodePath.node)) {
            removeNodePath.replaceWith(t.blockStatement([]));
            removeNodePath.skip();
          }
        }
      },
      /**
       * AST:三目运算符 条件表达式.
        interface ConditionalExpression extends BaseNode {
            type: "ConditionalExpression";
            test: Expression;
            consequent: Expression;
            alternate: Expression;
        }
       * 示例:__ENV__ === "debug" ? 'debug' : 'release';
       * 替换为:"debug" 或者 "release"
       * @param {*} path
       */
      ConditionalExpression(path) {
        if (checkValidBinaryExpression(t, path.node.test)) {
          let node = path.node.test;
          let conditionEnvValue = conditionEnvs[node.left.name];
          let operator = String(node.operator);
          let right = node.right;

          let rightValue = String(right.value);
          let replaceExpression = null;
          if (operator.indexOf('!=') !== -1) {
            // !=/!===
            replaceExpression =
              conditionEnvValue !== rightValue
                ? path.node.consequent
                : path.node.alternate;
          } else {
            // ===
            replaceExpression =
              conditionEnvValue === rightValue
                ? path.node.consequent
                : path.node.alternate;
          }
          path.replaceWith(replaceExpression);
        }
      },
      /**
     * AST:逻辑运算符表达式. 这里只判断 && 运算符场景
    interface LogicalExpression extends BaseNode {
        type: "LogicalExpression";
        operator: "||" | "&&" | "??";
        left: Expression;
        right: Expression;
    }
     * 示例:__ENV__ === "debug" && "release" 或者 __ENV__ !== "debug" && "release"
     * 替换为:"debug" 或者 "release"
     * @param {*} path
     */
      LogicalExpression(path) {
        if (
          checkValidBinaryExpression(t, path.node.left) &&
          path.node.operator === '&&'
        ) {
          let node = path.node.left;
          let conditionEnvValue = conditionEnvs[node.left.name];
          let operator = String(node.operator);
          let right = node.right;

          let rightValue = String(right.value);
          let replaceExpression = null;
          if (operator.indexOf('!=') !== -1) {
            // !=/!===
            replaceExpression =
              conditionEnvValue !== rightValue
                ? path.node.right
                : t.nullLiteral();
          } else {
            // ===
            replaceExpression =
              conditionEnvValue === rightValue
                ? path.node.right
                : t.nullLiteral();
          }
          path.replaceWith(replaceExpression);
        }
      },
    },
  };
};

使用步骤

1、安装
npm install --save-dev react-native-condition-pack
2、配置 babel.config.js 文件
module.exports = {
  plugins: [
    [
      'react-native-condition-pack',
      {
        // 自定义条件变量名称以及当前打包的值
        __ENV__: 'debug'
      },
    ],
  ],
};
3、让编译器支持条件变量的引用

条件变量如果在程序中没有定义,那么为了让 js、ts 能够识别条件变量而不报红,需要在全局进行声明,我们只需要在项目根目录创建一个 global.d.ts 来声明你所定义的条件变量即可,如下:

declare const __ENV__: "debug" | "release"

另外,如果使用了 ESLint 代码静态检查工具的,也需要让 ESLint 能够识别条件变量,需要在 .eslintrc.js 添加如何配置:

module.exports = {
  globals: {
    __ENV__: "readonly" // 将条件变量定义到这里
  }
}
4、在项目中使用
  • 条件表达式:if-else
if (__ENV__ == "debug") {
  console.log("debug code")
} else {
  console.log("release code")
} 
  • 三目运算符表达式:?:
__ENV__ === "debug" ? 'debug code' : 'release code'
  • 逻辑运算符:&&
__ENV__ === "debug" && "debug code"
5、使用注意事项
  • 代码中用来判断的条件变量必须和在babel.config.js中定义的保持一致,不能使用中间变量替代,如下为错误示例:
const env = __ENV__
env === "debug" ? 'debug code' : 'release code' // 错误
  • 条件变量的值更改之后需要清空缓存,不然 Babel 不会重新编译代码,可以在每次运行 RN 的时候自动清空缓存:
scripts: {
  "start": "react-native start --reset-cache"
}

待改进

目前该插件是通过 babel.config.js 的配置植入到 Babel 编译过程的,每次修改条件变量的值都需要清空缓存,比较麻烦,后期考虑在 metro 中植入。

本文为原创,转载请注明出处

推荐阅读更多精彩内容