大家好,我是心锁,23届准毕业生。
近期在尝试编写一个供予项目使用的eslint插件,目的是为了不写一行开发文档即实现项目规范强制落地。
那么如何编写、启动和测试就比较头疼了,于是踩坑了一晚上之后,我决定把相关的开发流程分享出来。
本文旨在探索和完成一轮完整的eslint插件开发历程,确保即便是没有eslint插件开发的读者也可以快速开始eslint插件开发。本文概览如下:
正如大部分教程一样,我们采用yo和generator-eslint来初始化项目。
我们启动项目:
# 基本的脚手架
pnpm add -g yo generator-eslint
本步骤的目的是创建一个eslint插件的基本模板。
需要注意的是,我们运行yo eslint:plugin
并不会自动创建文件夹,所以需要事先创建文件夹:
mkdir eslint-plugin-super-hero
cd ./eslint-plugin-super-hero
运行yo eslint:plugin
需要回答几个问题:
这些问题代表:
.eslintrc
文件中进行插件引用时填写的值。具体就是:完成上述步骤记得安装一下依赖,建议使用pnpm install
pnpm install
pnpm add typescript -D
这应该是本步骤完成后的目录结构图:
运行yo eslint:rule,会为我们生成一条插件下规则的基本模版。
后边的不重要是,随意填,大概率要改的。
运行完该命令,会在lib/rules/xxx.js
生成如下的文件,该文件即我们定义一条规则需要书写代码的地方。
/**
* @fileoverview force the import order
* @author import-sorter // 合着作者名在这儿呗
*/
"use strict";
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: "force the import order",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
},
create(context) {
// 变量定义区
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// 工具函数区
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
// 用来真正分析AST的函数位置
};
},
};
来看一下meta,初始生成的meta是以下结构。
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: "force the import order",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
},
这些是一条规则的基本信息,以eslint官方的文档来看,meta对象包含规则的元数据:
"extends": "eslint:recommended"
中的属性是否启用规则。其他的属性我们在开发的时候会讲到,这里主要补充一个message
s参数,因为这是一个必要参数,但是示例中没有。
messageId
,value值即为对应的报错消息。create函数是插件真正工作的地方,create函数要求返回一个对象,对象的Key值是AST节点的类型,Value值则是调用的函数。
create(context) {
return {
// 访问某种类型的AST节点时会调用的函数
};
},
其中,create方法的cotext参数类型如下,这里包含了大量我们可能用得到的接口,这里可以简单看一下,实际开发的时候可以针对需要看文档。
interface RuleContext {
id: string;
options: any[]; //schema配置后,可以看到参数
settings: { [name: string]: any }; // 共享设置
parserPath: string;
parserOptions: Linter.ParserOptions;
parserServices: SourceCode.ParserServices; //解析器提供的规则服务的对象
getAncestors(): ESTree.Node[]; //获取上一级AST节点
getDeclaredVariables(node: ESTree.Node): Scope.Variable[]; //获取变量表
getFilename(): string; //获取文件名称,可以实现文件名与代码关联规范
getPhysicalFilename(): string; //对文件进行 linting 时,它返回磁盘上文件的完整路径,不包含任何代码块信息
getCwd(): string; //当前工作目录的目录的路径
getScope(): Scope.Scope; // 获取作用域的信息。官方声明弃用,未来可能移除,可通过SourceCode.getScope(node)替代
getSourceCode(): SourceCode; // 获取源代码的AST树
markVariableAsUsed(name: string): boolean; //当前作用域中给定名称的变量标记为已使用。这会影响no-unused-vars规则。true如果找到具有给定名称的变量并将其标记为已使用,则返回,否则返回false。
report(descriptor: ReportDescriptor): void; //报告代码中的问题,核心函数,在ReportDescriptor中我们可以声明更多信息,包括错误提示、修复方式等
}
这里边最核心的就是context.report
方法,用于向eslint报告错误,同时也可以通过该函数传递fix方法用于自动修复错误。
interface ReportDescriptorOptions extends ReportDescriptorOptionsBase {
suggest?: SuggestionReportDescriptor[] | null | undefined;
}
type ReportDescriptor = ReportDescriptorMessage & ReportDescriptorLocation & ReportDescriptorOptions;
type ReportDescriptorMessage = { message: string } | { messageId: string };
type ReportDescriptorLocation =
| { node: ESTree.Node }
| { loc: AST.SourceLocation | { line: number; column: number } };
type ReportFixer = (fixer: RuleFixer) => null | Fix | IterableIterator<Fix> | Fix[];
interface SuggestionReportOptions {
data?: { [key: string]: string };
fix: ReportFixer;
}
type SuggestionDescriptorMessage = { desc: string } | { messageId: string };
type SuggestionReportDescriptor = SuggestionDescriptorMessage & SuggestionReportOptions;
通过接口定义我们可以知道,一般情况下,我们可以通过下述代码报告错误。其中的messageId
对应我们定义在meta
中的messages属性
context.report({
node,
messageId: "xxxxxx",
fix(fixer) {
return fixer.insertTextBefore(
sourceCode.ast.body[0],
newImportStatement
);
},
});
在我们运行3.2 的时候,同步生成了测试文件,这也是我们调试代码最便携的地方。下边是生成的代码中最主要的部分。
const ruleTester = new RuleTester();
ruleTester.run("import-sorter", rule, {
valid: [
// give me some code that won't trigger a warning
],
invalid: [
{
code: "-",
errors: [{ message: "Fill me in.", type: "Me too" }],
},
],
});
valid指那些不会导致警告和报错的样例,我想了想,这部分总觉得不是很重要没有很细致研究。主要在invalid,invalid接受一个数组,里边是会导致eslint报错的代码:
interface InvalidTestCase extends ValidTestCase {
errors: number | Array<TestCaseError | string>;
output?: string | null | undefined;
}
其中,errors中除了可以和基本样例一样对message和type进行测试之外,也支持下边的参数
interface TestCaseError {
message?: string | RegExp | undefined;
messageId?: string | undefined;
type?: string | undefined; //报错AST节点的类型
data?: any;
line?: number | undefined;
column?: number | undefined;
endLine?: number | undefined;
endColumn?: number | undefined;
suggestions?: SuggestionOutput[] | undefined;
}
实际操作比理论更有效,我们尝试做一个文件导入排序规则。
import操作常见于页面的最顶部,我们在导入的时候,应该也会发现如果我们随意排序这些操作,对于阅读并不是很友好。如下边这份代码。短短六行就是六种不同类型的导入。
import { io } from 'socket.io-client'
import { history } from '@renderer/utils'
import type { Socket } from 'socket.io-client'
import type { IMMessage } from './type'
import styles from './index.module.scss'
import './index.scss'
第一行,是第三方包的代码导入。第二行,是通过alias实现的绝对路径项目代码导入。第三行代码是第三方包的类型导入。第四行是项目相对路径的类型导入。第五行是css module静态资源导入,第六行是静态资源导入。
而显然,我们还可能遇见更多的导入类型。
目前我总结了下边的几种类型,按照组合区分优先级。
首先是常规的文件导入方式:
然后是导入类型:
以及下边导入方法的区分:
同时,我们也希望能有一些特殊的规则,作为一名React技术栈走得比较深的前端玩家,我们还可以添加一条规则,让React导入必须在第一条。
由于我们的插件,需要识别import type { xxx } from “xxx”;
我们需要准备typescript解析器。
pnpm add @typescript-eslint/parser
同时,修改我们的配置文件,增加parser
"use strict";
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:eslint-plugin/recommended",
"plugin:node/recommended",
],
env: {
node: true,
},
overrides: [
{
files: ["tests/**/*.js"],
env: {
mocha: true,
},
},
],
};
const testConfig = {
parser: require.resolve('@typescript-eslint/parser'),
env: {
es2020: true,
node: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
};
module.exports = {
testConfig,
};
之后在新建测试文件tests/lib/rules/xxx.js
时,应该添加以下代码:
首先,我们需要定义一些常量,以便在后面的代码中使用。这些常量定义了不同类型的导入语句和规则。
// 导入种类
const ImportType = {
ThirdParty: 1,
Absolute: 2,
Relative: 3,
Unknown: 4,
};
// 导入方式
const ImportMethod = {
Destructuring: 1,
Default: 2,
Namespace: 3,
Mixed: 4,
SideEffect: 5,
};
// React导入必须放在第一位
const ReactImportRegex = /^react/;
接下来,我们需要编写一个函数,该函数将导入语句按照类型和规则进行排序。这个函数有两个参数:一个是导入语句的数组,另一个是ESLint的上下文对象。该函数的主要流程如下:
距离我们实现代码只有一点点了,在具体实现代码之前,我们需要学习一下AST,否则想写下去是比较困难的。
首先,我们需要知道AST的概念,AST是抽象语法树。它是我们程序源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
——听起来是不是很抽象,还好我们并不需要过于深入了解其中原理。
我们现在只需要知道,ESLint的工作流程。
首先会把我们的源代码通过parser解析器转换成AST语法树。没错就是.eslintrc.js
中常见的parser字段,目前我们需要关心的只有如何为我们的插件选择一个第三方解析器,比如“@typescript-eslint/parser”就是一个解析器。
以下边这段代码示例:
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import "./App.scss";
import { Spin } from "antd";
import { useSDMall } from "@/hooks";
我们直接通过AST在线解析,经过AST解析,出来的结果如下(如果你跑不了,注意网站中可以切换解析器):
可以看出,我们只需要在首层的Program节点访问源代码,然后遍历body,由于Import只能在文件最顶部,所以访问从头到最后一个import文件,在排序后重新插入即可。
贴一下第一行的AST代码,实际上在入门eslint规则开发时,我们并不需要记忆下各个节点都有哪些属性,大多是需要的时候现查现场解析,根据需要的节点特征配合https://github.com/estree/estree查阅即可。
{
"type": "ImportDeclaration",
"source": {
"type": "Literal",
"value": "react",
"raw": "\"react\"",
"range": [
18,
25
]
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"local": {
"type": "Identifier",
"name": "React",
"range": [
7,
12
]
},
"range": [
7,
12
]
}
],
"importKind": "value",
"assertions": [],
"range": [
0,
26
]
},
那么现在我们知道了两件事,第一,我们要在访问Program节点时修改AST树。第二,我们知道了要被修改的Node的type是ImportDeclaration。
那应该如何访问Program节点呢?
这个非常好办,我们的rules文件中的create函数,返回值只需要返回一个对象,对象的key值为需要访问节点的type,value则是我们的访问器函数,也就是类似这种结构:
create(context) {
...
return {
Program() {
...
},
};
},
那么反正我们现在是明白了以下几点:
接下来,我们就可以开始实现按照规则对导入语句进行排序的函数了。具体实现代码可以参考下面:
下面是该函数的代码实现:
function sortImports(importStatements, context) {
// 用于存储排序后的导入语句
const sortedImports = [];
// 用于存储不同种类的导入语句
const importGroups = {
[ImportType.ThirdParty]: [],
[ImportType.Absolute]: [],
[ImportType.Relative]: [],
[ImportType.Unknown]: [],
};
// 将导入语句分组和分类
for (const importStatement of importStatements) {
const importPath = importStatement.source.value;
const importType = getImportType(importPath, context);
importGroups[importType].push(importStatement);
}
// React相关导入必须放在第一位
const reactImports = importGroups[ImportType.ThirdParty].filter(
(importStatement) => ReactImportRegex.test(importStatement.source.value)
);
if (reactImports.length > 0) {
sortedImports.push(reactImports);
}
// 按照规则和种类对导入语句进行排序
const sortedAbsoluteImports = sortAbsoluteImports(
importGroups[ImportType.Absolute]
);
const sortedRelativeImports = sortRelativeImports(
importGroups[ImportType.Relative]
);
const sortedThirdPartyImports = sortThirdPartyImports(
importGroups[ImportType.ThirdParty].filter(
(importStatement) =>
!ReactImportRegex.test(importStatement.source.value)
)
);
const sortedUnknownImports = importGroups[ImportType.Unknown];
const sortMethodTask = [
sortedThirdPartyImports,
sortedAbsoluteImports,
sortedRelativeImports,
sortedUnknownImports,
];
for (const importStatements of sortMethodTask) {
// 用于存储不同方式的导入语句
const importMethods = {
[ImportMethod.Destructuring]: [],
[ImportMethod.Default]: [],
[ImportMethod.Namespace]: [],
[ImportMethod.Mixed]: [],
[ImportMethod.SideEffect]: [],
};
const resultList = [];
for (const importStatement of importStatements) {
const importMethod = getImportMethod(importStatement);
importMethods[importMethod].push(importStatement);
}
resultList.push(...importMethods[ImportMethod.SideEffect]);
resultList.push(...importMethods[ImportMethod.Default]);
resultList.push(...importMethods[ImportMethod.Namespace]);
resultList.push(...importMethods[ImportMethod.Destructuring]);
resultList.push(...importMethods[ImportMethod.Mixed]);
sortedImports.push(resultList);
}
return sortedImports;
}
在这个函数中,我们使用了一些辅助函数,以便判断导入语句的种类、方式和规则。这些辅助函数的代码如下:
function getImportType(importPath, context) {
if (/^\.\//.test(importPath)) {
// 相对路径
return ImportType.Relative;
} else if (/^@\//.test(importPath)) {
// 绝对路径
return ImportType.Absolute;
} else if (
context
.getScope()
.through.some((ref) => ref.identifier.name === importPath)
) {
// 未知类型
return ImportType.Unknown;
} else {
// 第三方库
return ImportType.ThirdParty;
}
}
function getImportMethod(importStatement) {
const { specifiers } = importStatement;
if (
specifiers.some(
(specifier) => specifier.type === "ImportDefaultSpecifier"
)
) {
// 默认导入
return ImportMethod.Default;
} else if (
specifiers.some(
(specifier) => specifier.type === "ImportNamespaceSpecifier"
)
) {
// 命名导入
return ImportMethod.Namespace;
} else if (specifiers.length === 0) {
// 无副作用导入
return ImportMethod.SideEffect;
} else if (
specifiers.every((specifier) => specifier.type === "ImportSpecifier")
) {
// 解构导入
return ImportMethod.Destructuring;
} else {
// 混合导入
return ImportMethod.Mixed;
}
}
function sortAbsoluteImports(absoluteImports) {
return absoluteImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortRelativeImports(relativeImports) {
return relativeImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortThirdPartyImports(thirdPartyImports) {
return thirdPartyImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortImports(importStatements, context) {
// 用于存储排序后的导入语句
const sortedImports = [];
// 用于存储不同种类的导入语句
const importGroups = {
[ImportType.ThirdParty]: [],
[ImportType.Absolute]: [],
[ImportType.Relative]: [],
[ImportType.Unknown]: [],
};
// 将导入语句分组和分类
for (const importStatement of importStatements) {
const importPath = importStatement.source.value;
const importType = getImportType(importPath, context);
importGroups[importType].push(importStatement);
}
// React导入必须放在第一位
const reactImports = importGroups[ImportType.ThirdParty].filter(
(importStatement) => ReactImportRegex.test(importStatement.source.value)
);
if (reactImports.length > 0) {
sortedImports.push(reactImports);
}
// 按照规则和种类对导入语句进行排序
const sortedAbsoluteImports = sortAbsoluteImports(
importGroups[ImportType.Absolute]
);
const sortedRelativeImports = sortRelativeImports(
importGroups[ImportType.Relative]
);
const sortedThirdPartyImports = sortThirdPartyImports(
importGroups[ImportType.ThirdParty].filter(
(importStatement) =>
!ReactImportRegex.test(importStatement.source.value)
)
);
const sortedUnknownImports = importGroups[ImportType.Unknown];
// 将排序后的导入语句存储到数组中
const sortMethodTask = [
sortedThirdPartyImports,
sortedAbsoluteImports,
sortedRelativeImports,
sortedUnknownImports,
];
for (const importStatements of sortMethodTask) {
// 用于存储不同方式的导入语句
const importMethods = {
[ImportMethod.Destructuring]: [],
[ImportMethod.Default]: [],
[ImportMethod.Namespace]: [],
[ImportMethod.Mixed]: [],
[ImportMethod.SideEffect]: [],
};
const resultList = [];
for (const importStatement of importStatements) {
const importMethod = getImportMethod(importStatement);
importMethods[importMethod].push(importStatement);
}
resultList.push(...importMethods[ImportMethod.SideEffect]);
resultList.push(...importMethods[ImportMethod.Default]);
resultList.push(...importMethods[ImportMethod.Namespace]);
resultList.push(...importMethods[ImportMethod.Destructuring]);
resultList.push(...importMethods[ImportMethod.Mixed]);
sortedImports.push(resultList);
}
return sortedImports;
}
最后,我们需要将该函数与ESLint集成,以便在规则中使用它。在ESLint规则中,我们可以使用context.getSourceCode()
方法获取源代码,并使用sortImports()
函数对导入语句进行排序。下边这是一份完整的代码。
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "force the import order",
recommended: false,
url: null,
},
fixable: "code",
schema: [],
messages: {
"import-sorter": "Import statements are not sorted",
},
},
create(context) {
function getImportType(importPath, context) {
if (/^\.\//.test(importPath)) {
// 相对路径
return ImportType.Relative;
} else if (/^@\//.test(importPath)) {
// 绝对路径
return ImportType.Absolute;
} else if (
context
.getScope()
.through.some((ref) => ref.identifier.name === importPath)
) {
// 未知类型
return ImportType.Unknown;
} else {
// 第三方库
return ImportType.ThirdParty;
}
}
function getImportMethod(importStatement) {
const { specifiers } = importStatement;
if (
specifiers.some(
(specifier) => specifier.type === "ImportDefaultSpecifier"
)
) {
// 默认导入
return ImportMethod.Default;
} else if (
specifiers.some(
(specifier) => specifier.type === "ImportNamespaceSpecifier"
)
) {
// 命名导入
return ImportMethod.Namespace;
} else if (specifiers.length === 0) {
// 无副作用导入
return ImportMethod.SideEffect;
} else if (
specifiers.every((specifier) => specifier.type === "ImportSpecifier")
) {
// 解构导入
return ImportMethod.Destructuring;
} else {
// 混合导入
return ImportMethod.Mixed;
}
}
function sortAbsoluteImports(absoluteImports) {
return absoluteImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortRelativeImports(relativeImports) {
return relativeImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortThirdPartyImports(thirdPartyImports) {
return thirdPartyImports.sort((a, b) => {
const aPath = a.source.value;
const bPath = b.source.value;
if (aPath < bPath) {
return -1;
} else if (aPath > bPath) {
return 1;
} else {
return 0;
}
});
}
function sortImports(importStatements, context) {
// 用于存储排序后的导入语句
const sortedImports = [];
// 用于存储不同种类的导入语句
const importGroups = {
[ImportType.ThirdParty]: [],
[ImportType.Absolute]: [],
[ImportType.Relative]: [],
[ImportType.Unknown]: [],
};
// 将导入语句分组和分类
for (const importStatement of importStatements) {
const importPath = importStatement.source.value;
const importType = getImportType(importPath, context);
importGroups[importType].push(importStatement);
}
// React导入必须放在第一位
const reactImports = importGroups[ImportType.ThirdParty].filter(
(importStatement) => ReactImportRegex.test(importStatement.source.value)
);
if (reactImports.length > 0) {
sortedImports.push(reactImports);
}
// 按照规则和种类对导入语句进行排序
const sortedAbsoluteImports = sortAbsoluteImports(
importGroups[ImportType.Absolute]
);
const sortedRelativeImports = sortRelativeImports(
importGroups[ImportType.Relative]
);
const sortedThirdPartyImports = sortThirdPartyImports(
importGroups[ImportType.ThirdParty].filter(
(importStatement) =>
!ReactImportRegex.test(importStatement.source.value)
)
);
const sortedUnknownImports = importGroups[ImportType.Unknown];
// 将排序后的导入语句存储到数组中
const sortMethodTask = [
sortedThirdPartyImports,
sortedAbsoluteImports,
sortedRelativeImports,
sortedUnknownImports,
];
for (const importStatements of sortMethodTask) {
// 用于存储不同方式的导入语句
const importMethods = {
[ImportMethod.Destructuring]: [],
[ImportMethod.Default]: [],
[ImportMethod.Namespace]: [],
[ImportMethod.Mixed]: [],
[ImportMethod.SideEffect]: [],
};
const resultList = [];
for (const importStatement of importStatements) {
const importMethod = getImportMethod(importStatement);
importMethods[importMethod].push(importStatement);
}
resultList.push(...importMethods[ImportMethod.SideEffect]);
resultList.push(...importMethods[ImportMethod.Default]);
resultList.push(...importMethods[ImportMethod.Namespace]);
resultList.push(...importMethods[ImportMethod.Destructuring]);
resultList.push(...importMethods[ImportMethod.Mixed]);
sortedImports.push(resultList);
}
return sortedImports;
}
return {
Program() {
const sourceCode = context.getSourceCode();
const importStatements = sourceCode.ast.body.filter(
(node) => node.type === "ImportDeclaration"
);
if (importStatements.length <= 1) {
return;
}
const comments = sourceCode.getAllComments();
const firstImportIndex = importStatements[0].range[0];
const leadingComments = comments.filter(
(comment) => comment.range[1] < firstImportIndex
);
const hasDisable = leadingComments.some(
(comment) =>
comment.value === " eslint-disable " ||
comment.value.includes("eslint-disable import-sorter")
);
if (hasDisable) {
return;
}
const sortedImports = sortImports(importStatements, context);
const importCode = sortedImports
.filter((result) => result.length > 0)
.map((result) =>
result
.map((importStatement) => sourceCode.getText(importStatement))
.join("\n")
)
.join("\n\n");
const oldImportCode = importStatements
.map((importStatement) => sourceCode.getText(importStatement))
.join("");
if (
oldImportCode.replaceAll("\n", "") === importCode.replaceAll("\n", "")
)
return;
const start = importStatements[0].range[0];
const end = importStatements[importStatements.length - 1].range[1];
context.report({
loc: { start, end },
messageId: "import-sorter",
fix(fixer) {
return fixer.replaceTextRange([start, end], importCode);
},
});
},
};
},
};
在这个规则中,我们使用了ESLint的fixable
属性,以便在规则报告中提供自动修复的选项。如果用户选择修复,ESLint将使用sortImports()
函数对导入语句进行排序,并替换源代码中的导入语句。替换的时候,我们用到了前文说过的context.report
在report方法中声明并完成一个fix函数,fix函数中可以返回多个fixer完成修复。
显然,并不是所有的项目都是以@/
表示绝对路径,所以我们以这个需求作为示例,演示如何给eslint规则引入参数。
首先,我们需要修改meta
,我们添加了用于定义绝对路径前缀的可选项。
meta: {
type: "suggestion",
docs: {
description: "force the import order",
recommended: false,
url: null,
},
fixable: "code",
schema: [
// custom absolute path prefix
{
type: "object",
properties: {
absolutePathPrefix: {
type: "string",
},
},
additionalProperties: false,
},
],
messages: {
"import-sorter": "import statements are not sorted",
},
},
这个例子中,我们为absolutePathPrefix
指定了值"@/src"
,表示项目中的绝对路径都以"@/src"
开头。在规则代码中,我们使用该值来判断导入语句的类型。这使得该规则适用于不同的
{
"rules": {
"import-sorter": [
"error",
{
"absolutePathPrefix": "@/src"
}
]
}
}
这个例子中,我们为absolutePathPrefix
指定了值"@/src"
,表示项目中的绝对路径都以"@/src"
开头。在规则代码中,我们使用该值来判断导入语句的类型。这使得该规则适用于不同的项目。另外,我们也可以尝试将不同的排序逻辑抽象为单独的函数,以便更好地重用和测试。同时,我们也可以添加更多的选项,例如允许用户自定义排序规则,或者在某些情况下忽略某些导入语句。
在开发过程中你或许会发现,为什么你写的规则没有生效?
原因很简单,规则没有配置,记得extends
属性么?我们应当导出一份推荐属性用来方便插件使用。下边是一个简单的例子:
module.exports.configs = {
recommended: {
rules: {
"super-hero/import-sorter": "error"
}
}
}
这之后要使用,只需要在你的.eslintrc.js
配置文件中,添加以下内容来使用这个插件和规则:
module.exports = {
plugins: ["super-hero"],
extends: ["plugin:super-hero/recommended"],
rules: {
// 在这里添加其他规则
},
};
这将启用super-hero插件和其中的推荐规则。你也可以在rules
中添加其他自定义规则。
解决了这个小插曲,我们试着进行调试,方便地调试有利于我们更好地开发。
npx eslint --fix ./xxx.ts
这方法就跟手打C编译器编译命令再启动一样,我们可以试试vscode eslint插件
前端开发者们大多安装了该插件,我们可以设定插件的restart快捷键,在每次更新自定义插件时快速重启,该插件会在我们进入任意文件时自动执行一次eslint插件找到问题。
官方模版中的的mocha,本身即具备了调试能力,在其中完全可以打印、debugger。由于mocha提供了非常便携的--inspect-brk
选项,我们添加一行debug
命令
mocha tests --recursive --inspect-brk
此时,当弹出该消息,我们可以打开任意一个页面的Chrome的控制台。
单击上述按钮,我们就可以在熟悉的控制台debugger node代码了。使用时,可以在代码需要断点的地方输入debugger
即可调试。
这份代码显然具有多个可改进的问题。
/* eslint-disable */
屏蔽规则,我也很困惑为什么该规则在import-sorter
上不生效,难道是因为我访问Program节点?我的其他没有访问Program节点,是可以通过disable
屏蔽的。sortImports()
函数中,我们将导入语句分组并排序,然后将它们保存到一个数组中。这种方法比较简单,但是效率可能不够高。通过eslint plugin的强制规范,我们可以让项目具备更强有效的规范性,一位新人将技术文档吃透的时间成本、导致代码混乱熵增加的程度,完全可以用代码的形式大幅降低与遏制,让技术文档不必形于markdown文档,而是贯彻在每一个角落。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有