管中窥eslint【附赠各框架超快eslint&prettier配置脚本】

虽然我是eslint的忠实拥簇者,不过最近在集成prettierr的过程当中,仍是以为贼麻烦。各类配置只知其一;不知其二,为啥有些又要加config又要加plugin,有些加了config就不用加plugin了…诸如此类的。因此就专门拿时间看了看eslint的背后流程~html

.eslintrc

eslint固然支持不少种格式的配置文件,我习惯用.eslintrc的json形式。vue

rules

若是一份.eslintrc空空如也,那它固然没法工做。经过在rules里配置想要的规则,才能让eslint知道你须要哪些校验。 这也说明了,eslint自己维护了一些校验规则。因此咱们才能方便的以rules名称的方式选择开启其中的部分嘛。node

rules是如何工做的?

从大方向而言,你们都清楚,无非是AST分析,--fix就是替换嘛。不过真的要作AST分析,仍是以为是一项dirty work…一份js有那么多节点,而校验规则又那么多,怎么来匹配,又怎么确保效率呢?不可能每一个校验规则都扫描一次全部节点吧。react

其实从eslint提供的开发者开发自定义rule的文档接口中就能够大体看出一点逻辑。在eslint从外到内,再从内到外的阅读一份代码的过程当中,提供了无数个钩子(包括进入退出文档、进入退出每种类型的节点…)供开发者注册。在这些钩子内,开发者能够经过eslint传入的当前node节点入参,以及全局挂载在context上的工具方法们,进行自定义校验;若校验失败,则通知eslint失败缘由。git

挑两个最经常使用的rules看看格式就理解了~程序员

例一:no-console

代码地址:eslint/no-console.js at master · eslint/eslint · GitHubes6

超级简化版以下:github

module.exports = {
	meta: {
        type: "suggestion",

      
        messages: {
            unexpected: "Unexpected console statement."
        }
    },
	create(context){
		function isConsole(reference) {
        const id = reference.identifier;
        return id && id.name === "console";
		}

		function report(reference) {
            const node = reference.identifier.parent;
            context.report({
                node,
                loc: node.loc,
                messageId: "unexpected"
            });
        }
		return {
				// 钩子节点
            "Program:exit"() {
            		const scope = context.getScope();
					scope.through.filter(isConsole);

                	const references = scope.through.filter(isConsole);
                references.forEach(report);
				}
        };

		}
	}
	
复制代码

关键就在create函数中return的对象。该对象的每一个key都表明调用检测函数的钩子节点。能够看到,no-console是在Program:exit,也就是阅读完整份节点文档时作的校验;若是发现有identifier为console的,就经过调用context.report通知到eslint。typescript

例二:no-extra-semi

代码地址: github.com/eslint/esli…json

超级简化版以下:

module.exports = {
    meta: {
        fixable: "code",
        messages: {
            unexpected: "Unnecessary semicolon."
        }
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        function report(nodeOrToken) {
            context.report({
                node: nodeOrToken,
                messageId: "unexpected",
                fix(fixer) {
return new FixTracker(fixer, context.getSourceCode())
                        .retainSurroundingTokens(nodeOrToken)
                        .remove(nodeOrToken);
                }
            });
        }

        function checkForPartOfClassBody(firstToken) {
            for (let token = firstToken;
                token.type === "Punctuator" && !astUtils.isClosingBraceToken(token);
                token = sourceCode.getTokenAfter(token)
            ) {
                if (astUtils.isSemicolonToken(token)) {
                    report(token);
                }
            }
        }

        return {
            EmptyStatement(node) {
                const parent = node.parent,
                    allowedParentTypes = [
                        "ForStatement",
                        "ForInStatement",
                        "ForOfStatement",
                        "WhileStatement",
                        "DoWhileStatement",
                        "IfStatement",
                        "LabeledStatement",
                        "WithStatement"
                    ];

                if (allowedParentTypes.indexOf(parent.type) === -1) {
                    report(node);
                }
            },

            ClassBody(node) {
checkForPartOfClassBody(sourceCode.getFirstToken(node, 1)); 
            },

            MethodDefinition(node) {
checkForPartOfClassBody(sourceCode.getTokenAfter(node));
            }
        };
    }
复制代码

如上,能够看到检测semi的时机是在EmptyStatement,ClassBody,MethodDefinition三类节点下。这里比较有趣的是看似陌生的EmptyStatment。js里无特殊意义的分号(好比return结尾)的节点类型就是EmptyStatement噢。(这一点我以前从未知道!)

不过看到这里时,内心未免会嘀咕起来,这么多node type,鬼知道什么对应什么阿。后来我找到了一个好办法——去 astexplorer 上手动写写找下对应;至于总体的AST节点类型有哪些,能够去 estree/es5.md 瞅瞅。它列举了最基础的节点类型,后续更新的es版本都会基于该版本作一些延伸。

这份rule还有一份特别之处——它支持自动fix。在调用context.report()时有传一个fix函数,以返回经矫正过的节点信息。

extends

说到rules,那你们确定会有本身喜欢的一套rules配置,固然也会想共享一套配置。这就是extends的做用啦。extends不光能够继承rules,还能够继承.eslintrc里的全部配置项。extends即表明了”我想用别人这套.eslintrc config的意思“。

eslint-extends包名格式为eslint-config-xxx,配置时在.eslintrc下的extends中加上xxx便可使用。

Eslint里一样也有内置的一套默认推荐配置,也就是常见的eslint:recommended代码)。因此,最简单的基础错误校验配置方式,就是在.eslintrc里的extends里配上eslint:recommended就好啦。

plugins

除了eslint里内置的基础检测项,人们确定还会有更多的自定义需求。比方若是我想本身写一套rules来集成到eslint里校验呢?那么就该plugins出场啦。

plugin的包名格式为eslint-plugin-xxx,该包的导出对象包含rules字段,其值是rules名称和对应校验函数的大对象。

示例eslint-plugin-import/index.js:

exports.rules = {
  'no-unresolved': require('./rules/no-unresolved'),
  'named': require('./rules/named'),
	...
}
复制代码

使用时,先在.eslintrc里注入plugin;再在rules指定要使用的plugin里的rule名称。

.eslintrc:

{
	"plugins": ["xxx"],
	"rules": {
		"xxx/xxx-rule": "error"
	}
}
复制代码

不过这时烦恼就出现了,总不能让用户再敲一遍rules嘛。若是有些其他的配置也必需要用户指定怎么办。

plugins中集成的配置

因此通常plugin里也会导出几套推荐配置,可是这些推荐配置默认是不会生效的噢。须要用户安装plugin:xxx/configName的指定名称去extends该config。由于这些config里,通常也会配置本身为plugin,因此用户免去了须要在eslintrc下特别指定plugin的操做。不过须要注意下plugin里导出的config有无extend其余config包,如有,也要记得安装上。

示例 eslint-plugin-import/index.js:

exports.rules = {	... }
exports.configs = {
	recommended: require('../config/recommended'),
	...
}
复制代码

使用:

原(需安装eslint-plugin-import和eslint-config-import):
{
	"plugins": ["import"],
	"extends": ["import"]
}

更方便(只需安装eslint-plugin-import):
{
	"extends": ["plugin: import/recommended"]
}
复制代码

parser & parserOptions & Processor

这三个我以前一直都傻傻分不太清。因此就拎到一块儿总结了。

parser

parser指定解析ast用的解析器。eslint的默认parser是内置的espree。另外还很常见的有:

  • @typescript-eslint/parser
  • vue-eslint-parser
  • babel-eslint

前二者好理解,他们的语法树确定有些不同,因此须要单独parse嘛。而babel-eslint也就是babel内置使用的parser,和espree比有什么不一样呢?espree不也能指定es版本吗?

虽然espree也能够经过parserOptions.ecmaVersion指定es版本,但仅仅局限于写进es标准的语法,好比decorator等exprimental试验版本就不大能支持。babel-eslint能处理的语法更为全面,毕竟人家是babel呀。另外,只要指定parse: "babel-eslint"就啥配置都不用了,也不用额外写parserOptions.ecmaVersion,用起来也更为方便把。

p.s. 使用了typescript或者vue的插件或config后,就注意不要再在本身的.eslintrc内指定parser了,这样会覆盖掉它们集成的parser配置拉。

parserOptions

即给parser传的options。针对espree而言,咱们经常使用的通常有这么几项:

  • parserOptions.sourceType: ‘script’/‘module’; 如今基本都是module了。
  • parserOptions.ecmaVersion:espree支持的es版本。
  • parserOptions.ecmaFeatures: { globalReturn’, ‘impliedStrict’, ‘jsx’} 。用jsx相关的框架时,好比React或Vue,jsx都要记得写上。

值得注意的一点是,使用其余的parser时,其parserOptions得根据其parser的文档来,好比babel-eslint的parserOptions

Processor

Processor看起来就很小众了,它的做用是:

Processors can extract JavaScript code from another kind of files, then lets ESLint lint the JavaScript code. Or processors can convert JavaScript code in preprocessing for some purpose.

也就是提供预处理某种代码的功能。

有个身边的例子,eslint-plugin-vue就针对.vue文件使用了本身写的processor来进行转换。

globals & env

globals的做用是指定全局变量,避免被误认为unused-vars。而env的做用是指定在该环境下特有的一系列全局变量,好比node的require, es6的Promise。不一样的env并不互斥,因此多指定几个也不要紧。通常你们经常使用的配置为:

{
	"env": {
		"browser": true,
    	"node": true,
    	"es6": true,
		"commonjs": true,
	}
}
复制代码

以上,粗浅的知道了eslint的工做原理,在配置时就不会再一头雾水了。至少知道config和plugin的原理,eslint是如何读取到其中的配置值的~

Prettier

Prettier是至关强大的格式化工具。Eslint虽然也能指定结尾加不加分号呀,空几格呀,但它更偏向于“语法检查”,只是同时附带了一些轻微格式化的功效。而对于整个代码的排版,不少人都是依靠代码编辑器来的。这样一我的开发还好,多人开发时,很容易由于互相没协商好编辑器格式化风格,形成风格很不一致的状况。再加上你们都很热爱”format on save”,因此ctrl+s一下,整篇代码的git blame都变成本身了…

因此用上了prettier以后,我每天都很开心。

虽然有人抱怨说prettier会乱折行,可是只要按本身的喜好配好了,用起来仍是很顺手的。

Prettier的配置很简单,最基础的就这么几项(不过想一想,程序员格式化界争论来争论去不也就这么几项么)。如下是我最喜欢的.prettierrc配置:

{
  "printWidth": 100, // 每行最多字,折行时机的关键~
  "singleQuote": true, // 单引号
	"tabWidth": 2, // 几个空格
  "semi": false, // 结尾分号
  "trailingComma": "es5" // 这个存在感低一点但很实用,表示对象、数组的最后一项会补上逗号
}
复制代码

但是你们都讨厌安装重复的新东西呀。都用着eslint了,难道还要加个prettier命令么。因此prettier也提供了集成到eslint里的工具eslint-plugin-prettier,这就无比方便啦!eslint校验及fix的时候,也让prettier把本身的一份检查顺手作了~

p.s. 有一点要注意的是,为避免和eslint自带的format检验撞车,prettier在本身的基础配置中,会先把eslint全部自带的和格式检查相关的rules给关掉。因此若是既要配eslint: recommended又要配plugin:prettier/recommended的话,记得plugin:prettier/recommended要放到eslint: recommended后面噢。

p.p.s. 要注意编辑器的format on save功能也须要关闭,避免和eslint的format on save撞车。

playground:写一个本身的eslint插件把~

若是想要制定一套本身公司独有的Eslint校验规则,也不妨按着eslint开发者文档一试噢。

咱们举个很简单的例子,不容许代码里的string里出现foo,须要替换成boo。那只要新建一个名为eslint-plugin-myself的包,里头有一条名为’foo-to-boo’的规则。

入口eslint-plugin-myself/index:js:

module.exports = {
	rules: {
		'foo-to-boo': require('./foo-to-boo')
	},
	configs: {
		// 把这份config名为为all
		all: {
			plugins: ['eslint-plugin-myself'],
			rules: {
				'myself/foo-to-boo': 'error'
			}
		} 
	}
}
复制代码

foo-to-boo.js

modules.exports = {
	create: function(context) {
		function validate(node){
			const val = node.raw;
			// 校验规则
			if(val.match && val.match('foo')) {
				 // 报告eslint
               context.report({
                   node,
                   message: 'no foo..',
					  // 返回修复后的节点内容 
                   fix: (fixer) => fixer.replaceTextRange(node.range, val.replace(/foo/, ()=>'bar');
                });
           }
		}
		return {
			// 校验string 如'hahhfoohh'
			'Literal': validate,
			// 校验template 如`hahhfoohh`
			'TemplateElement': validate
		}
	}
}
复制代码

使用:

{
	"extends": ['plugin: myself/all']
}
复制代码

Bonus: 30s生成eslint全配置~

其实在平常使用中, eslint配置最好要做为脚手架初始化配置的一部分。可是总面临着要维护没有eslint,或是…没有什么用的eslint的老项目的状况。因此我手撸了一个小脚本,方便一次性生成项目的eslint+prettier的全配置,包括.eslintrc、.prettierrc及依赖包。支持nodejs, React及Vue环境,也支持对经常使用extends的极其微小的选择权:

example.gif

使用也很方便:

npx add-eslint-script
复制代码

项目地址

默认配置地址以下,也能够此为base直接采用~

node项目默认配置

react项目默认配置

vue项目默认配置

碎碎念:关于eslint开发者

经过此次对eslint的详细背调,我才了解到其做者Nicholas C. Zakas从年轻时开始,就常年抱恙了。他患有严重的莱姆病,是一种发做缘由和治疗方案都不大明确的神经性疾病。如今他已没法正常工做,亦不能正常生活了。从 I have lyme disease 能够读到他的自述和每一年的健康情况发展。

看完他的故过后,我不由陷入了沉思…阿,生活不易。在痛其体肤的状况下,他依然能坚持作完这么细致,甚至能够说是细枝末节的工做;写parser,分析一棵棵AST树…也很使人钦佩了。

Bless Eslint!!

相关文章
相关标签/搜索