如何在 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 中植入。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容