美文网首页
如何在 React Native 中实现条件编译

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

作者: peaktan | 来源:发表于2021-03-13 21:54 被阅读0次

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

    以下面的 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 中植入。

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

    相关文章

      网友评论

          本文标题:如何在 React Native 中实现条件编译

          本文链接:https://www.haomeiwen.com/subject/daegqltx.html