本文从Cocos Creator 开发的角度登程,认真探讨了关注 JavaScript API 兼容性的必要性,以及如何借助工具和 Polyfill 来躲避 Cocos Creator 我的项目的兼容性问题。

一、引言:JavaScript虚拟机的差异性

不同的浏览器和挪动设施所应用的 JavaScript 虚拟机(VM)千差万别,所反对的 API 也天壤之别。

咱们来理解一下 Cocos Creator 在各个端所应用的 JavaScript VM

  • 对于 iOS 客户端和 Mac 客户端:在 Cocos Creator 1.6 及以前,Cocos Creator 始终是应用非零碎原生的 SpiderMonkey 作为 JS VM ;从 1.7 开始,Cocos Creator 引入了 JSB 2.0 ,反对了 V8、JavaScriptCore 等多种 JS VM 。于是 Cocos Creator 便将 iOS 端和 Mac 端的 JS VM 都改为了零碎自带的 JavaScriptCore ,以达到节俭包体的目标;到了 2.1.3Cocos Creator 又将 Mac 端的 JS VM 切换到了 V8,以晋升利用性能。
  • 对于 Android 客户端和 Windows 客户端:在 Cocos Creator 1.6 及以前,Cocos Creator 同样是应用 SpiderMonkey 作为 JS VM ;从 1.7 开始,得益于 JSB 2.0V8 成了 AndroidWindows 客户端的 JS VM
  • 对于 Web 端:应用浏览器自身的JavaScript VM 来解析 JavaScript 代码。

从上可见,因为 JS VM 不同,同一份代码在不同的平台上运行可能会有很大差别。为了让咱们的产品可能给尽可能多的用户应用,咱们在开发阶段就须要时刻留神 JavaScriptAPI 兼容性。

举个例子:fetch() 办法是一个用来取代 XMLHTTPRequest 的 API。相比后者,它的长处在于可读性更高,且能够很不便地应用 Promise 写出更优雅的代码。

fetch(    'http://domain/service',    { method: 'GET' }).then( response => response.json() ).then( json => console.log(json) ).catch( error => console.error('error:', error) );

然而,fetch() 办法不反对所有的 IE 浏览器,也无奈在 2017 年以前的 Chrome、Firefox Safari 版本上运行。当你的用户有很大一部分是上述的用户时,你就须要思考禁止应用 fetch() API ,而从新回到 XMLHTTPRequest 的怀抱。

在开发阶段,人工保障 API 的兼容性是不牢靠的。更牢靠的形式是借助工具来自动化扫描。例如上面要介绍的 eslint-plugin-compat

二、应用 eslint-plugin-compat

eslint-plugin-compatESLint 的一个插件,由前 uber 工程师 Amila Welihinda 开发。它能够帮忙发现代码中的不兼容 API

上面介绍如何在工程中接入 eslint-plugin-compat

2.1 装置 eslint-plugin-compat

装置 eslint-plugin-compat 和装置其余 ESLint 插件相似:

$ npm install eslint-plugin-compat --save-dev

还能够顺便把依赖的 browserslistcaniuse-lite 一起装置了:

$ npm install browserslist caniuse-lite --save-dev

2.2 批改 ESLint 配置

之后,咱们须要批改 ESLint 的配置,加上该插件的应用:

// .eslintrc.json{  "extends": "eslint:recommended",  "plugins": [    "compat"  ],  "rules": {    //...    "compat/compat": 2  },  "env": {    "browser": true    // ...  },    // ...}

2.3 配置指标运行环境

通过在 package.json 中减少 browserslist 字段来配置指标运行环境。示例:

{  // ...  "browserslist": ["chrome 70", "last 1 versions", "not ie <= 8"]}

下面的值示意 Chrome 版本 70 以上,或每种浏览器的最近一个版本,或者非 ie 8 及以下。这里的填写格局是遵循 browserslist (https://github.com/browsersli... )所定义的一套形容标准。browserslist 是一套形容产品指标运行环境的工具,它被宽泛用在各种波及浏览器/挪动端的兼容性反对工具中,例如 eslint-plugin-compatbabel、Autoprefixer 等。上面咱们来具体理解一下 browserslist 的形容标准。

browserslist 反对指定指标浏览器类型,并且可能灵便组合多种指定条件。

指定指标浏览器类型

browserslist 收录了如下一些浏览器,能够在条件中应用(留神大小写敏感):

  • Android:用于 Android WebView
  • Baidu:用于百度浏览器。
  • BlackBerrybb:用于黑莓浏览器。
  • Chrome:用于 Google Chrome
  • ChromeAndroidand_chr:用于 Android Chrome
  • Edge:用于 Microsoft Edge
  • Electron:用于 Electron framework。 将会被转换成 Chrome 版本。
  • Explorerie:用于 Internet Explorer
  • ExplorerMobileie_mob:用于 Internet Explorer Mobile
  • Firefoxff:用于 Mozilla Firefox
  • FirefoxAndroidand_ff:用于 Android Firefox
  • iOSios_saf:用于 iOS Safari
  • Node:用于 Node.js
  • Opera:用于 Opera
  • OperaMiniop_mini:用于 Opera Mini
  • OperaMobileop_mob:用于 Opera Mobile
  • QQAndroidand_qq:用于 Android QQ 浏览器。
  • Safari:用于 desktop Safari。
  • Samsung:用于 Samsung Internet。
  • UCAndroidand_uc:用于 Android UC 浏览器。
  • kaios:用于 KaiOS 浏览器。

browseslist 的条件语法

browserslist 反对非常灵活的条件语法,上面给出一些例子作为参考(留神大小写敏感),供读者们触类旁通。

  • > 5%:示意要兼容寰球用户统计比例 > 5% 的浏览器版本。>=<<= 也都是可用的。
  • > 5% in US:示意要兼容美国用户统计比例 > 5% 的浏览器版本。这里的 US 是美国的 Alpha-2 编码 1。也能够换成其余国家/地区的 Alpha-2 编码。例如,中国就是 CN
  • > 5% in alt-AS:示意要兼容亚洲用户统计比例 > 5% 的浏览器版本。这里的 alt-AS 示意亚洲地区1
  • > 5% in my stats:示意要兼容自定义的用户统计比例 > 5% 的浏览器版本。
  • cover 99.5%:示意要兼容用户份额累计前 99.5% 的浏览器版本。
  • cover 99.5% in US:同上,但通过 Alpha-2 编码来加上国家/地区的限定。
  • cover 99.5% in my stats:应用用户的数据。
  • maintained node versions:所有官网还在保护的 Node.js 版本。
  • current node:Browserslist 当初正在应用的 Node.js 版本。
  • extends browserslist-config-mycompany:示意要兼容 browserslist-config-mycompany 这个 npm 包的查问后果。
  • ie 6-8:示意要兼容 IE 6 ~ IE 8 的版本(即 IE 6、IE 7 和 IE 8)。
  • Firefox > 20:示意要兼容 > 20 的 Firefox 版本。>=<<= 也都是可用的。
  • iOS 7:示意要兼容 iOS 7
  • Firefox ESR:示意要兼容最新的 Firefox ESR 版本。
  • PhantomJS 2.1 and PhantomJS 1.9:示意要兼容 PhantomJS 2.1 和 1.9 版本。
  • unreleased versionsunreleased Chrome versions:示意要兼容未公布的开发版本。后者则具体指明是要兼容未公布的 Chrome 版本。
  • last 2 major versionslast 2 iOS major versions:示意要兼容最近两个次要版本所蕴含的所有小版本。后者则具体指明是要兼容 iOS 的最近两个次要版本所蕴含的所有小版本。
  • since 2015last 2 years:自 2015 年或最近两年到当初所公布的所有版本。
  • dead:官网不再保护或者超过两年没有更新的浏览器版本。
  • last 2 versions:每种浏览器的最近两个版本。
  • last 2 Chrome versionsChrome 浏览器的最近两个版本。
  • defaultsBrowserslist 的默认规定(> 0.5%, last 2 versions, Firefox ESR, not dead)。
  • not ie <= 8:从后面的条件中再排除掉低于或者等于 IE 8 的浏览器。

在浏览这些规定的时候,举荐拜访 http://browsersl.ist 输出雷同的命令进行测试,能够间接得出符合条件的浏览器版本。

browserlist 上测试条件

仔细的读者可能会发现最初一条的查问后果会报错,这是因为 not 操作须要放在一个查问条件之后(下文会介绍)。你能够从其余规定中随便挑一条规定来组合,例如 ie 6-10, not ie <= 8 将会筛出 IE 9 和 IE 10 。

browseslist 的条件组合

browserslist 反对多种条件的组合,上面咱们来理解 browseslist 的条件组合办法。

  • ,or 都能够用来示意逻辑 “或”。例如,last 1 version or > 1%last 1 version, > 1% 等价,都示意每种浏览器的最近 1 个版本,或者 > 1% 的市场份额。“或” 操作相当于集合论中的并集。
  • and 用来示意逻辑 “与”。例如,last 1 version and > 1% 示意每种浏览器的最近一个版本,且 > 1% 的市场份额。“与” 操作相当于集合论中的交加。
  • not 用来示意逻辑 “非”。例如 > .5% and not ie <= 8 示意 > 1% 的市场份额且排除 ie 8 及以下的版本。“非” 操作相当于集合论外头的补集,所以 not 不能作为第一个条件,因为你总须要晓得“补”的是什么的“集”。

三种条件组合类型能够用上面的表格来示意:

条件组合类型示意图示例
or/, 组合 (并集)> .5% or last 2 versions > .5%, last 2 versions
and 组合 (交加)> .5% and last 2 versions
not 组合 (补集)> .5% and not last 2 versions > .5% or not last 2 versions > .5%, not last 2 versions

配置你的 browserslist

理解了以上规定后,咱们能够来配置实用于咱们的工程的 browserslist

举个例子:如果咱们的我的项目心愿在 iOS 8 及以上,或者版本号 49 及以上且市场份额大于 0.2% 的 Chrome 桌面浏览器运行,那么能够应用如下的规定:

// ..."browserslist": [  ">.2% and chrome >= 49",  "iOS >= 8"],

实现后,能够应用 npx browserslist 来测试你配置的 browserslist

$ npx browserslistchrome 78chrome 77chrome 76chrome 75chrome 74chrome 73chrome 72chrome 63chrome 49ios_saf 13.0-13.2ios_saf 12.2-12.4ios_saf 12.0-12.1ios_saf 11.3-11.4ios_saf 11.0-11.2ios_saf 10.3ios_saf 10.0-10.2ios_saf 9.3ios_saf 9.0-9.2ios_saf 8.1-8.4ios_saf 8

也能够拜访 https://browsersl.ist/ 上输出条件测试后果。

测试成果

实现了 browserslist 规定的配置后,咱们就能够联合 ESLint 扫描工程中的 API 兼容问题。同时 VS Code 插件也能够即时提醒不兼容的 API 调用。

三、应用 eslint-plugin-builtin-compat

eslint-plugin-compat 的原理是针对确认的类型和属性,应用 caniuse (http://caniuse.com) 的数据集 caniuse-db 以及 MDN(https://developer.mozilla.org... )的数据集 mdn-browser-compat-data 里的数据来确认 API 的兼容性。但对于不确定的实例对象,因为难以判断该实例的办法的兼容性,为了防止误报,eslint-plugin-compat 抉择了跳过这类 API 的查看。

例如,foo.includes 在不确定 foo 是否为数组类型的时候,就无奈判断 includes 办法的兼容性。在下图中,咱们在应用下面的 browserslint 配置的状况下,includes 办法的兼容问题并没有被扫描进去:

然而,从 caniuse 上能够查知,Array.prototype.includes() 办法不能被 iOS 8 兼容:

实际上,Cocos Creator 的 engine 我的项目自 2.1.3 版本开始,就曾经针对 Array.prototype.includes() 办法退出了 Polyfill ,从而彻底躲避了该 API 的兼容问题。在本节前面介绍 Polyfill 的时候咱们将介绍如何防止该 API 的误报。

为了防止漏报这种问题,咱们能够联合另一个兼容查看插件 eslint-plugin-builtin-compat 。该插件同样借助 mdn-browser-compat-data 来进行兼容扫描,与 eslint-plugin-compat 不同的是,该插件不会放过实例对象,因而它会把所有 foo.includesincludes 办法当成是 Array.prototype.includes() 办法来扫描。可想而知,这个插件可能会导致误报。因而倡议将其告警级别改为 warning 级别。

3.1 装置 eslint-plugin-builtin-compat

$ npm install eslint-plugin-builtin-compat --save-dev

3.2 批改 ESLint 配置

eslint-plugin-compat 相似,咱们能够批改 ESLint 的配置,加上该插件的应用。但因为该插件容易误报,因而只倡议将其告警级别改为 warning 级别:

// .eslintrc.json{  "extends": "eslint:recommended",  "plugins": [    "compat",    "builtin-compat"  ],  "rules": {    //...    "compat/compat": 2,    "builtin-compat/no-incompatible-builtins": 1  },  "env": {    "browser": true    // ...  },    // ...}

退出该插件后,能够发现 Array.prototype.includes() 办法将会被该插件告警:

四、应用 Polyfill 解决兼容问题

ESLint 在开发阶段扫描出 API 兼容问题诚然是一种防治兼容性问题的伎俩,但如果团队里的共事并不认真留神 ESLint 的扫描后果,甚至没有将 ESLint 作为代码合入扫描的一环的话,就有可能会有漏网之鱼持续肆虐。

因而,一种更为一劳永逸的办法是为一些罕用的 API 补上相应 Polyfill 。这样一方面能够为不兼容的浏览器版本增加上反对,另一方面又能够使得团队成员安心地应用新的 API ,进步开发效率。

4.1 Cocos Creator engine 里的 Polyfill

实际上,Cocos Creatorengine 我的项目也内置了很多常见 API Polyfill

其中就包含了 Array.prototype.includes()

因而,如果应用 2.1.3 以上版本的 Cocos Creator 构建带有 Array.prototype.includes() 办法的工程,编译进去的利用将能够顺利在 iOS 8 机器上运行。这是因为 Array.prototype.includes() 在构建时被对立被 “翻译” 成了 engine 我的项目里提供的办法。

相应地,为了防止 Polyfill 里的 isArrayfindincludesAPI eslint-plugin-builtin-compat 误报,能够在 .eslintrc 中将这些 API 退出该插件的排除列表中:

// .eslintrc.json{  "extends": "eslint:recommended",  "plugins": [    "compat",    "builtin-compat"  ],  // ...  "settings": {    "builtin-compat-ignore": ["ArrayBuffer", "find", "log2", "parseFloat", "parseInt", "assign", "values", "trimLeft", "startsWith", "endsWith", "repeat"]  }  // ...}

4.2 自行减少 Polyfill

engine 我的项目里的 Polyfill 并不能笼罩所有的 API 。如果你心愿应用的某个不兼容 API 并没有蕴含在 engine 我的项目中,那么就得思考给你本人的我的项目补上该 APIPolyfill

例如,string.prototype.padStart()string.prototype.padEnd() 两个 API 别离提供了用于字符串的头部和尾部补全的便当办法:

'x'.padStart(5, 'ab')  // 'ababx''x'.padStart(4, 'ab')  // 'abax''x'.padEnd(5, 'ab')  // 'xabab''x'.padEnd(4, 'ab')  // 'xaba'

而这两个办法只在 iOS 10 及以上版本才被反对:

寻找 Polyfill

如何寻找这两个办法的 Polyfill 呢?一个最权威的起源就是 MDN 站点(https://developer.mozilla.org... )。以 string.prototype.padStart() 为例,咱们能够在站点右上角的搜寻框中输出 padStart

之后敲回车进入搜寻,在搜寻后果中点击最匹配的后果:

就进入了 string-prototype-padStart 的文档页,在左侧的导航栏中能够看到有 Polyfill 的栏目:

点击它即可跳转到对应的 Polyfill 实现:

!

编写自定义的 Polyfill 脚本

找到了 string.prototype.padStart()string.prototype.padEnd() 两个 APIPolyfill 后,咱们在本人的工程中编写一个自定义的 Polyfill 脚本。例如叫做 ABCPolyfill.js

/** * ABCPolyfill.js * 补一些 polyfill,解决若干兼容问题 */var ABCPolyfill = function () {    console.log('ABC polyfill');    if (!String.prototype.padStart) {        String.prototype.padStart = function padStart(targetLength, padString) {            targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;            padString = String(typeof padString !== 'undefined' ? padString : ' ');            if (this.length >= targetLength) {                return String(this);            } else {                targetLength = targetLength - this.length;                if (targetLength > padString.length) {                    padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed                }                return padString.slice(0, targetLength) + String(this);            }        };    }    if (!String.prototype.padEnd) {        String.prototype.padEnd = function padEnd(targetLength,padString) {            targetLength = targetLength>>0; //floor if number or convert non-number to 0;            padString = String((typeof padString !== 'undefined' ? padString : ' '));            if (this.length > targetLength) {                return String(this);            }            else {                targetLength = targetLength-this.length;                if (targetLength > padString.length) {                    padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed                }                return String(this) + padString.slice(0,targetLength);            }        };    }};module.exports.ABCPolyfill = ABCPolyfill;

接下来,咱们要在利用启动后加载执行这个 Polyfill 脚本里的 ABCPolyfill() 办法,主动打上这两个 APIPolyfill 。咱们能够再编写一个利用初始化脚本,例如叫做 ABCInit.js ,该脚本用于在利用初始化时执行一些指定工作。

/** * ABCInit.js * 利用启动时的一些初始化工作 */import ABCPolyfill from 'ABCPolyfill';// 初始化操作function doInit() {  ABCPolyfill.ABCPolyfill();}(function () {    doInit();})();

之后能够在你的工程的初始场景里脚本组件中援用该脚本即可失效:

/** * 工程的初始场景挂载的脚本组件 */require('ABCInit');// ...

为了防止 eslint-plugin-builtin-compat 误报,能够将 padStartpadEnd 也追加进排除名单中:

// .eslintrc.json{  "extends": "eslint:recommended",  "plugins": [    "compat",    "builtin-compat"  ],  // ...  "settings": {    "builtin-compat-ignore": ["ArrayBuffer", "find", "log2", "parseFloat", "parseInt", "assign", "values", "trimLeft", "startsWith", "endsWith", "repeat", "padStart", "padEnd"]  }  // ...}

五、小结

  1. 时刻留神 API 兼容性;
  2. 应用 eslint-plugin-compat 查看动态类型的不兼容 API ,并将告警级别设为谬误;
  3. 应用 eslint-plugin-builtin-compat 查看动静类型的不兼容 API,并将告警级别设为正告;
  4. 思考为不兼容 API 减少 Polyfill

  1. 1 ↩