虽然我是eslint的忠实拥簇者,不过最近在集成prettierr的过程当中,仍是以为贼麻烦。各类配置只知其一;不知其二,为啥有些又要加config又要加plugin,有些加了config就不用加plugin了…诸如此类的。因此就专门拿时间看了看eslint的背后流程~html
eslint固然支持不少种格式的配置文件,我习惯用.eslintrc的json形式。vue
若是一份.eslintrc空空如也,那它固然没法工做。经过在rules里配置想要的规则,才能让eslint知道你须要哪些校验。 这也说明了,eslint自己维护了一些校验规则。因此咱们才能方便的以rules名称的方式选择开启其中的部分嘛。node
从大方向而言,你们都清楚,无非是AST分析,--fix就是替换嘛。不过真的要作AST分析,仍是以为是一项dirty work…一份js有那么多节点,而校验规则又那么多,怎么来匹配,又怎么确保效率呢?不可能每一个校验规则都扫描一次全部节点吧。react
其实从eslint提供的开发者开发自定义rule的文档接口中就能够大体看出一点逻辑。在eslint从外到内,再从内到外的阅读一份代码的过程当中,提供了无数个钩子(包括进入退出文档、进入退出每种类型的节点…)供开发者注册。在这些钩子内,开发者能够经过eslint传入的当前node节点入参,以及全局挂载在context上的工具方法们,进行自定义校验;若校验失败,则通知eslint失败缘由。git
挑两个最经常使用的rules看看格式就理解了~程序员
代码地址: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
代码地址: 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函数,以返回经矫正过的节点信息。
说到rules,那你们确定会有本身喜欢的一套rules配置,固然也会想共享一套配置。这就是extends的做用啦。extends不光能够继承rules,还能够继承.eslintrc里的全部配置项。extends即表明了”我想用别人这套.eslintrc config的意思“。
eslint-extends包名格式为eslint-config-xxx
,配置时在.eslintrc下的extends中加上xxx
便可使用。
Eslint里一样也有内置的一套默认推荐配置,也就是常见的eslint:recommended
(代码)。因此,最简单的基础错误校验配置方式,就是在.eslintrc里的extends里配上eslint:recommended
就好啦。
除了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嘛。若是有些其他的配置也必需要用户指定怎么办。
因此通常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指定解析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配置拉。
即给parser传的options。针对espree而言,咱们经常使用的通常有这么几项:
值得注意的一点是,使用其余的parser时,其parserOptions得根据其parser的文档来,好比babel-eslint的parserOptions。
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的做用是指定全局变量,避免被误认为unused-vars。而env的做用是指定在该环境下特有的一系列全局变量,好比node的require, es6的Promise。不一样的env并不互斥,因此多指定几个也不要紧。通常你们经常使用的配置为:
{
"env": {
"browser": true,
"node": true,
"es6": true,
"commonjs": true,
}
}
复制代码
以上,粗浅的知道了eslint的工做原理,在配置时就不会再一头雾水了。至少知道config和plugin的原理,eslint是如何读取到其中的配置值的~
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
撞车。
若是想要制定一套本身公司独有的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']
}
复制代码
其实在平常使用中, eslint配置最好要做为脚手架初始化配置的一部分。可是总面临着要维护没有eslint,或是…没有什么用的eslint的老项目的状况。因此我手撸了一个小脚本,方便一次性生成项目的eslint+prettier的全配置,包括.eslintrc、.prettierrc及依赖包。支持nodejs, React及Vue环境,也支持对经常使用extends的极其微小的选择权:
使用也很方便:
npx add-eslint-script
复制代码
默认配置地址以下,也能够此为base直接采用~
经过此次对eslint的详细背调,我才了解到其做者Nicholas C. Zakas从年轻时开始,就常年抱恙了。他患有严重的莱姆病,是一种发做缘由和治疗方案都不大明确的神经性疾病。如今他已没法正常工做,亦不能正常生活了。从 I have lyme disease 能够读到他的自述和每一年的健康情况发展。
看完他的故过后,我不由陷入了沉思…阿,生活不易。在痛其体肤的状况下,他依然能坚持作完这么细致,甚至能够说是细枝末节的工做;写parser,分析一棵棵AST树…也很使人钦佩了。
Bless Eslint!!