前言
随着项目的日渐庞大,必定导致负责该项目的人员不断增加或者更换,那么就意味着代码风格不尽相同的人员同时参与同一个项目开发,会导致该项目的代码风格是“百花齐放”。
问题来了,如何保持统一的代码风格呢?
这还不简单?
使用ESLint插件,加载各种规则去实时校验,在搭配pre-commit进行git提交校验不就行了吗?
那么问题又来了,当社区所提供的规则,不满足一些定制化程度比较高的规范时,我们就需要利用ESLint的插件机制,来实现自定义规则的校验。
在之前一段时间,针对组内一些比较特殊的规范,实现了一款插件:eslint-plugin-imports-sorter,提供了如下的校验。
-
校验引入顺序:第三方库/官方库 > 绝对路径资源 > 相对路径资源
// error import React from 'react' import { BarEnum } from '../../const' import style from '../index.less' import utils from 'src/utils' import { pick } from 'loadsh' // right import React from 'react' import { pick } from 'loadsh' import utils from 'src/utils' import { BarEnum } from '../../const' import style from '../index.less'
-
不同引入顺序换行分割(可配置)
import React from 'react' import { pick } from 'loadsh' import utils from 'src/utils' import { BarEnum } from '../../const' import style from '../index.less'
-
相同引入顺序,层级排序(可配置)
import React from 'react' import { pick } from 'loadsh' import utils from 'src/utils' import style from '../index.less' import { BarEnum } from '../../const'
-
绝对路径资源配置别名
由于提供了自动修复的逻辑,因此搭配VsCode的配置(可以参考VsCode配置ESLint保存修复),可以实现如下效果。
一、ESLint校验流程
这里预先了解下ESLint配置中,影响最终规则校验的一些常规属性。
extends
中的每一项内容最终都指向了一个和 ESLint 本身配置规则相同的对象, 然后ESLint
会递归的合并所有继承来的配置ESLint
的核心就是规则(rule
),每条规则都是独立,互不影响。- 最终来进行校验的是结合
extends
、rule
、被ESLint注释命令包含的节点
而生成的最终生效配置,调用runRules
生成遍及AST
下规则指定的节点,进行校验。
1、拿到最终生效的规则配置
举个例子🌰
对于规则no-var
继承来的规则:no-var:2
自定义修改的规则:no-var:1
自定义指令修改规则:no-var:0
最终这条规则生效的配置为no-var:0
eslint/lib/linter/linter.js
自定义指令配置结构如下
/* 根据AST+注解生成的自定义指令配置 */
const commentDirectivesConfg = {
/* 局部修改某些规则的配置信息 */
configuredRules: {},
/* 通过 「exported variableName」 指令,声明的暴露出的变量 */
enabledGlobals: {},
exportedVariables: {},
problems: [],
disableDirectives: [
{
type: 'disable-next-line',
line: xxx,
column: xxx,
ruleId: 'ruleId'
}
]
}
2、runRules
所有规则遍历校验
最终每条规则会设置一个监听器,对当前节点进行校验。
总结
- 插件集成了规则+规则配置,规则的核心逻辑的实现是
create
方法中。 ESLint
校验过程的本质:根据最终得到的所有生效的规则配置,对规则选中的类型节点进行遍历校验。
二、拆解成AST(略)
三、插件开发流程
2.1 工欲善其事,必先利其器
这里我们使用Yeoman脚手架以及generator-eslint生成器来生成ESLint插件的默认开发模版。
- 安装依赖
// 安装 Yeoman
(sudo)npm i -g yo
// 安装 eslint 插件模板生成器
(sudo)npm i -g generator-eslint
- 创建模版
// 找到你要创建插件的文件夹
// 初始化模版
yo eslint:plugin
- 创建规则模版
yo eslint:rule
添加一些规则的相关信息
注意事项:插件的名称一般是以eslint-plugin-xxx来命名,但是也支持@xx/eslint-plugin-xx来命名,在插件配置的时候,只需要把开头或者中间的eslint-plugin-省略就好
-
目录结构
生成之后,我们的默认模板应该是这个图中这个样子。
简单说明下,这些目录的作用
- docs/rules: lib/rules 下校验规则的说明文档
- tests/libs/rules: lib/rules下的单元测试
- lib/rules:所有规则
- lib/index: 插件配置
2.2 本地实时调试
由于纯字符串代码中存在某些限制,不完全契合真实环境的
ESLint
校验流程,因此需要链接🔗到本地进行测试。
// 在插件工程下
sudo npm link
链接成功后,我们会得到如下结果
// 切换至项目工程
npm link xxxx
接下来,我们在插件中的改动都会同步至项目工程中
2.3 编写规则逻辑
1、配置插件参数(点我查看):规定了此条规则可接收哪些配置项,是否可自动修复,文档链接地址、错误提示文案等。
比较重要的几个参数
- meta
interface Config {
/* 不同引入方式换行分割:默认值true */
isCheckEmptyLine: boolean
/* 校验层级:默认值false */
isCheckDeepth: boolean
/* 绝对路径别名:默认值['src','@'] */
alias: Array<string>
}
那在使用的时候配置项就是如下格式
-
create
(function) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法。- 如果一个 key 是个节点类型或 selector,在 向下 遍历树时,ESLint 调用 visitor 函数
- 如果一个 key 是个节点类型或 selector,并带有
:exit
,在 向上 遍历树时,ESLint 调用 visitor 函数 - 如果一个 key 是个事件名字,ESLint 为代码路径分析调用 handler 函数 2、找到校验规则失败的条件(点我查看)
-
先明确需要校验的场景在哪类节点上,这是我相对比较重要的一点。 因为某些规则的实现方式可能有多重,但是实现成本一定程度上取决于你设定的
AST
节点类型
// 这里是ESlint支持的全部节点类型,可以通过[https://astexplorer.net/]来查看节点类型
const KNOWN_NODES = new Set([
"AssignmentExpression",
"AssignmentPattern",
"ArrayExpression",
"ArrayPattern",
"ArrowFunctionExpression",
"AwaitExpression",
"BlockStatement",
"BinaryExpression",
"BreakStatement",
"CallExpression",
"CatchClause",
"ChainExpression",
"ClassBody",
"ClassDeclaration",
"ClassExpression",
"ConditionalExpression",
"ContinueStatement",
"DoWhileStatement",
"DebuggerStatement",
"EmptyStatement",
"ExperimentalRestProperty",
"ExperimentalSpreadProperty",
"ExpressionStatement",
"ForStatement",
"ForInStatement",
"ForOfStatement",
"FunctionDeclaration",
"FunctionExpression",
"Identifier",
"IfStatement",
"Literal",
"LabeledStatement",
"LogicalExpression",
"MemberExpression",
"MetaProperty",
"MethodDefinition",
"NewExpression",
"ObjectExpression",
"ObjectPattern",
"Program",
"Property",
"RestElement",
"ReturnStatement",
"SequenceExpression",
"SpreadElement",
"Super",
"SwitchCase",
"SwitchStatement",
"TaggedTemplateExpression",
"TemplateElement",
"TemplateLiteral",
"ThisExpression",
"ThrowStatement",
"TryStatement",
"UnaryExpression",
"UpdateExpression",
"VariableDeclaration",
"VariableDeclarator",
"WhileStatement",
"WithStatement",
"YieldExpression",
"JSXFragment",
"JSXOpeningFragment",
"JSXClosingFragment",
"JSXIdentifier",
"JSXNamespacedName",
"JSXMemberExpression",
"JSXEmptyExpression",
"JSXExpressionContainer",
"JSXElement",
"JSXClosingElement",
"JSXOpeningElement",
"JSXAttribute",
"JSXSpreadAttribute",
"JSXText",
"ExportDefaultDeclaration",
"ExportNamedDeclaration",
"ExportAllDeclaration",
"ExportSpecifier",
"ImportDeclaration",
"ImportSpecifier",
"ImportDefaultSpecifier",
"ImportNamespaceSpecifier",
"ImportExpression"
]);
- 读取配置项,编写错误判断的逻辑。(根据你规则的功能而定) 3、 抛出错误信息及修复行为(点我查看)
是否可修复,取决于该规则校验失败的原因是否偏向于主观化。
举个例子🌰
场景1: 在某些调试过程中,临时修改一些变量,参数,方便调试。但是偶尔会忘记修改回来。
插件功能:打上/* eslint:todo */
注释的节点抛出警告。
上述这样的插件,他的错误就是趋于主观化,你通过上下文不能确定这个错误能否修复。
使用 context.report()
时指定 fix
函数。fix
函数接收一个参数,即一个 fixer
对象,你可以用它来进行修复。例如:
context.report({
node: node,
message: "Missing semicolon",
fix: function(fixer) {
return fixer.insertTextAfter(node, ";");
}
});
重要: 除非规则输出 meta.fixable
属性,ESLint 不会进行修复,即使该规则实现了 fix
函数。
注意事项⚠:
- 避免任何可能改变代码运行时行为和导致其停止工作的修复。
- 做尽可能小的修复。那些不必要的修复可能会与其他修复发生冲突,应该避免。
- 使每条消息只有一个修复。这是强制的,因为你必须从
fix()
返回修复操作的结果。