
何為條件編譯,有什么應(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)載請注明出處