uni-app 已将常用的组件、JS API 封装到框架中,开发者按照 uni-app 规范开发即可保证多平台兼容,大部分业务均可直接满足。
但每个平台有自己的一些特性,因此会存在一些无法跨平台的情况。
- 大量写 if else,会造成代码执行性能低下和管理混乱。
- 编译到不同的工程后二次修改,会让后续升级变的很麻烦。
在 C 语言中,通过 #ifdef、#ifndef 的方式,为 windows、mac 等不同 os 编译不同的代码。
uni-app参考这个思路,为uni-app提供了条件编译手段,在一个工程里优雅的完成了平台个性化实现。
使用uni-app开发时,经常用到条件编译,上述是uni-app的官方描述,可见使用非常简单,下面是使用方法,平台默认是uni-app支持的一些平台,如:APP-PLUS、H5、MP-WEIXIN、MP-ALIPAY等。
| 条件编译写法 | 说明 |
|---|---|
| #ifdef APP-PLUS 需条件编译的代码 #endif | 仅出现在 App 平台下的代码 |
| #ifndef H5 需条件编译的代码 #endif | 除了 H5 平台,其它平台均存在的代码 |
| #ifdef H5 || MP-WEIXIN 需条件编译的代码 #endif | 在 H5 平台或微信小程序平台存在的代码(这里只有 ||,不可能出现&&,因为没有交集) |
支持的文件
- .vue
- .js
- .css
- pages.json
- 各预编译语言文件,如:.scss、.less、.stylus、.ts、.pug
下面来探究一下扩展出来的平台如何使用条件编译。如:MP-A、MP-C、MP-B,这三个平台代来自B小程序的扩展,代表三家不同公司,其中大部分业务相同,个别业务具有差异,因此需要将差异的部分通过条件编译分开。
遇到如下场景,改如何使用条件编译呢?
1.仅B公司需求
// js代码举例
// #ifdef MP-B
js代码...
// #endif
这是最简单的场景,通过上述代码,可以很好的实现隔离。
2.仅B公司不需要的业务,其他都需要
// js代码举例
// #ifndef MP-B
js代码...
// #endif
这个场景也比较单一,通过上述代码,可以很好的实现隔离。
3.仅B,C公司需要的业务,其他都不需要
// js代码举例
// #ifdef MP-B || MP-C
js代码...
// #endif
这个场景有点复杂了,参照官方文档,通过上述代码,可以很好的实现隔离吗?
这么做没有达到预想的结果,仅MP-B有效,MP-C没有效果,看看原因在哪里。
使用的编译命令是:
cross-env NODE_ENV=development A_NAME=c uniapp-cli custom mp-c
经过一番寻找,找到负责uni-app编译的文件,位置在node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-preprocess-loader/preprocess/lib/preprocess.js,如何找到这个文件呢?一个是可以从uniapp-cli命令开始,一步一步往下按照程序运行的顺序寻找,过程中可能遇到很多困难,参数传来传去,方法调用次数太多等。还有一个是直接查找,uni-app的依赖包都在@dcloudio中,把这个文件拷出来,在编辑器中全局搜索“ifdef”尝试一下,果然有这个结果,直接定位到这个文件。看到主要负责条件编译的代码在这里:
if (opts.type.if) {
rv = replaceRecursive(rv, opts.type.if, function (startMatches, endMatches, include, recurse) {
var variant = startMatches[1]
var test = (startMatches[2] || '').trim()
switch (variant) {
case 'if':
case 'ifdef':
return testPasses(test, context) ? (padContent(startMatches.input) + recurse(include) + padContent(endMatches.input)) : padContent(startMatches.input + include + endMatches.input)
case 'ifndef':
return !testPasses(test, context) ? (padContent(startMatches.input) + recurse(include) + padContent(endMatches.input)) : padContent(startMatches.input + include + endMatches.input)
// case 'ifdef':
// return typeof getDeepPropFromObj(context, test) !== 'undefined' ? (padContent(startMatches.input) + recurse(include) + padContent(endMatches.input)) : padContent(startMatches.input + include + endMatches.input) // fixed by xxxxxx
// case 'ifndef':
// return typeof getDeepPropFromObj(context, test) === 'undefined' ? (padContent(startMatches.input) + recurse(include) + padContent(endMatches.input)) : padContent(startMatches.input + include + endMatches.input) // fixed by xxxxxx
default:
throw new Error('Unknown if variant ' + variant + '.')
}
})
}
我们主要看下case 'ifdef'这里执行了啥,通过排查发现testPasses方法出了问题,继续深究发现错在这行代码:
return new Function('context', 'with (context||{}){ return ( ' + test + ' ); }')
这里的意思标明context中是否有符合test条件的属性,通过打印得到context,和test的内容:
// context 内容如下
{
APP_PLUS: false,
H5: false,
MP_ALIPAY: false,
MP_QQ: false,
MP_WEIXIN: true,
MP: true,
APP: false,
APP_PLUS_NVUE: false,
APP_VUE: false,
APP_NVUE: false,
MP_B: true
}
// test
MP_C || MP_B
由于with的缺陷,导致test第一个属性匹配不到context,就会报异常,所以解释了上面的现象,只有第一个条件起作用,“||”后的条件不起作用。
知道了原因,现在来探讨一下修改的方向。由于这些平台是扩展出来的,当自定义编译时,只把当前编译的平台注入到context中,其他的平台不会注入。如果所有扩展的平台都注入到context中,就可以解决了。第二个是改变new Function这段代码,改变test形式,不用with来判断,使用其他手段去匹配context中的属性。
框架不支持这样做,那么遵循少数服从多数的原则,把少数的需求每个平台写一遍,最后编译的结果没什么影响,但这样代码看起来会多些。
4.仅B,C公司不需要的业务,其他都需要
// js代码举例
无
这个场景很复杂了,这怎么写呢? 官方文档好像也没有这样的举例。
通过场景3的分析可以了解到,遇到这种情况目前做法是用条件编译的方式把每个平台写一遍代码。
本次分析了项目中遇到的uni-app条件编译问题,并通过“笨”方法补救。在实际工作中,有些问题可能不需要深入去探究,还是以实际解决问题为主要。若是在时间交充沛的情况下,倒是可以了解其运行机制,这样可以更加熟悉框架某一方面的使用,还可能在看框架代码时发现文档上面没有写到的“小技巧”。