自定义 ESLint 插件:禁止直接发起 fetch 或 axios 请求

自定义 ESLint 插件:禁止直接发起 fetchaxios 请求

这个自定义 ESLint 插件的目的是强制前端项目中的所有数据请求都必须通过一个统一的 apiService 模块进行,而不能在组件或其他非 apiService 文件中直接调用 fetchaxios

首先创建一个名为 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 模块本身可以自由地使用 fetchaxios

    • AST 遍历return { CallExpression(node) { ... } } 定义了一个"访问器"。当 ESLint 遍历 AST 并在当前节点找到一个 CallExpression (函数调用) 时,它会执行 CallExpression 方法中的逻辑。

    • 识别 fetchaxios 调用

      • fetch:检查 node.callee 是否是一个名为 fetchIdentifier
      • axios 方法:检查 node.callee 是否是一个 MemberExpression (例如 axios.get),然后进一步检查其 object 是否是 axiosproperty 是否是 getpost 等允许的 HTTP 方法。
    • 报告错误 :如果检测到不允许的 API 调用,context.report() 方法会被调用,向 ESLint 报告一个错误,并附带相应的节点和错误消息。

3. 将插件引入你的业务项目

假设你的业务项目目录为 my-frontend-app/

方法一:作为本地包通过 file: 协议引用 (推荐)

  1. 在插件目录 (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 // 标记为私有,防止意外发布
    }
  2. 在你的业务项目根目录的 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-pluginmy-frontend-app 内部,则路径为 file:./path/to/my-eslint-plugin

  3. 安装依赖 :在你的业务项目根目录执行 npm installyarn 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.getfetch 的错误,并提示你使用指定的 API 服务模块。

相关推荐
姑苏洛言2 小时前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
烛阴2 小时前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
Alice_hhu2 小时前
ResizeObserver 解决 echarts渲染不出来,内容宽度为 0的问题
前端·javascript·echarts
逃逸线LOF3 小时前
CSS之动画(奔跑的熊、两面反转盒子、3D导航栏、旋转木马)
前端·css
萌萌哒草头将军4 小时前
⚡️Vitest 3.2 发布,测试更高效;🚀Nuxt v4 测试版本发布,焕然一新;🚗Vite7 beta 版发布了
前端
技术小丁4 小时前
使用 HTML + JavaScript 在高德地图上实现物流轨迹跟踪系统
前端·javascript·html
小小小小宇5 小时前
React 并发渲染笔记
前端
stark张宇5 小时前
Web - 面向对象
前端·javascript
yanyu-yaya5 小时前
mac电脑安装 nvm 报错如何解决
java·前端·macos
sunbyte5 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Form Wave(表单label波动效果)
前端·javascript·css·vue.js·tailwindcss