携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
上一篇文章详细地介绍了eslint如何使用和配置,如果还有不清楚的,建议先看一看这篇看完这篇文章,我不信你还对eslint一知半解,里面有关于规则和插件的详细讲解。知道了eslint如何使用和配置之后,是时候进阶一下,来看一下eslint自定义规则和插件是怎么玩的了。
耐心看完这篇文章,看完后你将会有如下收获:
- 知道如何根据自己的需求去自定义自己的规则
- 基本能看得懂其他开源的eslint插件的源码
什么是规则,什么是插件可能大家会弄混,这里简单解释一下,就是我们一般都是通过自定义插件来把我们自定义的规则暴露出来
为什么需要自定义插件
因为在不同的团队中,难免会遇到这样的情况,就是我们想制定的某些内部规范,在现有的官方和第三方规则rule
里面都是没有的,这个时候,就只能自己动手,丰衣足食了。
举个栗子:假如项目里有一个公共方法叫publicMethod,调用的时候是这么调用的util.publicMethod
,而你们团队规定调用的时候,必须要在这个方法上面加上一个特殊的注释,像下面这样:
// $team xxxx
util.publicMethod();
如果你没有写注释,或者注释不是以$team开头的,我们希望eslint可以给我们一个warning提醒一下,这个时候我们就可以自己去自定义一个规则来达到上面的效果了。
接下来,我们以上面的栗子作为需求,开发我们自己的eslint插件。
初始化插件的开发环境
创建插件开发环境最简单的方式是使用官方给我们提供的脚手架。
先全局安装两个包,执行如下命令:
npm i -g yo
npm i -g generator-eslint
上面的两个工具就是用来帮我们生成插件项目框架的,类似于vue-cli
再执行如下命令生成项目框架
yo eslint:plugin
这是一个交互式命令,需要你填写一些基本信息,如下
yo eslint:rule
? What is your name? 随便写个你的名字
? What is the plugin ID? 你的插件的id,推荐(eslint-plugin-xxx)的命名方式
? Type a short description of this plugin: 描述你的插件是干啥的
? Does this plugin contain custom ESLint rules? Yes
? Does this plugin contain one or more processors? No (这里我们用不到处理器,就直接选No)
执行完上面的命令后只是创建了插件架子,我们还需要在这个架子下创建一条规则,所以继续执行如下命令:
npx yo eslint:rule
这也是一个交互式命令,如下:
? What is your name? 还是你的名字
? Where will this rule be published? ESLint Plugin
? What is the rule ID? 你的规则id
? Type a short description of this rule: 你这条规则的描述
? Type a short example of the code that will fail: 这里写你这条规则校验不通过的案例代码
执行完上述命令后,会给我们生成如下的目录结构:
├── README.md 这是你的插件的描述文档,包括如何安装使用等
├── docs
│ └── rules 这是你的自定义规则的描述文档
│ └── my-custom-rules1.md
├── lib 源码目录
│ ├── index.js 插件的入口
│ └── rules 这里放你的每一条自定义规则
│ └── my-custom-rules1.js
├── package.json
├── pnpm-lock.yaml
└── tests 测试目录
└── lib
└── rules
└── my-custom-rules1.js
其实你如果不想安装那个脚手架,就按照上面的目录结构自己创建也是可以的。
上面的操作,相信大家都看得懂了,其实就是跟着步骤做就可以了,这是eslint给我们规范好的开发插件的标准流程。
开始动手前你需要先了解的
eslint的自定义规则和自定义插件是要遵循官方给定的格式的
rule的基本格式
一个规则的源码基本格式如下,不要问为什么,因为它就是这样定的
module.exports = {
// meta里面的元数据,对于我们自定义规则,其实只关心schema就行了
meta: {
// 规则的类型problem|suggestion|layout
// problem: 这条规则识别的代码可能会导致错误或让人迷惑。应该优先解决这个问题
// suggestion: 这条规则识别的代码不会导致错误,但是建议用更好的方式
// layout: 表示这条规则主要关心像空格、分号等这种问题
type: "suggestion",
// 对于自定义规则,docs字段是非必须的
docs: {
description: "描述你的规则是干啥的",
// 规则的分类,假如你把这条规则提交到eslint核心规则里,那eslint官网规则的首页会按照这个字段进行分类展示
category: "Possible Errors",
// 假如你把规则提交到eslint核心规则里
// 且像这样extends: ['eslint:recommended']继承规则的时候,这个属性是true,就会启用这条规则
recommended: true,
// 你的规则使用文档的url
url: "https://eslint.org/docs/rules/no-extra-semi"
},
// 标识这条规则是否可以修复,假如没有这属性,即使你在下面那个create方法里实现了fix功能,eslint也不会帮你修复
fixable: "code",
// 这里定义了这条规则需要的参数
// 比如我们是这样使用带参数的rule的时候,rules: { myRule: ['error', param1, param2....]}
// error后面的就是参数,而参数就是在这里定义的
schema: []
},
create: function(context) {
// 这是最重要的方法,我们对代码的校验就是在这里做的
return {
// callback functions
};
}
};
关于create方法
这里我们着重讲一下这个create方法,create方法返回的是一个对象,这个对象是干什么用的呢?我们得从AST
讲起。为了让大家更容易理解,我尽量用通俗易懂的例子
eslint校验一个代码文件的时候,你可以简单理解成分为下面几个步骤:
- 首先是读取这个文件的内容,比如读取a.js,你可以理解成读出来一个字符串。
- 然后eslint会创建一个对象,假设是如下对象
const astObj = {
type: 'Program', // 这个一段程序
start: 0, // 从第0行开始
end: 100, // 第100行结束
body: [], // 这里存放后面具体解析出来的代码内容
sourceType: 'module' // 这是一个模块
};
- 然后就是一行行去解析这个字符串,比如第一行定义了一个变量
let a = 1;
,我们称之为VariableDeclaration
(变量定义),此时会执行类似以下的操作
const curAst = {
type: 'VariableDeclaration', // 标识这是一个变量定义的节点
// ....后面还有很多字段
};
// 将当前解析好的节点push进去astObj的body里面
astObj.body.push(curAst);
- 假设下一行遇到一个函数定义
function test() {}
,此时会执行以下操作
const curAst = {
type: 'FunctionDeclaration', // 标识这是一个函数定义的节点
body: []
// ...后面还有很多内容,且函数也有自己的body,eslint的解析器会继续递归解析函数体的内容,然后push到这个节点的body里
}
// 将当前解析好的节点push进去astObj的body里面
astObj.body.push(curAst);
- 经过上面的不断递归解析操作,就构建出一棵ast抽象语法树了,具体转换后的结果可以自行到ast Explorer上自行尝试,如下图所示
- 构建好ast之后,就可以对这棵树进行遍历了,遍历的时候,可以给这个遍历的方法传一个访问者对象,当遍历到某个类型的节点的时候,就会调用这个访问者对象对应的方法,类似下面的例子:
// 访问者对象,我们可以在这里定义我们想要处理的节点,当访问到这个节点的时候,就会调用响应的方法
const visitor = {
// 当遍历的时候遇到type为FunctionDeclaration的节点的时候,就会调用这个方法,并把当前的节点传进来
'FunctionDeclaration': function(node) {},
// 当遍历的时候遇到type为CallExpression的节点的时候,就会调用这个方法,并把当前的节点传进来
'CallExpression': function(node) {},
};
// astObj是上面构建出来的抽象语法树
traverseAst(astObj, visitor);
至此,关于ast的转换和遍历都讲完了,我相信大家应该理解了,接下来我们回到上面的那个create
方法,其实上面的create
方法返回的对象,就是我刚刚所说的访问者visitor
对象。
假如说我的自定义规则想校验关于函数调用的语句如下:
obj.doSomething();
那这条语句对应的ast节点类型就是CallExpression
,所以我们的create
方法返回的visitor对象就可以像下面这样写
create(context) {
return {
// 当遇到函数调用的时候,就会调用此方法,并把当前节点传进来
'CallExpression': function(node) {
// 这里写具体的校验逻辑
}
}
}
大致的处理流程已经讲清楚了,当然,实际的ast转换遍历比我上面的讲的要复杂的多,但是原理基本都是类似的。
最后还要再讲一下create
方法的入参context
,这个context
对象包含了我们这条自定义规则的上下文相关的信息,我只举几个常用的如下
parserOptions
-编译器选项,就是我们.eslintrc.js里的那个id
-规则idoptions
-通过这个可以拿到规则传进来的参数getFilename()
-返回源文件文件名getScope()
- 返回当前遍历节点的作用域getSourceCode()
- 返回SourceCode
对象,就是源码对象,很有用report()
- 当校验不通过的时候,通过这个方法输出错误信息
开始写真正的规则代码
通过上面的前置知识介绍,现在可以真正的开始写我们规则的代码了,回到我们刚刚通过脚手架创建的目录,打开lib/rules/yourRules.js
,这个是你自定义规则的存放目录,后续要新增规则,直接再这个目录下创建就行。
我们先回顾一下我们的需求,我们要写一个校验方法调用的时候是否有加特定注释的规则
,我们预期的规使用方式如下:
// .eslintrc.js
{
rules: {
'myCustomPlugin/myRule1': [
'error',
{
objName: 'test', //这里配置对象名
propNames: ['testCall'], //这里配置对象的方法名,调用这些方法必须写注释,否则规则会报错
commentName: '$test', //这里是你要写的注释的开头,也就是说,当调用test.testCall()时,你必须写注释,且注释必须以$test开头才算合法
}
]
}
}
当你的代码里有以下语句的时候就会报错或者告警,因为没有加注释
test.testCall();
当你的代码像下面这样写,则会被我们的规则认为是合法的
// $test 这里随便写注释
test.testCall()
具体代码如下,详细解析看注释即可:
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: "调用某些对象方法时需要加上注释",
},
// 我们需要使用这个规则的时候给我们传递参数,具体的参数含义可以看上面👆🏻
schema: [{
type: 'object',
properties: {
objName: {
type: 'string',
},
propNames: {
type: 'array',
},
commentName: {
type: 'string',
}
}
}],
// 报错或者告警的时候的提示语
messages: {
callWithoutComment: '"{{ callMethod }}"上需加上"{{ comment }}"开头的注释',
// 带占位符的提示信息
callWithoutRightComment: '"{{ callMethod }}"上的注释"{{ realComment }}"并非"{{ comment }}"开头' // 带占位符的提示信息
}
},
create(context) {
// 这里拿到规则传进来的参数
const options = context.options[0];
// 解构出对象名,方法名和注释名
const {
objName,
propNames,
commentName
} = options;
// 通过sourceCode可以拿到当前节点的很多信息,比如当前节点的注释
const sourceCode = context.getSourceCode();
// 以下是visitor对象,什么是visitor对象可以看看上面的介绍
return {
// 当有方法调用的时候,就会调用此方法且把当前节点传进来
// 比如abc.doSometing这个语句就会调用此方法
CallExpression(node) {
// 比如是abc.doSomething
// curObjName就是abc,curProName就是doSomething
const curObjName = node.callee ? .object ? .name;
const curPropName = node.callee ? .property ? .name;
if (!objName || !propNames) {
return;
}
// 如果不是我们在规则参数里指定的对象名和方法名,则无需校验直接通过
if (curObjName !== objName || !propNames ? .includes(curPropName)) {
return;
}
// 通过sourceCode获取当前这个语句的注释,是一个数组
const comments = sourceCode.getCommentsBefore(node) || [];
const curComment = comments.map(comment = >comment.value).join(' ').trim();
// 假如没加注释,则报错
if (curComment ? .trim() === '') {
context.report({
node,
messageId: 'callWithoutComment',
data: {
callMethod: `$ {
curObjName
}.$ {
curPropName
}`,
comment: commentName,
}
});
return;
}
// 假如加了注释,且是合法的注释,则通过
if (curComment.includes(commentName)) {
return;
}
// 假如加了注释,但是不是合法的注释,也报错
context.report({
node,
messageId: 'callWithoutRightComment',
data: {
callMethod: `$ {
curObjName
}.$ {
curPropName
}`,
comment: commentName,
realComment: curComment,
}
});
}
};
},
};
测试我们的代码
经过上面之后,我们已经把主要代码写完了,此时需要给我们的代码写一些测试案例,做点单元测试,打开test/lib/rules/yourRule.js
,假如你后续新增了一条规则,直接在这个目录下增加相应的测试文件即可。
具体测试代码如下:
const rule = require("../../../lib/rules/yourRule"),
RuleTester = require("eslint").RuleTester;
// 这是传给我们的规则的参数
const options = [{
objName: 'test',
propNames: ['testCall', 'testCall2'],
commentName: '$test',
}]
const ruleTester = new RuleTester();
ruleTester.run("youRule", rule, {
// 这是合法的可以通过规则的案例
valid: [{
code: ` // $test
test.testCall('xxx');`,
// 传给规则的参数
options
},
{
code: ` // $test注释
test.testCall2('xxx');`,
// 传给规则的参数
options
},
],
invalid: [
{
code: ` //
test.testCall('xxx');`,
errors: [{
message: '"test.testCall"上需加上"$test"开头的注释',
type: "CallExpression"
}],
options
},
{
code: ` // $test xxx2
console.log(100)
test.testCall('xxx');`,
errors: [{
message: '"test.testCall"上需加上"$test"开头的注释',
type: "CallExpression"
}],
options
},
{
code: `
console.log(100)
test.testCall('xxx')
// $note xxx3
`,
errors: [{
message: '"test.testCall"上需加上"$test"开头的注释',
type: "CallExpression"
}],
options
},
{
code: `
// xxxx
test.testCall('xxx');
`,
errors: [{
message: '"test.testCall"上的注释"xxxx"并非"$test"开头',
type: "CallExpression"
}],
options
},
],
});
此时就可以直接跑我们的测试案例的,你可以通过node xxx.js
的方式跑,也可以通过vscode来debug。
假如你通过vscode的debug来跑,具体的操作顺序是
点击debug按钮->点创建launch.json->选择Node环境->将launch.json里面的program字段的路径改成你的测试代码的路径即可。
在项目里面使用你的插件
当你通过上面的步骤跑完测试案例没有问题之后,就可以将你的eslint发布到npm上了,具体如何发布,其实就是简单的两步。
npm login
#根据提示登录完之后
npm publish
发布了之后,你就可以在你的项目里安装使用你的插件了,比如你的自定义插件名叫eslint-plugin-myCustomPlugin
安装
npm i -D eslint-plugin-myCustomPlugin
使用
// .eslintrc.js
module.exports = {
// 你的插件
plugins: ['myCustomPlugin'],
rules: {
// 你的具体开启的规则
'myCustomPlugin/myRule': [
'error',
// 你的规则参数
{
objName: 'test',
propNames: 'testCall',
commentName: '$test'
}
],
},
};
使用效果如下,可以看出,当代码里有test.testCall();
但是右没有加$test开头的注释的时候
,我们的规则就会给这行代码报错提示了:
大功告成,完结撒花。等等,还没完,以上说的都是自定义规则,还没说自定义插件呢。
自定义插件
假如说我们自己定义了多条规则,但是像上面的.eslintrc.js
里面要在rules里面一条条手动开启也太麻烦了,此时我们就可以通过插件的config选项去将我们的规则暴露出去了。
实际上自定义插件和自定义规则一样,也是要遵循eslint的固定模板的,如下所示:
插件必须要有一个rules
对象,里面包含我们的自定义的规则
module.exports = {
// 这是插件一定要有的字段
// 里面的规则,就是刚刚我们上面写的规则
rules: {
"yourRule1": {
create: function (context) {
// 这里实现具体代码
}
},
}
};
还有一个重要的选项configs
,这是让我们在项目里面不用一条条开启规则的关键,如下所示
我们就是通过config去封装并开启好多个规则,暴露出去给别的项目使用的,直白点将,就是我们写好了一个.eslintrc.js
,放到configs这个对象里,然后发布到npm上,让别人可以直接使用
module.exports = {
// 里面的一个个config,其实就相当于一个个.eslintrc.js里面的配置
myConfig: {
plugins: ["myPlugin"],
env: ["browser"],
rules: {
semi: "error",
"myPlugin/my-rule": "error",
"eslint-plugin-myPlugin/another-rule": "error"
}
},
myOtherConfig: {
plugins: ["myPlugin"],
env: ["node"],
rules: {
"myPlugin/my-rule": "off",
"eslint-plugin-myPlugin/another-rule": "off"
"eslint-plugin-myPlugin/yet-another-rule": "error"
}
}
};
通过上面的介绍之后,再回到我们刚刚的插件项目的代码,我们打开lib/index.js
这个文件,这就是自定义插件的源码文件。
可以看到现在只有两行代码,这两行代码是脚手架给我们生成的,仅仅是把自定义的规则暴露出去了,并没有设置configs
字段
const requireIndex = require("requireindex");
// 在这里导入了我们上面写的自定义规则,并且一起导出去了
module.exports.rules = requireIndex(__dirname + "/rules");
我们改造一下上面的代码,如下所示
const requireIndex = require("requireindex");
// 在这里导入了我们上面写的自定义规则
const rules = requireIndex(__dirname + "/rules");
module.exports = {
// rules是必须的
rules,
// 增加configs配置
configs: {
// 配置了这个之后,就可以在其他项目中像下面这样使用了
// extends: ['plugin:pluginName/recommended']
recommended: {
// 需要注意,这里一定要有env字段,否则eslint会认为这个config不合法
env: ['browser'],
// 这里面其实就相当于一份你写好的.eslintrc.js文件
// 你可以使用自己的规则,也可以在这里包含eslint的核心规则和第三方开源规则
plugins: ['yourPluginName'],
rules: {
'youPluginName/yourRule1': ['error'],
'youPluginName/yourRule2': ['error'],
}
}
}
}
修改完之后,就可以发布到npm上了,这样一个自定义的插件就完成了,回到项目里修改使用方式:
原先没有configs
的时候,我们是这样用的
module.exports = {
// 你的插件
plugins: ['myCustomPlugin'],
rules: {
// 你的具体开启的规则
'myCustomPlugin/myRule': [
'error',
// 你的规则参数
{
objName: 'test',
propNames: 'testCall',
commentName: '$test'
}
],
},
};
现在你只需要像下面这样用,看起来就清爽多了,这样就算你自定义了很多条规则,使用起来也只需像下面一样一行搞定
module.exports = {
// 你的插件
extends: ['plugin:yourPluginName/recommended'],
};
总结
总的来说就是五步走:
- 通过脚手架创建项目框架
- 在规则目录下新增规则
- 在create方法的返回对象里找到自己需要处理的ast节点,写相应的校验逻辑
- 测试验证自己的逻辑
- 如果有需要的话,在插件的源码文件里增加
configs
配置,批量暴露规则
其实关于自定义插件和自定义规则,核心的地方还是自定义规则,本质上自定义插件最核心的就是把自定义规则暴露出去。而自定义规则则需要你自己去看ast的字段,根据自己需要来写自己的规则代码。
相关文章推荐: