背景
近半年,已產生幾起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高級語法特性都能很好的支持; - 可以對代碼做壓縮、混淆處理,減小文件大小,提高靜態文件加載性能,提高源代碼安全性;
方案的缺點:
- 在構建之前需要對源文件進行改造,工作量比較大,容易出錯;
- 需要引入構建工具,添加構建配置,可能還需要修改
JenkinsfileCI腳本;
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() 方法將會被該插件告警:
該方案的優點:
- 配置比較簡單,不用改造腳本代碼,不用修改
JenkinsfileCI腳本;
該方案的缺點:
- 源代碼直接在控制枱能看到,不安全;
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同學反饋,謝謝支持與配合!