何为条件编译,有什么应用场景
以下面的 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
中植入。
本文为原创,转载请注明出处