如何在 React Native 中實現(xiàn)條件編譯

何為條件編譯,有什么應(yīng)用場景

以下面的 JAVA 代碼為例:

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

DEBUG 環(huán)境下,編譯出來的源碼只會包含 code block 1,其他環(huán)境編譯打包出來的源碼只會包含 code block 2,條件編譯在 C++、Java、Objective-C 這樣的編譯語言中默認(rèn)支持,但是在 RN 中卻是沒有提供這個功能。

這種按照特定條件編譯的功能有哪些應(yīng)用場景呢?我們?yōu)槭裁葱枰兀?/strong>

我們可能經(jīng)常會遇到類似這樣的需求:

  • 代碼需要根據(jù)運行環(huán)境,運行不同的代碼。比如,測試環(huán)境可以在頁面上浮層顯示調(diào)試信息,生產(chǎn)環(huán)境則不提示;同時又不希望輸出的代碼中存在判斷環(huán)境的 if-else 這樣的判斷代碼使得程序包體積增大
  • 項目交付給多個客戶使用,而某些客戶會有一些定制模塊。這些定制模塊只給特定用戶使用,不希望也一起打包在不相干的程序包中,但也不希望給定制客戶單獨維護(hù)一個特殊項目而增加維護(hù)成本。然后使用參數(shù)構(gòu)建不同程序:如 npm run build --a 構(gòu)建 a 用的程序包,npm run build -b 構(gòu)建 b 用的程序包。
  • 我們的代碼通常要兼容 iOS 和 Android,如果不用平臺文件來隔離這兩端的代碼,那么打包的時候 iOS 的 bundle 包中會包含 Android 的代碼Android 的 bundle 包中會包含 iOS 的代碼,這是我們不希望看到的。

使用條件編譯的方法,可以優(yōu)雅的解決上面提到的問題,發(fā)布的程序包中不會有多余的代碼存在,同時維護(hù)也方便。

在 RN 中實現(xiàn)條件編譯

我們知道 Java 和 Objective-C 這類編譯語言,有提供預(yù)編譯的功能,天然支持了條件編譯的能力。JavaScript 作為腳本語言,實際上是沒有編譯過程的,代碼編寫完之后能夠直接運行。那我們在 JavaScript 中如何實現(xiàn)條件編譯呢?

事實上由于 JavaScript 最初的設(shè)計缺陷,導(dǎo)致支持 JavaScript 的團(tuán)隊和社區(qū)不斷對其進(jìn)行完善,也就是我們熟知的 ES5、ES6、ES7 等的演進(jìn),包括 React.js、Vue.js 等構(gòu)建于 JavaScript 語言之上的框架出現(xiàn),使的 JavaScript 的呈現(xiàn)形態(tài)多種多樣,但是瀏覽器內(nèi)核的變化卻是異常的緩慢,只能運行 ES5 的 JavaScript 代碼,這個問題催生了 JavaScript 編譯器 Babel,最后衍生出了很多的 JavaScript 打包工具,比如:grunt , gulp,webpack, rollup 等。

RN 使用的是自定義的打包工具 metro,底層仍然會調(diào)用 Babel 將 ES6、React 轉(zhuǎn)成 ES5 的代碼,所以這是我們的突破口,可以通過自定義 Babel 插件來完成這項工作!

Babel 編譯代碼的過程:

總結(jié)就是:先將代碼轉(zhuǎn)換為AST(Parse) → 對AST進(jìn)行編輯生成新的AST(Transform) → 將轉(zhuǎn)換之后的AST生成新的代碼(Generate)

原理:可以發(fā)現(xiàn),代碼的轉(zhuǎn)換處理都是在 Transform 環(huán)節(jié)進(jìn)行的,我們需要在 Babel 將源碼轉(zhuǎn)換為 AST 之后、處理各種代碼文件之前,將代碼內(nèi)容根據(jù)設(shè)置的條件進(jìn)行修改,去掉當(dāng)前條件下不需要的代碼,保留需要的代碼,從而實現(xiàn)條件編譯的功能。

核心代碼如下:

/**
 * 條件變量名稱以及當(dāng)前值,通過 babel 配置傳遞過來
 * {
 * __ENV__: 'debug'
 * }
 */
let conditionEnvs = {};

/**
 * 判斷是否有效的二進(jìn)制表達(dá)式,
 * 二進(jìn)制表達(dá)式的操作符包含:"+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" (required)
 * 符合條件的表達(dá)式操作符為:"===", "==", "!==", "!="
 * @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 條件表達(dá)式
        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);

          // 找出要移除的條件分支節(jié)點
          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');
          }

          // 將要移除的條件分支替換為空實現(xiàn):{},并跳過子節(jié)點:由于替換了對應(yīng)的節(jié)點,如果不跳過子節(jié)點,會報錯
          if (removeNodePath.node && !t.isIfStatement(removeNodePath.node)) {
            removeNodePath.replaceWith(t.blockStatement([]));
            removeNodePath.skip();
          }
        }
      },
      /**
       * AST:三目運算符 條件表達(dá)式.
        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:邏輯運算符表達(dá)式. 這里只判斷 && 運算符場景
    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',
      {
        // 自定義條件變量名稱以及當(dāng)前打包的值
        __ENV__: 'debug'
      },
    ],
  ],
};
3、讓編譯器支持條件變量的引用

條件變量如果在程序中沒有定義,那么為了讓 js、ts 能夠識別條件變量而不報紅,需要在全局進(jìn)行聲明,我們只需要在項目根目錄創(chuàng)建一個 global.d.ts 來聲明你所定義的條件變量即可,如下:

declare const __ENV__: "debug" | "release"

另外,如果使用了 ESLint 代碼靜態(tài)檢查工具的,也需要讓 ESLint 能夠識別條件變量,需要在 .eslintrc.js 添加如何配置:

module.exports = {
  globals: {
    __ENV__: "readonly" // 將條件變量定義到這里
  }
}
4、在項目中使用
  • 條件表達(dá)式:if-else
if (__ENV__ == "debug") {
  console.log("debug code")
} else {
  console.log("release code")
} 
  • 三目運算符表達(dá)式:?:
__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"
}

待改進(jìn)

目前該插件是通過 babel.config.js 的配置植入到 Babel 編譯過程的,每次修改條件變量的值都需要清空緩存,比較麻煩,后期考慮在 metro 中植入。

本文為原創(chuàng),轉(zhuǎn)載請注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容