一、JSLint JSHint ESLint
参考
前端工具考 - ESLint 篇
JS Linter 进化史
C 语言诞生之初,程序员编写的代码风格各异,在移植时会出现一些因为不严谨的代码段导致无法被编译器执行的问题。于是在 1979 年,一款叫 lint[1] 的程序被开发出来,能够通过扫描源代码检测潜在的错误。此后 lint 的功能不断完善,类似的工具相继出现。不仅可以检测代码中的潜在 Bug,还能做一些类型检查。
1、JavaScript 为什么需要 lint
起初 JavaScript 被开发出来的目的只是用在 Web 页面里实现一些简单的交互(例如表单提交)。随着互联网发展,网站需要展示的内容更加丰富,交互也变得复杂,前端项目也越来越庞大;2009 年,NodeJS 的诞生使得 JavaScript 可以跑在服务端,更是让其地位更加突出。在 2017 年 GitHub 开发语言排行榜中,JavaScript 毫无疑问排在第一位。[2]
再加上 JavaScript 本身设计上存在许多缺陷[3],代码不严谨也可能就会触发神奇的错误。例如 == 和 === 的混用,可能会产生类型不一致引起的 Bug,经验不足的开发者很难察觉出异常。
2002 年,Douglas Crockford (译注:《JavaScript 语言精粹》的作者)开发了可能是第一款针对 JavaScript 的语法检测工具 —— JSLint[4],并于 2010 年开源。
2、JSLint 和 JSHint
JSLint 面市后,确实帮助许多 JavaScript 开发者节省了不少排查代码错误的时间。但是 JSLint 的问题也很明显——几乎不可配置,所有的代码风格和规则都是内置好的;再加上 Douglas Crockford 老爷子推崇道系「爱用不用」的优良传统,不会向开发者妥协开放配置或者修改他觉得是对的规则。于是 Anton Kovalyov 吐槽:「JSLint 是让你的代码风格更像 Douglas Crockford 的而已」[5],并且在 2011 年 Fork 原项目开发了 JSHint。
JSHint 的特点就是可配置,同时文档也相对完善,而且对开发者友好。很快大家就从 JSLint 转向了 JSHint。
3、ESLint 的诞生
起初几年,JSHint 一直是前端代码检测工具的首选,包括 Nicholas C. Zakas (《JavaScript高级程序设计》作者)也是 JSHint 的用户。但在 2013 年,Zakas 大佬发现 JSHint 已经无法满足自己定制化规则的需求,而且和 Anton 讨论后达成共识这根本在不可能在 JSHint 上实现。同时 Zakas 还设想发明一个基于 AST 的 lint,可以动态执行额外的规则,同时可以很方便的扩展规则。[6]
2013 年的 6 月份,Zakas 发布了全新的 lint 工具——ESLint[7]。
几乎同一时间,另一款和 ESLint 实现机制类似的代码风格检测工具——JSCS[8]——也诞生了。
4、可扩展性的胜利
ESLint 的出现并没有撼动 JSHint 的霸主地位。由于前者是利用 AST 处理规则,用 Esprima 解析代码,执行速度要比只需要一步搞定的 JSHint 慢很多;其次当时已经有许多编辑器对 JSHint 支持完善,生态足够强大。真正让 ESLint 逆袭的是 ECMAScript 6 的出现。
2015 年 6 月,ES2015 规范正式发布。但是发布后,市面上浏览器对最新标准的支持情况极其有限。如果想要提前体验最新标准的语法,就得靠 Babel 之类的工具将代码编译成 ES5 甚至更低的版本,同时一些实验性的特性也能靠 Babel 转换。这时 JSHint 就略尴尬,ES2015 变化很大,短期内无法完全支持。ESLint 可扩展的优势一下就体现出来了,不仅可以扩展规则,甚至连解析器也能替换。Babel 团队就为 ESLint 开发了 babel-eslint[9] 替换默认解析器,让 ESLint 率先支持 ES2015 语法。
也是在 2015 年,React 的应用越来越广泛,诞生不久的 JSX 也愈加流行。ESLint 本身也不支持 JSX 语法。还是因为可扩展性,eslint-plugin-react[10] 的出现让 ESLint 也能支持当时 React 特有的规则。
2016 年,JSCS 开发团队认为 ESLint 和 JSCS 实现原理太过相似,而且需要解决的问题也都一致,最终选择合并到 ESLint,并停止 JSCS 的维护。[11]
至此,ESLint 完美躺赢,替代 JSHint 成为前端主流工具。[12]
二、ESLint入门
参考
利用 ESLint 检查代码质量
ESLint中文网
1.安装
npm install -g eslint
2.准备一个js文件来测试一下
function merge () {
var ret = {};
for (var i in arguments) {
var m = arguments[i];
for (var j in m) ret[j] = m[j];
}
return ret;
}
console.log(merge({a: 123}, {b: 456}));
3.执行eslint merge.js
这时是没有输出结果的,原因是没有指定配置文件。现在添加一个.eslintrc.js,使用内置配置
module.exports = {
extends: 'eslint:recommended',
};
重新执行eslint merge.js可以看到输出了 2 个错误:
/example/merge.js
10:1 error Unexpected console statement no-console
10:1 error 'console' is not defined no-undef
✖ 2 problem (2 error, 0 warnings)
这两条提示信息还是足够我们搞清楚是怎么回事的:
- Unexpected console statement no-console - 不能使用console
- ‘console’ is not defined no-undef - console变量未定义,不能使用未定义的变量
针对第 1 条提示,我们可以禁用no-console规则。将配置文件.eslintrc.js改为这样:
module.exports = {
extends: 'eslint:recommended',
rules: {
'no-console': 'off',
},
};
说明:配置规则写在rules对象里面,key表示规则名称,value表示规则的配置。每条规则有 3 个等级:off、warn和error。off表示禁用这条规则,warn表示仅给出警告,并不会导致检查不通过,而error则会导致检查不通过。可以参考规则说明文档
重新执行检查还是提示no-undef:
/example/merge.js
10:1 error 'console' is not defined no-undef
✖ 1 problem (1 error, 0 warnings)
这是因为 JavaScript 有很多种运行环境,比如常见的有浏览器和 Node.js,另外还有很多软件系统使用 JavaScript 作为其脚本引擎,比如 PostgreSQL 就支持使用 JavaScript 来编写存储引擎,而这些运行环境可能并不存在console这个对象。另外在浏览器环境下会有window对象,而 Node.js 下没有;在 Node.js 下会有process对象,而浏览器环境下没有。
所以在配置文件中我们还需要指定程序的目标环境:
module.exports = {
extends: 'eslint:recommended',
env: {
node: true,
},
rules: {
'no-console': 'off',
},
};
再重新执行检查时,已经没有任何提示输出了,说明merge.js已经完全通过了检查。
4.使用共享的配置文件
上文我们以eslint:recommended为基础配置,然后在此之上修改no-console这条规则。而在大多数时候,我们可能会根据自己个人或团队的习惯,定制更多的规则,比如限定缩进是 2 个空格和使用单引号的字符串等。而如果每一个项目都要这样写到.eslintrc.js文件上,管理起来会比较麻烦。
我们可以将定义好规则的.eslintrc.js文件存储到一个公共的位置,比如public-eslintrc.js:
module.exports = {
extends: 'eslint:recommended',
env: {
node: true,
},
rules: {
'no-console': 'off',
'indent': [ 'error', 2 ],
'quotes': [ 'error', 'single' ],
},
};
然后将原来的.eslintrc.js文件改成这样:
module.exports = {
extends: './public-eslintrc.js',
};
为了验证这样的修改是否生效,将merge.js中的var ret = {};这一行前面多加一个空格,再执行 ESLint 检查:
/example/merge.js
2:4 error Expected indentation of 2 space characters but found 3 indent
✖ 1 problem (1 error, 0 warnings)
这时候提示的是缩进只能为 2 个空格,而文件的第 2 行却发现了 3 个空格,说明公共配置文件public-eslintrc.js已经生效了。
5.代码格式化
在规则说明文档中有如下说明
为了让你对规则有个更好的理解,ESLint 对其进行了分门别类。
所有的规则默认都是禁用的。在配置文件中,使用 "extends": "eslint:recommended" 来启用推荐的规则,报告一些常见的问题,在下文中这些推荐的规则都带有一个对勾标记。
命令行的 --fix 选项用来自动修复规则所报告的问题(目前,大部分是对空白的修复),在下文中会有一个扳手的图标。
使用上述配置,将merge.js中的var ret = {};这一行前面多加一个空格。然后执行eslint merge.js --fix
,可以发现merge.js文件被修改了,新添加的空格被去除了。我们可以利用这个特性来自动格式化项目代码,这样就可以保证代码书写风格的统一。
5.例外情况
尽管我们在编码时怀着严格遵守规则的美好愿景,而凡事总有例外。定立 ESLint 规则的初衷是为了避免自己犯错,但是我们也要避免不顾实际情况而将其搞得太过于形式化,本末倒置。
ESLint 提供了多种临时禁用规则的方式,比如我们可以通过一条eslint-disable-next-line
备注来使得下一行可以跳过检查:
// eslint-disable-next-line
var a = 123;
var b = 456;
在上面的示例代码中,var a = 123
不会受到检查,而var b = 456
则右恢复检查,在上文我们使用的eslint-config-lei
的配置规则下,由于不允许使用var
声明变量,则var b
这一行会报告一个error
。
我们还可以通过成对的eslint-enable
和eslint-disable
备注来禁用对某一段代码的检查,但是稍不小心少写了一个eslint-disable
就可能会导致后面所有文件的检查都被禁用,所以我并不推荐使用。
详细使用方法可以参考文档:Disabling Rules with Inline Comments - 使用行内注释禁用规则
6.总结
刚开始接触 ESLint 时觉得太难,是因为过太过于迷信权威。比如 Airbnb 公司的 JavaScript 风格,在 GitHub 上受到了很大的好评,其实我自己也非常认可这样的编码风格。但每个团队都会根据自己的的实际情况来定制不同的东西,我们并不能随便照搬过来。所以当使用eslint-config-airbnb这个配置进行 ESLint 检查时,满屏都是error和warning,从而觉得这东西根本没啥卵用。
另外我也犯了「大忌」:直接使用eslint-config-airbnb这种某个公司高度定制化的配置,而不是eslint:recommended这样保守的。而且是直接用来检查整个项目好几十个 JS 文件,可想而知那是怎样的画面(本文最后版本的merge.js文件使用airbnb的配置,总共 12 行的代码就提示了 8 个问题:✖ 8 problems (7 errors, 1 warning))。
本文的目的是让读者以一个比较低的姿态开始接触 ESLint,先学会简单地配置规则,如果要更深入地定制自己的规则,建议阅读
规则说明文档
ESLint 规则详解(一)
ESLint 规则详解(二)
三、其它问题
1.eslint --init
在第二部分入门示例时,是手动创建的.eslintrc.js。其实也可以使用eslint --init来创建
可以选个模板,也可以自己回答问题
PS E:\eslint> eslint --init
? How would you like to configure ESLint? Use a popular style guide
? Which style guide do you want to follow? (Use arrow keys)
> Airbnb (https://github.com/airbnb/javascript)
Standard (https://github.com/standard/standard)
Google (https://github.com/google/eslint-config-google)
比如选airbnb
module.exports = {
"extends": "airbnb"
};
也可以自己回答问题来创建
> eslint --init
? How would you like to configure ESLint? Answer questions about your style
// 是否校验 Es6 语法
? Are you using ECMAScript 6 features? Yes
// 是否校验 Es6 模块语法
? Are you using ES6 modules? Yes
// 代码运行环境,Browser 指浏览器
? Where will your code run? Browser
// 是否校验 CommonJs 语法
? Do you use CommonJS? Yes
// 是否校验 JSX 语法
? Do you use JSX? Yes
// 是否校验 React 语法
? Do you use React? Yes
// 首行空白选择 Tab 键还是 Space
? What style of indentation do you use? Tabs
// 字符串使用单引号 'string' 还是双引号 "string"
? What quotes do you use for strings? Double
// 操作系统
? What line endings do you use? Windows
// 每行代码结尾是否校验加分号 ;
? Do you require semicolons? Yes
// 以 .js 格式生成配置文件
? What format do you want your config file to be in? JavaScript
// 因为要校验 Reac 语法,所以这里需要下载一个 React 语法规则的包
Installing eslint-plugin-react@latest
2."extends": "eslint:recommended"
值为 "eslint:recommended" 的 extends 属性启用一系列核心规则,这些规则是经过前人验证的最佳实践(所谓最佳实践,就是大家伙都觉得应该遵循的编码规范),想知道最佳实践具体有哪些编码规范,可以在 eslint规则表 中查看被标记为 √ 的规则项。
如果觉得官方提供的默认规则不好用,可以自定义规则配置文件,然后发布成 Npm 包,eslint-config-airbnb 就是别人自定义的编码规范,使用 npm 安装后,在我们自己的 .eslintrc.js 中进行配置 "extends": "airbnb",eslint-config 这个前缀可以省略不写,这样我们就使用了 eslint-config-airbnb 中的规则,而不是官方的规则 "extends": "eslint:recommended" 了。关于如何创建自定义规则配置并共享可以参考: 如何自定义规则配置
关于 "airbnb" 编码规范说两句,在接触到大多数开源项目中,大多数的作者都会使用 "airbnb" 编码规范而不是 官方的"extends": "eslint:recommended" 编码规范。如果我们觉得 eslint-config-airbnb 规则配置中个别规则并不符合当前项目的要求,可以直接在 .eslintrc.js 配置 rules 属性,优先级高于共享规则 airbnb。
3.rules
ESLint 附带有大量的规则,修改规则应遵循如下要求:
- "off" 或 0 - 关闭规则
- "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
- "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
有的规则没有属性,只需控制是开启还是关闭,像这样:"eqeqeq": "off",有的规则有自己的属性,使用起来像这样:"quotes": ["error", "double"]
4.【文件配置】
ESLint 支持几种格式的配置文件
- JavaScript - 使用 .eslintrc.js 然后输出一个配置对象。
- YAML - 使用 .eslintrc.yaml 或 .eslintrc.yml 去定义配置的结构。
- JSON - 使用 .eslintrc.json 去定义配置的结构,ESLint 的 JSON 文件允许 JavaScript 风格的注释。
- (弃用) - 使用 .eslintrc,可以使 JSON 也可以是 YAML。
- package.json - 在 package.json 里创建一个 eslintConfig属性,在那里定义配置。
如果同一个目录下有多个配置文件,ESLint 只会使用一个。优先级顺序如下
1 .eslintrc.js
2 .eslintrc.yaml
3 .eslintrc.yml
4 .eslintrc.json
5 .eslintrc
6 package.json
除了配置一个独立的 .eslintrc.* 文件,也可以直接在 package.json 文件里的 eslintConfig 字段指定配置,ESLint 将自动在要检测的文件目录里寻找它们,紧接着是父级目录,一直到文件系统的根目录
{
"name": "mypackage",
"version": "0.0.1",
"eslintConfig": {
"env": {
"browser": true,
"node": true
}
}
}
四、gulp-eslint
npm install gulp-eslint --save
const eslint = require('gulp-eslint');
task('default', () => {
return src(['scripts/*.js'])
// eslint() attaches the lint output to the "eslint" property
// of the file object so it can be used by other modules.
.pipe(eslint())
// eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
.pipe(eslint.format())
// To have the process exit with an error code (1) on
// lint error, return the stream and pipe to failAfterError last.
.pipe(eslint.failAfterError());
});
比如,针对上面的例子,可以写一个gulpfile.js
var gulp = require('gulp'),
eslint = require('gulp-eslint');
gulp.task('lint', function () {
return gulp.src(['merge.js'])
.pipe(eslint())
.pipe(eslint.format())
});
gulp.task('default', ['lint'], function () {
console.log("suc");
});
执行结果
PS E:\eslint> gulp default
[16:10:52] Using gulpfile E:\eslint\gulpfile.js
[16:10:52] Starting 'lint'...
[16:10:53]
E:\eslint\merge.js
4:3 error Unexpected console statement no-console
12:1 error Unexpected console statement no-console
✖ 2 problems (0 errors, 2 warnings)
[16:10:53] Finished 'lint' after 495 ms
[16:10:53] Starting 'default'...
suc
[16:10:53] Finished 'default' after
这里明明报错了,还是执行到console.log("suc")了。原来是gulpfile.js还要再加一行
gulp.task('lint', function () {
return gulp.src(['merge.js'])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
这下子就过不去了
✖ 2 problems (2 errors, 0 warnings)
[16:21:13] 'lint' errored after 503 ms
[16:21:13] ESLintError in plugin "gulp-eslint"
Message:
Failed with 2 errors
这里其实可以通过一个bool变量来控制:.pipe(exit ? eslint.failAfterError() : eslint.result(function () {}));
五、在vscode中使用eslint
1.首先,打开 VSCode 扩展面板并搜索 ESLint 扩展,然后点击安装
安装完成之后,点击重新加载,或者重启vscode
2.依次点击vscode中的 文件 > 首选项 > 设置 打开 VSCode 配置文件
可以看到已经启用eslint了
3.打开merge.js可以看到报错,和上面命令行中的相同:
4.注意,我们已经安装过全局的eslint并且生成eslintrc.js了。如果没有这些操作,也是不行的。参考提升效率之搭建eslint+vscode开发环境