1
头图

背景

近半年,已产生几起FreeMarker项目(后面统一简称FM项目)在IE浏览器或者360浏览器兼容模式环境下下因使用 ES6+ 高级语法特性而运行出错的线上问题,导致业务流程无法执行下去。虽然一直在强调开发同学在做FM项目的需求时不要使用ES6,但是口头上的的团队公约约束性不强,加上开发同学早已习惯性使用ES6,使之问题层出不穷,另外,还有些Web Apis和样式在IE上存在兼容性问题(比如:Element.scrollIntoView()Element.scrollTo()),这些API和样式的使用也需要强制禁用,因此亟需用工具在编程阶段约束。

因使用ES6或者使用兼容性较差的Web Apis导致的缺陷:

image-20210812142554665.png

因使用ES6或者使用兼容性较差的Web Apis导致的线上问题(都是三级事件):

image-20210812143136040.png

另外,因jira单备注不够清晰,样式兼容性导致的问题未作统计。

方案

下面方案只针对ES6+语法特性和兼容性较差的Web Apis的处理,对于样式兼容性处理另作方案。

Babel转译

webpack(使用其他构建工具也可以,比如 gulp)中引入babelES6语法转译成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)都不会转码。

举例来说,es2015Array 对象上新增了 Array.from 方法。babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill。(内部集成了 core-jsregenerator)

使用时,在所有代码运行之前增加 require('babel-polyfill')。或者更常规的操作是在 webpack.config.js 中将 babel-polyfill 作为第一个 entry。因此必须把 babel-polyfill 作为 dependencies 而不是 devDependencies

babel-polyfill 主要有两个缺点:

  1. 使用 babel-polyfill 会导致打出来的包非常大,因为 babel-polyfill 是一个整体,把所有方法都加到原型链上。比如我们只使用了 Array.from,但它把 Object.defineProperty 也给加上了,这就是一种浪费了。这个问题可以通过单独使用 core-js 的某个类库来解决,core-js 都是分开的。
  2. babel-polyfill 会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。

因此在实际使用中,如果我们无法忍受这两个缺点(尤其是第二个),通常我们会倾向于使用 babel-plugin-transform-runtime

但如果代码中包含高版本 js 中类型的实例方法 (例如 [1,2,3].includes(1)),这还是要使用 polyfill。下图是官网描述:

image-20210817145550319.png

方案的优点:

  • 任何JavaScript高级语法特性都能很好的支持;
  • 可以对代码做压缩、混淆处理,减小文件大小,提高静态文件加载性能,提高源代码安全性;

方案的缺点:

  • 在构建之前需要对源文件进行改造,工作量比较大,容易出错;
  • 需要引入构建工具,添加构建配置,可能还需要修改Jenkinsfile CI脚本;

ESlint警告

ESlint是一款插件化的javascript代码检测工具,我们可以利用其在FM项目脚本中是否使用了ES6+语法,如果脚本中有使用,立即报错处理,并提示哪些关键字的使用属于ES6语法。另外,为防止开发同学强推使用了ES6+语法的代码,在git hookspre-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账号部分改成自己的

执行效果图:

image-20210813113825777.png

但是上述配置只能检视到一些ES6语法,对于fetch、includes等不支持IE浏览器的api是检视不到的,你就需要考虑禁止使用 fetch()API。在开发阶段,人工保证 API 的兼容性是不可靠的,更可靠的方式是借助工具来自动化扫描,例如下面要介绍的 eslint-plugin-compat

使用 eslint-plugin-compat

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

api-compat-1.png

下面介绍如何在工程中接入 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 调用。

image-20210818093845893.png

使用 eslint-plugin-builtin-compat

eslint-plugin-compat 的原理是针对确认的类型和属性,使用 caniuse (http://caniuse.com) 的数据集 caniuse-db 以及 MDNhttps://developer.mozilla.org... )的数据集 mdn-browser-compat-data 里的数据来确认 API 的兼容性。但对于不确定的实例对象,由于难以判断该实例的方法的兼容性,为了避免误报,eslint-plugin-compat 选择了跳过这类 API 的检查。

例如,foo.includes 在不确定 foo 是否为数组类型的时候,就无法判断 includes 方法的兼容性。在下图中,我们在使用上面的 browserslint 配置的情况下,includes 方法的兼容问题并没有被扫描出来:

api-compat-includes-1.png

然而,从 caniuse 上可以查知,Array.prototype.includes() 方法不能被 IE 兼容:

image-20210817151252336.png

为了避免漏报这种问题,我们可以结合另一个兼容检查插件 eslint-plugin-builtin-compat 。该插件同样借助 mdn-browser-compat-data 来进行兼容扫描,与 eslint-plugin-compat 不同的是,该插件不会放过实例对象,因此它会把所有 foo.includesincludes 方法当成是 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() 方法将会被该插件告警:

image-20210818094029925.png

该方案的优点:

  • 配置比较简单,不用改造脚本代码,不用修改Jenkinsfile CI脚本;

该方案的缺点:

  • 源代码直接在控制台能看到,不安全;

IDEA中引入ESlint

实际需求中,后端同学可能会负责FM项目的部分前端需求,因此还需要满足IDEAESlint的使用。

(1)查看本地有没有安装node环境和全局安装eslint,如果没有就需要安装,安装之后重启IDEA,然后勾选Enable启动ESlint检视

image-20210813163012398.png

(2)如果重启IDEA后启动eslint服务时报如下错误,这是因为IEADplugins/JavaScriptLanguage/languageService/eslint版本过低,存在插件兼容问题

image-20210813163045270.png

image-20210813163626125.png

解决方法,照如下修改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;

正常效果:

image-20210813164107066.png

综上所述,如果已有项目组在FM项目中已经引入构建工具,比如备货组的商城页面已经引入多种语言开发,再使用构建工具打包,此类项目不用考虑ES6+语法问题,其他场景下的FM项目可以选择使用第二种方案。本文的方案作为抛砖引玉,大家具体场景具体分析。

组织实施


问题反馈与建议

一切使用问题或者更好的建议都可以向XX同学反馈,谢谢支持与配合!


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。