背景
近半年,已产生几起FreeMarker
项目(后面统一简称FM
项目)在IE
浏览器或者360浏览器兼容模式环境下下因使用 ES6+
高级语法特性而运行出错的线上问题,导致业务流程无法执行下去。虽然一直在强调开发同学在做FM
项目的需求时不要使用ES6
,但是口头上的的团队公约约束性不强,加上开发同学早已习惯性使用ES6
,使之问题层出不穷,另外,还有些Web Apis
和样式在IE
上存在兼容性问题(比如:Element.scrollIntoView()
、Element.scrollTo()
),这些API
和样式的使用也需要强制禁用,因此亟需用工具在编程阶段约束。
因使用ES6
或者使用兼容性较差的Web Apis
导致的缺陷:
因使用ES6
或者使用兼容性较差的Web Apis
导致的线上问题(都是三级事件):
另外,因jira
单备注不够清晰,样式兼容性导致的问题未作统计。
方案
下面方案只针对ES6+
语法特性和兼容性较差的Web Apis
的处理,对于样式兼容性处理另作方案。
Babel
转译
在webpack
(使用其他构建工具也可以,比如 gulp
)中引入babel
将ES6
语法转译成ES5
,但是在构建之前需要将源文件做如下改造:
var managePage = {};
function debounc() {}
改造后:
window.managePage = {}
window.debounc = function debounc() {}
为什么不设置libraryTarget: 'window'
以全局变量输出?
因为libraryTarget: 'window'
的打包方式只能将export
导出的对象以全局对象的方式输出,需将源代码做如下改造,改造后在线上运行不会有问题,在本地启动项目会报“xxx is not defined”
,因为export
包裹后的变量会变成了局部变量。
export var managePage = {};
export function debounc() {}
babel-polyfill
babel
默认只转换 js
语法,而不转换新的 API
,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise
等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign
)都不会转码。
举例来说,es2015
在 Array
对象上新增了 Array.from
方法。babel
就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill
。(内部集成了 core-js
和 regenerator
)
使用时,在所有代码运行之前增加 require('babel-polyfill')
。或者更常规的操作是在 webpack.config.js
中将 babel-polyfill
作为第一个 entry
。因此必须把 babel-polyfill
作为 dependencies
而不是 devDependencies
babel-polyfill
主要有两个缺点:
- 使用
babel-polyfill
会导致打出来的包非常大,因为babel-polyfill
是一个整体,把所有方法都加到原型链上。比如我们只使用了Array.from
,但它把Object.defineProperty
也给加上了,这就是一种浪费了。这个问题可以通过单独使用core-js
的某个类库来解决,core-js
都是分开的。 babel-polyfill
会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。
因此在实际使用中,如果我们无法忍受这两个缺点(尤其是第二个),通常我们会倾向于使用 babel-plugin-transform-runtime
。
但如果代码中包含高版本 js
中类型的实例方法 (例如 [1,2,3].includes(1)
),这还是要使用 polyfill
。下图是官网描述:
方案的优点:
- 任何
JavaScript
高级语法特性都能很好的支持; - 可以对代码做压缩、混淆处理,减小文件大小,提高静态文件加载性能,提高源代码安全性;
方案的缺点:
- 在构建之前需要对源文件进行改造,工作量比较大,容易出错;
- 需要引入构建工具,添加构建配置,可能还需要修改
Jenkinsfile
CI
脚本;
ESlint警告
ESlint
是一款插件化的javascript
代码检测工具,我们可以利用其在FM
项目脚本中是否使用了ES6+
语法,如果脚本中有使用,立即报错处理,并提示哪些关键字的使用属于ES6
语法。另外,为防止开发同学强推使用了ES6+
语法的代码,在git hooks
的pre-commit
钩子中执行eslint
命令校验,若通过,则代码成功commit
;若不通过,控制台会打印错误日志。
注意:请开发同学在commit
时不要添加--no-verify
参数,否则会跳过校验
具体落地流程:
(1)在VScode
编辑器中安装ESlint
拓展插件,并启用
(2)在项目根目录或者需要检视的项目下手动或者执行npx eslint --init
创建.eslintrc.json
配置文件,并添加如下配置项:
{
"root": true,
"env": {
"browser": true,
"jquery": true
},
"parserOptions": {
"ecmaVersion": 5,
"sourceType": "script"
},
"rules": {
}
}
(3)package.json
配置目标环境
{
"browserslist": [
"defaults",
"not ie <= 8"
]
}
(4)因为管理中心不需要支持IE
,因此关于此端项目的js
不需要ESlint
检视,我们在根目录下创建.eslintignore
配置文件,向其中添加需要跳过检视的文件路径,比如src/main/webapp/static/js/adjustPage.js
不需要检视:
// .eslintignore
src/main/webapp/static/js/adjustPage.js
(5)在根目录执行npm init
生成package.json
,安装lint-staged、husky
,当执行git commit
提交代码之前触发precommit
钩子进行eslint
校验
- 安装
lint-staged、husky
$ yarn add lint-staged husky -D
- 生成
.husky
文件夹以及pre-commit
钩子脚本
$ npx husky install // 生成.husky文件夹
$ npx husky add .husky/pre-commit 'npx lint-staged' // 添加pre-commit钩子到.husky中
注意:因为这里我用了npx
,需要大家全局安装npm i npx -g
- 配置
package.json
// 新增如下配置
{
"lint-staged": {
"*.js": [
"eslint --ignore-path .gitignore"
]
}
}
此时,执行git commit -m 'xxx'
后检视提交的js
脚本,但是git push
时会报错没有change-id
,因为通过gerrit code review
需要change-id
,执行下述命令即可解决:
scp -p -P 29418 a02313@gerrit.casstime.net:hooks/commit-msg .husky
注意:a02313
账号部分改成自己的
执行效果图:
但是上述配置只能检视到一些ES6
语法,对于fetch、includes
等不支持IE
浏览器的api
是检视不到的,你就需要考虑禁止使用 fetch()
等API
。在开发阶段,人工保证 API
的兼容性是不可靠的,更可靠的方式是借助工具来自动化扫描,例如下面要介绍的 eslint-plugin-compat
。
使用 eslint-plugin-compat
eslint-plugin-compat
是 ESLint
的一个插件,由前 uber
工程师 Amila Welihinda
开发。它可以帮助发现代码中的不兼容 API
。
下面介绍如何在工程中接入 eslint-plugin-compat
。
(1)安装 eslint-plugin-compat
安装 eslint-plugin-compat
和安装其他 ESLint
插件类似:
$ npm install eslint-plugin-compat --save-dev
(2)修改 ESLint
配置
之后,我们需要修改 ESLint
的配置,加上该插件的使用:
// .eslintrc.json
{
"extends": ["eslint:recommended", "plugin:compat/recommended"],
"rules": {
//...
},
"env": {
"browser": true
// ...
},
// ...
}
(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-compat 、babel、Autoprefixer
等。下面我们来详细了解一下 browserslist
的描述规范。
browserslist
支持指定目标浏览器类型,并且能够灵活组合多种指定条件。
测试效果
完成了 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()
方法不能被 IE
兼容:
为了避免漏报这种问题,我们可以结合另一个兼容检查插件 eslint-plugin-builtin-compat
。该插件同样借助 mdn-browser-compat-data
来进行兼容扫描,与 eslint-plugin-compat
不同的是,该插件不会放过实例对象,因此它会把所有 foo.includes
的 includes
方法当成是 Array.prototype.includes()
方法来扫描。可想而知,这个插件可能会导致误报。因此建议将其告警级别改为 warning
级别。
(1)安装 eslint-plugin-builtin-compat
$ npm install eslint-plugin-builtin-compat --save-dev
(2)修改 ESLint
配置
与 eslint-plugin-compat
类似,我们可以修改 ESLint
的配置,加上该插件的使用。但由于该插件容易误报,因此只建议将其告警级别改为 warning
级别:
// .eslintrc.json
{
"extends": ["eslint:recommended", "plugin:compat/recommended"],
"plugins": [
"builtin-compat"
],
"rules": {
//...
"builtin-compat/no-incompatible-builtins": 1
},
"env": {
"browser": true
// ...
},
// ...
}
加入该插件后,可以发现 Array.prototype.includes()
方法将会被该插件告警:
该方案的优点:
- 配置比较简单,不用改造脚本代码,不用修改
Jenkinsfile
CI
脚本;
该方案的缺点:
- 源代码直接在控制台能看到,不安全;
IDEA
中引入ESlint
实际需求中,后端同学可能会负责FM
项目的部分前端需求,因此还需要满足IDEA
中ESlint
的使用。
(1)查看本地有没有安装node
环境和全局安装eslint
,如果没有就需要安装,安装之后重启IDEA
,然后勾选Enable
启动ESlint
检视
(2)如果重启IDEA
后启动eslint
服务时报如下错误,这是因为IEAD
中plugins/JavaScriptLanguage/languageService/eslint
版本过低,存在插件兼容问题
解决方法,照如下修改plugins/JavaScriptLanguage/languageService/eslint/bin/eslint-plugin.js
文件,然后重启IDEA
//this.cliEngine = require(this.basicPath + "lib/cli-engine");
this.cliEngine = require(this.basicPath + "lib/cli-engine").CLIEngine;
正常效果:
综上所述,如果已有项目组在FM
项目中已经引入构建工具,比如备货组的商城页面已经引入多种语言开发,再使用构建工具打包,此类项目不用考虑ES6+
语法问题,其他场景下的FM
项目可以选择使用第二种方案。本文的方案作为抛砖引玉,大家具体场景具体分析。
组织实施
问题反馈与建议
一切使用问题或者更好的建议都可以向XX
同学反馈,谢谢支持与配合!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。