本文我们来谈谈微信小程序系统兼容性的那些坑。
微信小程序兼容性问题
微信小程序发布一周多了,兼容性问题,特别是 Android 平台兼容性问题特别严重。据我观察,好多小程序掉到兼容性的坑里。掉坑里不要紧,更让人捉急的是,从坑里爬上来的时候,手刚抓到坑沿,又被微信官方踩到(紧急修复兼容性的版本没审核通过,被微信打回重审),再次跌落坑底,然后眼睁睁地看着后台用户在破口大骂“什么东西都没有啊~,什么破小程序”。
微信小程序的兼容性问题除了微信本身的 Bug 外,大部分是目标平台对 JavaScript 标准库支持程度不同造成的。
微信本身的 Bug 引起的
微信本身的 Bug 引发的兼容性问题有个现成的例子,就是 wx.request()
返回的状态码 res.statusCode
的值在 iOS 下是 int 型数据,而在 Android 6.0.1 上却是 String 型数据。如果你判断服务器的返回状态码方法不当,可能就踩到坑里了。
wx.request({
url: 'http://api.example.com',
success: function (res) {
if (res.statusCode === 200) {
// success
} else {
// server failure
}
}
})
上述代码就踩坑了,正确的做法是使用 ==
而不是使用 ===
来判断。另外一个更规范的方法是使用 parseInt(res.statusCode) === 200
来实现。
Javascript 标准库兼容性问题
比如 Array.find()
方法在 iOS 10.2/Android 7.0 上完美支持,但在 Android 6.0.1 上却不支持。如果代码里用到了这个接口,就会导致在 Android 6.0.1 上无法正常工作。通过对比发现,这类接口不支持的个数还是比较多的。特别是 Android 平台版本众多,兼容性问题就更严重,可能一不小小心就掉到坑里。
解决方法
微信本身 Bug 只能绕过去,但对 JavaScript 引擎的兼容性,可以有更优雅的解决方法。比如,我们可以打补丁,使用 polyfill 来实现这些不支持的标准库方法。比如,修复 Android 6.0.1 平台不支持 String.startsWith()
的问题,可以使用下面的 polyfill 代码:
if (!String.prototype.startsWith) {
console.warn('define polyfill for Array.prototype.startsWith');
String.prototype.startsWith = function (searchString, position) {
position = position || 0;
return this.substr(position, searchString.length) === searchString;
};
}
推而广之,我们可以把平台不支持的标准库方法,使用 polyfill 实现。这就是 minapp-polyfill 这个项目的目的。
使用方法很简单,把 minapp-polyfill 项目里的 polyfill.js
拷贝到小程序源码目录下,在需要打补丁的 JavaScript 源文件头部引入如下代码即可:
import 'path/to/polyfill.js'
目前这个项目只是搭了个骨架,还有很多方法需要实现。PRs is welcome。
各个平台对 JavaScript 标准库支持情况
条件限制,这里统计了四个平台对 JavaScript 标准库的支持情况,分别是 iOS 10.2, Android 6.0.1, Android 7.0, 微信开发者工具,具体数据如下:
Component.apiName | iOS 10.2 | Android 6.0.1 | Android 7.0 | devtool |
---|---|---|---|---|
Array.toString | YES | YES | YES | YES |
Array.values | YES | N/A | YES | N/A |
Array.toLocaleString | YES | YES | YES | YES |
Array.concat | YES | YES | YES | YES |
Array.fill | YES | N/A | YES | YES |
Array.join | YES | YES | YES | YES |
Array.pop | YES | YES | YES | YES |
Array.push | YES | YES | YES | YES |
Array.reverse | YES | YES | YES | YES |
Array.shift | YES | YES | YES | YES |
Array.slice | YES | YES | YES | YES |
Array.sort | YES | YES | YES | YES |
Array.splice | YES | YES | YES | YES |
Array.unshift | YES | YES | YES | YES |
Array.every | YES | YES | YES | YES |
Array.forEach | YES | YES | YES | YES |
Array.some | YES | YES | YES | YES |
Array.indexOf | YES | YES | YES | YES |
Array.lastIndexOf | YES | YES | YES | YES |
Array.filter | YES | YES | YES | YES |
Array.reduce | YES | YES | YES | YES |
Array.reduceRight | YES | YES | YES | YES |
Array.map | YES | YES | YES | YES |
Array.entries | YES | N/A | YES | YES |
Array.keys | YES | N/A | YES | YES |
Array.find | YES | N/A | YES | YES |
Array.findIndex | YES | N/A | YES | YES |
Array.includes | YES | N/A | N/A | YES |
Array.copyWithin | YES | N/A | YES | YES |
Array.constructor | YES | YES | YES | YES |
Buffer | N/A | N/A | N/A | N/A |
DataView.getInt8 | YES | YES | YES | YES |
DataView.getUint8 | YES | YES | YES | YES |
DataView.getInt16 | YES | YES | YES | YES |
DataView.getUint16 | YES | YES | YES | YES |
DataView.getInt32 | YES | YES | YES | YES |
DataView.getUint32 | YES | YES | YES | YES |
DataView.getFloat32 | YES | YES | YES | YES |
DataView.getFloat64 | YES | YES | YES | YES |
DataView.setInt8 | YES | YES | YES | YES |
DataView.setUint8 | YES | YES | YES | YES |
DataView.setInt16 | YES | YES | YES | YES |
DataView.setUint16 | YES | YES | YES | YES |
DataView.setInt32 | YES | YES | YES | YES |
DataView.setUint32 | YES | YES | YES | YES |
DataView.setFloat32 | YES | YES | YES | YES |
DataView.setFloat64 | YES | YES | YES | YES |
DataView.constructor | YES | YES | YES | YES |
Date.toString | YES | YES | YES | YES |
Date.toISOString | YES | YES | YES | YES |
Date.toDateString | YES | YES | YES | YES |
Date.toTimeString | YES | YES | YES | YES |
Date.toLocaleString | YES | YES | YES | YES |
Date.toLocaleDateString | YES | YES | YES | YES |
Date.toLocaleTimeString | YES | YES | YES | YES |
Date.valueOf | YES | YES | YES | YES |
Date.getTime | YES | YES | YES | YES |
Date.getFullYear | YES | YES | YES | YES |
Date.getUTCFullYear | YES | YES | YES | YES |
Date.getMonth | YES | YES | YES | YES |
Date.getUTCMonth | YES | YES | YES | YES |
Date.getDate | YES | YES | YES | YES |
Date.getUTCDate | YES | YES | YES | YES |
Date.getDay | YES | YES | YES | YES |
Date.getUTCDay | YES | YES | YES | YES |
Date.getHours | YES | YES | YES | YES |
Date.getUTCHours | YES | YES | YES | YES |
Date.getMinutes | YES | YES | YES | YES |
Date.getUTCMinutes | YES | YES | YES | YES |
Date.getSeconds | YES | YES | YES | YES |
Date.getUTCSeconds | YES | YES | YES | YES |
Date.getMilliseconds | YES | YES | YES | YES |
Date.getUTCMilliseconds | YES | YES | YES | YES |
Date.getTimezoneOffset | YES | YES | YES | YES |
Date.setTime | YES | YES | YES | YES |
Date.setMilliseconds | YES | YES | YES | YES |
Date.setUTCMilliseconds | YES | YES | YES | YES |
Date.setSeconds | YES | YES | YES | YES |
Date.setUTCSeconds | YES | YES | YES | YES |
Date.setMinutes | YES | YES | YES | YES |
Date.setUTCMinutes | YES | YES | YES | YES |
Date.setHours | YES | YES | YES | YES |
Date.setUTCHours | YES | YES | YES | YES |
Date.setDate | YES | YES | YES | YES |
Date.setUTCDate | YES | YES | YES | YES |
Date.setMonth | YES | YES | YES | YES |
Date.setUTCMonth | YES | YES | YES | YES |
Date.setFullYear | YES | YES | YES | YES |
Date.setUTCFullYear | YES | YES | YES | YES |
Date.setYear | YES | YES | YES | YES |
Date.getYear | YES | YES | YES | YES |
Date.toJSON | YES | YES | YES | YES |
Date.toUTCString | YES | YES | YES | YES |
Date.toGMTString | YES | YES | YES | YES |
Date.constructor | YES | YES | YES | YES |
Error.toString | YES | YES | YES | YES |
Error.constructor | YES | YES | YES | YES |
Float32Array.constructor | YES | YES | YES | YES |
Float64Array.constructor | YES | YES | YES | YES |
Function.constructor | YES | YES | YES | YES |
Int16Array.constructor | YES | YES | YES | YES |
Int32Array.constructor | YES | YES | YES | YES |
Int8Array.constructor | YES | YES | YES | YES |
Map.forEach | YES | N/A | YES | YES |
Map.clear | YES | N/A | YES | YES |
Map.delete | YES | N/A | YES | YES |
Map.get | YES | N/A | YES | YES |
Map.has | YES | N/A | YES | YES |
Map.set | YES | N/A | YES | YES |
Map.keys | YES | N/A | YES | YES |
Map.values | YES | N/A | YES | YES |
Map.entries | YES | N/A | YES | YES |
Map.constructor | YES | N/A | YES | YES |
Math.abs | YES | YES | YES | YES |
Math.acos | YES | YES | YES | YES |
Math.asin | YES | YES | YES | YES |
Math.atan | YES | YES | YES | YES |
Math.acosh | YES | N/A | YES | YES |
Math.asinh | YES | N/A | YES | YES |
Math.atanh | YES | N/A | YES | YES |
Math.atan2 | YES | YES | YES | YES |
Math.cbrt | YES | N/A | YES | YES |
Math.ceil | YES | YES | YES | YES |
Math.clz32 | YES | N/A | YES | YES |
Math.cos | YES | YES | YES | YES |
Math.cosh | YES | N/A | YES | YES |
Math.exp | YES | YES | YES | YES |
Math.expm1 | YES | N/A | YES | YES |
Math.floor | YES | YES | YES | YES |
Math.fround | YES | N/A | YES | YES |
Math.hypot | YES | N/A | YES | YES |
Math.log | YES | YES | YES | YES |
Math.log10 | YES | N/A | YES | YES |
Math.log1p | YES | N/A | YES | YES |
Math.log2 | YES | N/A | YES | YES |
Math.max | YES | YES | YES | YES |
Math.min | YES | YES | YES | YES |
Math.pow | YES | YES | YES | YES |
Math.random | YES | YES | YES | YES |
Math.round | YES | YES | YES | YES |
Math.sign | YES | N/A | YES | YES |
Math.sin | YES | YES | YES | YES |
Math.sinh | YES | N/A | YES | YES |
Math.sqrt | YES | YES | YES | YES |
Math.tan | YES | YES | YES | YES |
Math.tanh | YES | N/A | YES | YES |
Math.trunc | YES | N/A | YES | YES |
Math.imul | YES | YES | YES | YES |
Object.toString | YES | YES | YES | YES |
Object.toLocaleString | YES | YES | YES | YES |
Object.valueOf | YES | YES | YES | YES |
Object.hasOwnProperty | YES | YES | YES | YES |
Object.propertyIsEnumerable | YES | YES | YES | YES |
Object.isPrototypeOf | YES | YES | YES | YES |
Object._defineGetter_ | YES | YES | YES | YES |
Object._defineSetter_ | YES | YES | YES | YES |
Object._lookupGetter_ | YES | YES | YES | YES |
Object._lookupSetter_ | YES | YES | YES | YES |
Object.constructor | YES | YES | YES | YES |
Promise.then | YES | YES | YES | YES |
Promise.catch | YES | YES | YES | YES |
Promise.constructor | YES | YES | YES | YES |
RegExp.compile | YES | YES | YES | YES |
RegExp.exec | YES | YES | YES | YES |
RegExp.toString | YES | YES | YES | YES |
RegExp.test | YES | YES | YES | YES |
RegExp.constructor | YES | YES | YES | YES |
Set.forEach | YES | N/A | YES | YES |
Set.add | YES | N/A | YES | YES |
Set.clear | YES | N/A | YES | YES |
Set.delete | YES | N/A | YES | YES |
Set.has | YES | N/A | YES | YES |
Set.entries | YES | N/A | YES | YES |
Set.values | YES | N/A | YES | YES |
Set.keys | YES | N/A | YES | YES |
Set.constructor | YES | N/A | YES | YES |
String.match | YES | YES | YES | YES |
String.padStart | YES | N/A | N/A | N/A |
String.padEnd | YES | N/A | N/A | N/A |
String.repeat | YES | N/A | YES | YES |
String.replace | YES | YES | YES | YES |
String.search | YES | YES | YES | YES |
String.split | YES | YES | YES | YES |
String.toString | YES | YES | YES | YES |
String.valueOf | YES | YES | YES | YES |
String.charAt | YES | YES | YES | YES |
String.charCodeAt | YES | YES | YES | YES |
String.codePointAt | YES | N/A | YES | YES |
String.concat | YES | YES | YES | YES |
String.indexOf | YES | YES | YES | YES |
String.lastIndexOf | YES | YES | YES | YES |
String.slice | YES | YES | YES | YES |
String.substr | YES | YES | YES | YES |
String.substring | YES | YES | YES | YES |
String.toLowerCase | YES | YES | YES | YES |
String.toUpperCase | YES | YES | YES | YES |
String.localeCompare | YES | YES | YES | YES |
String.toLocaleLowerCase | YES | YES | YES | YES |
String.toLocaleUpperCase | YES | YES | YES | YES |
String.big | YES | YES | YES | YES |
String.small | YES | YES | YES | YES |
String.blink | YES | YES | YES | YES |
String.bold | YES | YES | YES | YES |
String.fixed | YES | YES | YES | YES |
String.italics | YES | YES | YES | YES |
String.strike | YES | YES | YES | YES |
String.sub | YES | YES | YES | YES |
String.sup | YES | YES | YES | YES |
String.fontcolor | YES | YES | YES | YES |
String.fontsize | YES | YES | YES | YES |
String.anchor | YES | YES | YES | YES |
String.link | YES | YES | YES | YES |
String.trim | YES | YES | YES | YES |
String.trimLeft | YES | YES | YES | YES |
String.trimRight | YES | YES | YES | YES |
String.startsWith | YES | N/A | YES | YES |
String.endsWith | YES | N/A | YES | YES |
String.includes | YES | N/A | YES | YES |
String.normalize | YES | YES | YES | YES |
String.constructor | YES | YES | YES | YES |
Symbol.toString | YES | N/A | YES | YES |
Symbol.valueOf | YES | N/A | N/A | YES |
Symbol.constructor | YES | N/A | YES | YES |
TypeError.toString | YES | N/A | N/A | YES |
TypeError.constructor | YES | YES | YES | YES |
Uint16Array.constructor | YES | YES | YES | YES |
Uint32Array.constructor | YES | YES | YES | YES |
Uint8Array.constructor | YES | YES | YES | YES |
Uint8ClampedArray.constructor | YES | YES | YES | YES |
WeakMap.delete | YES | YES | YES | YES |
WeakMap.get | YES | YES | YES | YES |
WeakMap.has | YES | YES | YES | YES |
WeakMap.set | YES | YES | YES | YES |
WeakMap.constructor | YES | YES | YES | YES |
N/A 表示这个标准库方法在平台上不支持
Q: 这些数据是怎么来的,靠谱吗?
A: 这些数据是在真实小程序运行环境下运行,然后把 API 支持情况发送到服务器后台,再写个脚本把数据整理汇总后得来的。
Q: 其他平台,比如 Android 5.0 的支持情况怎么样?
A: 由于条件限制,手上没有 Android 5.0 的手机,有愿意配合收集数据的,私信留言。配合的方法很简单,用指定型号的手机打开一个微信小程序,按一个按钮即可。
Q: 为什么不使用 lodash 之类效率更高的库,而使用的标准库?
A: 使用 lodash 之类的确实效率更高,兼容性也更好。基于两个原因没有使用,一是 lodash 太大,而微信小程序限制在 1MB 以内。当然,可以用 lodash 模块化的版本来解决,但还有第二个原因,即 lodash 的一些 API 也有兼容性问题,比如我试过 lodash.findIndex 这个包,结果在 Android 6.0.1 上也无法成功运行 (这一点未做深入验证,感兴趣的同学可以验证一下)。
总结
从后台数据来看,小程序刚发布的前三天,确实带来了非常可观的流量红利,但这部分偿鲜的用户,很快就消失了。三天过后,基本上保持了平衡的访问量。流量红利和广告一样,是催化剂,真正有价值的还是要做用户需要的产品。
在此顺手安利一下开发的两个小程序 360好书推荐 和 51经典电影,偶尔想用的时候打开,可能会偶遇一些小惊喜。但坦白讲,这两个小程序都和微信倡导的小程序价值观不符。微信还是希望通过小程序把线下低频的,服务成本高(这里应该主要是时间成本,即便利性)的场景,转化为线上快捷的使用方式。