背景
ESLint 是一个很好用的 js 静态代码检查器,通过在编译环境(IDE)安装插件,可以时时的对 js 代码进行代码风格的校验以及常见的js代码错误。引入 ESLint 可以帮助我们检查编程规范从而提高代码的质量。关于 ESLint 的入门介绍网上有很多很好的资源,这里就不再赘述。
- 官方文档
- 利用ESLint检查代码质量 by 老雷
面对的问题
由于项目中有许多结合项目本身而自定义的编程规范,但对于一个人员流动较大、开发人员较多的团队通过口口相传和 code review 来执行编程规范显然是事倍功半的。这时就需要利用 ESLint 来 DIY 我们自己的规则。
小试牛刀
我们先来实现一个简单的自定义的 ESLint 规则。举个例子,我们不允许任何标识符 (identifier) 中出现
hello
这个单词。
我们先创建一个新项目然后创建一个叫eslint_rules
的文件夹,然后创建一个文件叫做 no-hello-in-identifier
的 js 文件,然后 no-hello-in-identifier.js
代码如下:
var _ = require('lodash')
module.exports = {
meta: {
messages: {
invalidName: 'Avoid use \'hello\' for identifier'
}
},
create (context) {
return {
Identifier (node) {
if (_.includes(node.name, 'hello')) {
context.report({
node,
messageId: 'invalidName'
})
}
}
}
}
}
现在我们已经定义了一个属于自己的规则,在这个规则中:
-
meta
用于定义这个规则的一些元数据(metadata)比如示例中的messages
就用于当检测到某代码违反此规则时显示的错误信息。meta
里面还可设置更多的元数据,之后会讲到。 -
create
方法返回一个对象,这个对象中包含着一些方法,这些方法用于在 ESLint 遍历 JS 代码的抽象语法树 (AST)时被调用。create
带有一个叫context
的参数,它是一个对象,这个对象提供了一些非常有用的方法来帮助我们实现规则。 - 在
crate
中的方法由两部分组成:选择器selector
和 访问器visitor
。示例中的 identifier 便是一个选择器,简而言之,选择器就是用来过滤我们想要的语法树节点 (node
)。比如我想要对所有的标识符节点做处理就用Identifier
作为选择器。更多关于选择器的内容请参考这里。而访问器就是函数主体,也就是我们怎样定义这个规则的地方。 -
ndoe.name
通过访问节点中的name
属性来获取节点的 name。 -
context.report
这个方法用来向用户报告错误信息。其中node
代表当前节点,messageId
引用了我们现在meta
中的信息。
现在问题来了,我们如何让 ESLint 去执行我们定义的规则呢?
其实很简单只需要将我们的 rule 引入到我们配置文件中,而后执行 eslint
即可 (如何引入 ESLint 以及 ESLint 的常规配置请参考前文提到的 ESLint 入门链接)。简单的配置内容如下:
module.exports = {
"extends": "eslint:recommended",
"rules": {
"no-hello-in-identifier": 2
}
};
现在我们写一个简单的 index.js
然后用我们的 ESLint rule 来检测它。
function helloWorld () {
return 'hello world!'
}
console.log(helloWorld())
当我们执行 eslint index.js --rulesdir eslint_rules/lib no-hello-in-identifier.js
后就可看到下面的结果
可以看到第一行和第四行是我们自定义的规则!现在我们自定义的规则已经开始起作用了。
到目前为止我们实现了一个简单的自定义规则,并将它成功的运用到了我们的 js 代码中。接下来我们就来实现一个我在项目中真实遇到的需求。
实战演练
在我工作的项目中,我们用引用了
reselect
来组装redux store
中的数据,然后将组装好的数据传给jsx component
用于渲染 Web 页面。但是在reselect
的官网上对于selector
的命名有两种方式:getXXXXX
和XXXXXSelector
于是两种命名方式在项目中同时存在。虽然这不是个大问题,但会造成新成员额外的学习成本,而且降低代码的可读性。现在我们就通过 ESLint 来使所有的 selector 都按照 getXXXXX 的格式来命名。
(注意:这里的 selector 指的是用于组装 redux store 数据的 selector,而不是前文中指的 ESLint 的 selector)
在开始 coding 之前,我们现在 tasking 一下我们需要做的事情。
- 通过
meta
设置我们想要输出的错误信息 - 过滤不必要的文件
- 找到合适的 ESLint
selector
(注意区分用于组装 redux store 数据的 selector) - 实现
visitor
现在我们就来一步一步实现我们的规则。
通过 meta 设置我们想要输出的错误信息
这一步很简单,与之前的例子大致相同。以下是代码:
module.exports = {
meta: {
messages: {
invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
}
},
create(context) {
//filer files code
return {
// customized rules
};
}
};
过滤不必要的文件
在我们的 code base 中所有的 selector
都会定义在文件名以 selector 或 selectors 结尾的文件。所以需要过滤掉其他文件以避免我们的规则出现“误杀”。这实现起来也很简单。
context
提供了一个叫 getFilename
的方法,可以帮助我们直接通过这个方法拿到当前的文件名,于是就可以过滤掉不需要校验的文件。代码如下:
const fileNameRegExp = /.*Selectors?.js$/;
module.exports = {
meta: {
messages: {
invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
}
},
create(context) {
if(!fileNameRegExp.test(context.getFilename())) return {};
return {
// customized rules
};
}
};
找到合适的 ESLint selector
那在这个文件下,什么要得节点才是我们想要的节点呢?首先,我们所有的 selector 都会暴露出去,所以这个节点必然在 export
或者 export default
之中,而通过 AST 的在线编译工具我们不难发现这两个关键字分别在 ExportNamedDeclaration
和 ExportDefaultDeclaration
之下。然而仅仅使用这两个是不行的,通过调用 node.name
我们发现我们拿到的并不是我们想要的变量名而是 undefinded
这是因为在语法树中这两个关键字下并没有 name
这个属性。但是,我们通过官方文档我们发现可以通过下面的方式来找到我们想要的变量名:
'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier'
找到了合适的 selector
, 就可以继续改进我们的代码:
const fileNameRegExp = /.*Selectors?.js$/;
module.exports = {
meta: {
messages: {
invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
}
},
create(context) {
if(!fileNameRegExp.test(context.getFilename())) return {};
return {
'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier': (node) => {
checkSelectorName(node, context);
}
};
}
};
目前我们只定义了一条规则,它检测了所有位于 ExportNamedDeclaration
下的 VariableDeclaration
下的 VariableDeclarator
的 Identifier
的变量名是否含有 hello
。(这里的 checkSelectorName() 我们之后会实现它)
但我们还需要检测所有位于 ExportDefaultDeclaration
下的 Identifier
, 所有我们需要加一条规则。方法很简单,在返回的对象中再添加一个 selector 和 visitor 即可:
const fileNameRegExp = /.*Selectors?.js$/;
module.exports = {
meta: {
messages: {
invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
}
},
create(context) {
if(!fileNameRegExp.test(context.getFilename())) return {};
return {
'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier': (node) => {
checkSelectorName(node, context);
},
'ExportDefaultDeclaration > Identifier': (node) => {
checkSelectorName(node, context);
}
};
}
};
这样整体框架就已经完成了,现在我们只需要实现 checkSelectorName()
也就是我们的 visitor
就可以了。
实现 visitor
这个检测很简单,这要检测我们的变量名是否满足正则,不满足则向用户发送错误报告。
const selectorRegExp = /^(?:(get)|(is)|(should)|(has))/;
const checkSelectorName = (node, context) => {
const nodeName = node.name;
if(!selectorRegExp.test(nodeName)) {
context.report({
node,
messageId: 'invalidName',
data: {
name: node.name
}
});
}
};
这样整个自定义的 ESLint 就完成了!