自定义 ESLint 插件:禁止直接发起 fetch
或 axios
请求
这个自定义 ESLint 插件的目的是强制前端项目中的所有数据请求都必须通过一个统一的 apiService
模块进行,而不能在组件或其他非 apiService
文件中直接调用 fetch
或 axios
。
首先创建一个名为 eslint-plugin-my-project
的插件,其中包含一个名为 no-direct-api-calls
的规则。
插件结构
首先,创建一个新的文件夹作为你的 ESLint 插件的根目录,例如 my-eslint-plugin/
。
perl
my-eslint-plugin/
├── index.js
└── rules/
└── no-direct-api-calls.js
1. my-eslint-plugin/index.js
这是插件的入口文件,它负责导出插件中包含的所有规则。
js
// my-eslint-plugin/index.js
module.exports = {
rules: {
// 导出你的规则,键名是规则的名称,值是规则的实现
'no-direct-api-calls': require('./rules/no-direct-api-calls'),
},
// 如果你有预设配置(例如 recommended),可以在这里添加
// configs: {
// recommended: {
// plugins: ['my-project'],
// rules: {
// 'my-project/no-direct-api-calls': 'error',
// },
// },
// },
};
代码讲解:
rules
: 这是一个对象,其中包含了你的插件提供的所有 ESLint 规则。'no-direct-api-calls'
: 这是你的规则的名称。require('./rules/no-direct-api-calls')
: 引入了实际实现规则逻辑的文件。
2. my-eslint-plugin/rules/no-direct-api-calls.js
这是规则的核心实现文件。它定义了规则的元数据、选项以及如何遍历 AST (抽象语法树) 来检测违规行为。
js
// my-eslint-plugin/rules/no-direct-api-calls.js
const path = require('path'); // Node.js 内置模块,用于处理文件路径
module.exports = {
meta: {
type: 'problem', // 规则的类型:'problem' (可能导致错误), 'suggestion' (建议), 'layout' (格式)
docs: {
description: 'Disallow direct fetch or axios calls outside of designated API service modules.', // 规则的简短描述
category: 'Best Practices', // 规则所属的类别
recommended: true, // 是否推荐在推荐配置中启用此规则
url: 'https://example.com/no-direct-api-calls', // 规则的详细文档链接(可选)
},
fixable: null, // 规则是否可自动修复。'code' 表示可修复,null 表示不可修复
schema: [ // 规则的配置选项,使用 JSON Schema 定义
{
type: 'object',
properties: {
allowedApiServiceFiles: { // 允许直接发起 API 请求的文件路径或模式列表
type: 'array',
items: {
type: 'string', // 数组的每个元素都是字符串
},
description: 'List of file paths or patterns where direct API calls are allowed (e.g., ["src/apiService.js", "src/utils/http.ts"]).',
},
},
additionalProperties: false, // 不允许额外的属性
},
],
messages: { // 规则报告的错误消息模板
noDirectApiCalls: "Direct '{{calleeName}}' calls are forbidden. Please use the designated API service module.",
},
},
create(context) {
// context 对象提供了规则执行所需的所有信息和工具
const options = context.options[0] || {}; // 获取规则的第一个选项对象
const allowedApiServiceFiles = options.allowedApiServiceFiles || []; // 获取允许的 API 服务文件列表,默认为空数组
const currentFilePath = context.getFilename(); // 获取当前正在 lint 的文件的完整路径
// 规范化当前文件路径,以便与配置中的路径进行可靠比较
const normalizedCurrentFilePath = path.normalize(currentFilePath);
// 检查当前文件是否在允许直接发起 API 请求的白名单中
// 如果当前文件是白名单中的文件,则此规则不应报告任何问题
const isAllowedFile = allowedApiServiceFiles.some(allowedPath => {
// 将配置中的允许路径也进行规范化处理
const normalizedAllowedPath = path.normalize(allowedPath);
// 使用 includes 方法检查当前文件路径是否包含允许的路径片段
// 例如,如果 allowedPath 是 "src/apiService.js",那么 "project/src/apiService.js" 会匹配
// 或者如果 allowedPath 是 "api/",那么 "project/src/api/users.js" 会匹配
return normalizedCurrentFilePath.includes(normalizedAllowedPath);
});
if (isAllowedFile) {
// 如果当前文件在白名单中,则返回空对象,表示不执行任何 AST 遍历
// 规则在此文件上不生效
return {};
}
// 返回一个 visitor 对象,其中定义了在 AST 遍历过程中要访问的节点类型
return {
// CallExpression 节点表示函数调用,例如 fetch() 或 axios.get()
CallExpression(node) {
let calleeName = ''; // 用于存储被调用的函数名(如 'fetch' 或 'axios.get')
// 1. 检查是否是全局的 fetch 调用
// node.callee 是被调用的表达式,如果它是 Identifier 类型且名为 'fetch'
if (node.callee.type === 'Identifier' && node.callee.name === 'fetch') {
calleeName = 'fetch';
}
// 2. 检查是否是 axios 的成员方法调用 (如 axios.get, axios.post 等)
// node.callee 是 MemberExpression 类型 (例如 axios.get)
else if (node.callee.type === 'MemberExpression') {
const object = node.callee.object; // MemberExpression 的对象部分 (例如 axios)
const property = node.callee.property; // MemberExpression 的属性部分 (例如 get)
// 检查对象是否是名为 'axios' 的 Identifier
if (object.type === 'Identifier' && object.name === 'axios' &&
// 检查属性是否是 Identifier 类型,并且其名称在允许的 axios 方法列表中
property.type === 'Identifier' &&
['get', 'post', 'put', 'delete', 'patch', 'request'].includes(property.name)) {
calleeName = `axios.${property.name}`;
}
}
// 如果找到了不允许直接调用的 API 函数 (fetch 或 axios.method)
if (calleeName) {
// 使用 context.report 方法报告一个 ESLint 错误
context.report({
node: node, // 报告错误的 AST 节点
messageId: 'noDirectApiCalls', // 错误消息的 ID,对应 meta.messages 中的键
data: { calleeName }, // 传递给消息模板的数据
});
}
},
};
},
};
代码讲解:
-
meta
对象:type
: 规则的分类,有助于 ESLint 知道如何处理它。docs
: 包含规则的描述、类别和是否推荐等信息。schema
: 定义规则的配置选项。这里我们定义了一个allowedApiServiceFiles
数组,用于指定哪些文件允许直接进行 API 调用。messages
: 定义了规则报告错误时使用的消息模板。{{calleeName}}
是一个占位符,会在报告时被实际的函数名替换。
-
create(context)
方法:-
这是规则的核心逻辑。ESLint 会为每个被 lint 的文件调用这个方法。
-
context
对象提供了访问当前文件信息、报告错误、获取规则选项等功能。 -
文件路径检查 :首先,它获取当前文件的路径 (
context.getFilename()
) 和规则配置中允许的 API 服务文件列表 (allowedApiServiceFiles
)。如果当前文件在白名单中,规则会直接返回一个空对象{}
,这意味着它不会对该文件执行任何检查。这确保了你的apiService
模块本身可以自由地使用fetch
或axios
。 -
AST 遍历 :
return { CallExpression(node) { ... } }
定义了一个"访问器"。当 ESLint 遍历 AST 并在当前节点找到一个CallExpression
(函数调用) 时,它会执行CallExpression
方法中的逻辑。 -
识别
fetch
和axios
调用:fetch
:检查node.callee
是否是一个名为fetch
的Identifier
。axios
方法:检查node.callee
是否是一个MemberExpression
(例如axios.get
),然后进一步检查其object
是否是axios
,property
是否是get
、post
等允许的 HTTP 方法。
-
报告错误 :如果检测到不允许的 API 调用,
context.report()
方法会被调用,向 ESLint 报告一个错误,并附带相应的节点和错误消息。
-
3. 将插件引入你的业务项目
假设你的业务项目目录为 my-frontend-app/
。
方法一:作为本地包通过 file:
协议引用 (推荐)
-
在插件目录 (
my-eslint-plugin/
) 添加package.json
文件:json// my-eslint-plugin/package.json { "name": "eslint-plugin-my-project", // 插件的 npm 包名 "version": "1.0.0", "main": "index.js", "private": true // 标记为私有,防止意外发布 }
-
在你的业务项目根目录的
package.json
中添加依赖:json// my-frontend-app/package.json { "name": "my-frontend-app", "version": "1.0.0", "devDependencies": { "eslint": "^8.0.0", "eslint-plugin-my-project": "file:./my-eslint-plugin" // 指向你的插件目录 } }
注意: 这里的
file:./my-eslint-plugin
假设my-eslint-plugin
文件夹与my-frontend-app
文件夹在同一级。如果my-eslint-plugin
在my-frontend-app
内部,则路径为file:./path/to/my-eslint-plugin
。 -
安装依赖 :在你的业务项目根目录执行
npm install
或yarn install
。
4. 配置 .eslintrc.*
文件
在你的业务项目的 .eslintrc.js
(或 .eslintrc.json
) 文件中配置 ESLint 来使用你的新插件和规则。
js
// my-frontend-app/.eslintrc.js
module.exports = {
// ... 其他 ESLint 配置,例如 parser, parserOptions, env, extends 等
plugins: [
'my-project', // 引用你的插件,对应插件 package.json 中的 "name" 字段 (去掉了 eslint-plugin- 前缀)
],
rules: {
// 启用你的自定义规则,并配置允许的 API 服务文件
'my-project/no-direct-api-calls': ['error', {
allowedApiServiceFiles: [
'src/services/api.js', // 你的统一 API 服务文件路径
'src/utils/http.ts', // 或者另一个 HTTP 工具文件
// 你可以添加更多允许的文件或目录模式
],
}],
// ... 其他规则
},
};
配置讲解:
-
plugins: ['my-project']
: 告诉 ESLint 加载名为eslint-plugin-my-project
的插件。 -
'my-project/no-direct-api-calls'
: 启用你的规则。'error'
: 表示当规则被违反时,ESLint 会报告一个错误。你也可以设置为'warn'
。{ allowedApiServiceFiles: [...] }
: 这是传递给规则的选项,对应meta.schema
中定义的allowedApiServiceFiles
。你需要在这里填写你项目中实际的 API 服务模块的文件路径。
5. 示例代码
会触发 ESLint 错误的代码 (my-frontend-app/src/components/UserList.jsx
):
jsx
// my-frontend-app/src/components/UserList.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios'; // 假设你安装了 axios
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// ❌ 错误:直接发起了 axios 请求
axios.get('/api/users')
.then(response => setUsers(response.data))
.catch(error => console.error('Error fetching users:', error));
// ❌ 错误:直接发起了 fetch 请求
fetch('/api/products')
.then(response => response.json())
.then(data => console.log('Products:', data))
.catch(error => console.error('Error fetching products:', error));
}, []);
return (
<div>
<h1>User List</h1>
{/* ... */}
</div>
);
}
export default UserList;
符合规则的代码 (my-frontend-app/src/services/api.js
):
js
// my-frontend-app/src/services/api.js (这个文件在 allowedApiServiceFiles 中)
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
timeout: 10000,
});
export const getUsers = () => {
return api.get('/users'); // ✅ 允许:在指定的 API 服务文件中调用 axios
};
export const getProducts = () => {
return fetch('/products').then(res => res.json()); // ✅ 允许:在指定的 API 服务文件中调用 fetch
};
export const createSomething = (data) => {
return api.post('/something', data); // ✅ 允许
};
符合规则的代码 (my-frontend-app/src/components/UserList.jsx
):
js
// my-frontend-app/src/components/UserList.jsx
import React, { useEffect, useState } from 'react';
import { getUsers } from '../services/api'; // 从统一的 apiService 模块导入
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// ✅ 正确:通过统一的 apiService 模块发起请求
getUsers()
.then(response => setUsers(response.data))
.catch(error => console.error('Error fetching users:', error));
}, []);
return (
<div>
<h1>User List</h1>
{/* ... */}
</div>
);
}
export default UserList;
当你运行 ESLint 时,它会报告 UserList.jsx
中直接调用 axios.get
和 fetch
的错误,并提示你使用指定的 API 服务模块。