乐趣区

关于兼容性:Cocos-Creator-最佳实践JavaScript兼容性问题规避

本文从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 browserslist
chrome 78
chrome 77
chrome 76
chrome 75
chrome 74
chrome 73
chrome 72
chrome 63
chrome 49
ios_saf 13.0-13.2
ios_saf 12.2-12.4
ios_saf 12.0-12.1
ios_saf 11.3-11.4
ios_saf 11.0-11.2
ios_saf 10.3
ios_saf 10.0-10.2
ios_saf 9.3
ios_saf 9.0-9.2
ios_saf 8.1-8.4
ios_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 ↩
退出移动版