1. 从一次真实的 ESLint 报错说起:它到底在检查什么
如果你刚接手一个前端项目,运行npm run lint后满屏红字,或者编辑器里每个文件都画着波浪线,那大概率是 ESLint 在“说话”。ESLint 是一个可插拔的 JavaScript/TypeScript 代码检查工具,它把代码解析成抽象语法树,再按你配置的规则逐条比对,发现不符合约定的写法就报出来。它不负责格式化(那是 Prettier 的活),而是负责“你这段逻辑写得对不对、有没有潜在 bug、风格是否统一”。
它适合谁?适合所有在团队里写 JS/TS 的人,尤其是多人协作、代码风格容易跑偏的项目。我见过一个项目,三个人三种分号风格,合并代码时 diff 里一半是分号增删,后来加上 ESLint 才把噪音压下去。
ESLint 的核心工作流分三步:解析(parserOptions 决定用哪个语法版本、是不是 ESM)、继承(extends 拉取社区或官方预设规则集)、覆盖(rules 里逐条改级别)。理解这三层,报错就不再是天书。
真实场景里最常见的入口是npx eslint .或npm run lint。第一次跑,你可能会看到类似这样的输出:
/Users/me/project/src/index.js 3:1 error 'var' is not allowed no-var 7:5 warning Expected '===' and instead saw '==' eqeqeq 12:3 error 'add' is defined but never used no-unused-vars ✖ 3 problems (2 errors, 1 warning)每一行格式是:文件路径、行:列、级别(error/warning)、描述、规则名。规则名是关键,你可以拿它去查文档,也可以直接在配置里改它的级别。error 会让命令以非零码退出,CI 里就会失败;warning 只提示不阻断。很多人第一次配 ESLint 时把所有规则都设成 error,结果本地开发寸步难行,这是典型的“用力过猛”。
这一节先建立认知:ESLint 不是玄学,它就是把你的代码和一份规则清单做比对。下一节我们进入落地环节,从零把配置跑起来。
2. 从零初始化 ESLint 配置:.eslintrc.js 与 npm 脚本怎么落地
在项目里落地 ESLint,第一步是安装依赖。以目前仍大量使用的 ESLint 7 生态为例(很多老项目锁在这个版本),命令是:
npm i -D eslint@7 eslint-webpack-plugin@3如果你不用 webpack,只想要命令行检查,那eslint@7单独装就够。装完后初始化配置,推荐在项目根目录手写.eslintrc.js,而不是用eslint --init交互式生成,因为手写你能完全掌控每一行。
一个可直接复制、覆盖大多数中小型项目的配置如下:
// .eslintrc.js module.exports = { // 继承官方推荐规则,先有一份基线 extends: ["eslint:recommended"], // 指定运行环境,决定哪些全局变量可用 env: { node: true, // 启用 Node 全局变量,如 process、__dirname browser: true, // 启用浏览器全局变量,如 window、document es2021: true, // 启用 ES2021 全局变量 }, // 解析选项:告诉 ESLint 用什么语法解析代码 parserOptions: { ecmaVersion: 2021, // 支持到 ES2021 语法 sourceType: "module", // 使用 ESM,允许 import/export ecmaFeatures: { jsx: true, // React 项目必须开,否则 JSX 报解析错误 }, }, // 具体规则,覆盖 extends 里的默认值 rules: { "no-var": 2, // 禁止 var,必须用 let/const eqeqeq: ["warn", "smart"], // 强制 ===,smart 模式放过 null 比较 "no-unused-vars": ["warn", { args: "none" }], // 未使用变量警告,忽略未用参数 semi: ["error", "always"], // 必须写分号 "array-callback-return": "warn", // 数组回调必须有 return "default-case": [ "warn", { commentPattern: "^no default$" }, // 允许用注释跳过 default ], }, };这里有几个容易踩的点。env和parserOptions是两回事:env管全局变量,parserOptions管语法。你写了import但没设sourceType: "module",就会报Parsing error: 'import' and 'export' may appear only with 'sourceType: module'。React 项目忘了jsx: true,第一个 JSX 标签就解析失败。
接着在package.json里加脚本,让检查可复用:
{ "scripts": { "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix" } }--ext指定要检查的扩展名,不加的话默认只查.js。--fix能自动修掉一部分规则(比如分号、引号),但像no-unused-vars这种需要你手动删代码的,它修不了。
如果你用 webpack5,还要把 ESLint 接进构建流程,让编译时就暴露问题:
// webpack.config.js const ESLintPlugin = require("eslint-webpack-plugin"); module.exports = { // ...其他配置 plugins: [ new ESLintPlugin({ extensions: ["js", "jsx"], // 检查的文件类型 fix: false, // 构建时不自动修,避免意外改动 failOnError: true, // 有 error 就让构建失败 }), ], };最后别忘了.eslintignore,把不需要检查的目录排除,否则dist、node_modules里的压缩代码会拖慢速度还刷屏:
dist node_modules coverage *.min.js到这里,配置层就齐了。下一节我们真正跑一次请求,看报错怎么变成修复。
3. 可复制配置片段与编辑器、CI 的集成细节
配置写好了,但 ESLint 的价值一半在命令行,一半在编辑器实时反馈。如果你只在提交前跑npm run lint,那体验是滞后的——写完一百行才发现三十个警告,改起来很痛苦。所以要把 ESLint 接进 VS Code。
VS Code 需要安装 ESLint 扩展。注意版本匹配:如果你项目用的是 ESLint 7,扩展建议用 2.2.2 左右的版本,太新的扩展可能默认走 ESLint 8+ 的扁平配置(eslint.config.js),和老项目的.eslintrc.js不兼容,会出现“扩展装了但没反应”的情况。装好后在.vscode/settings.json里加:
{ "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ], "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }editor.codeActionsOnSave让保存时自动修可修复的问题,这是提升幸福感的关键。但注意,它只修--fix能处理的规则,逻辑类问题还是得你自己看。
CI 集成是另一道防线。在 GitHub Actions 里加一个 job:
name: lint on: [push, pull_request] jobs: eslint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - run: npm ci - run: npm run lint这样任何 PR 只要 lint 不过就红叉,代码风格问题在合并前就被拦住。我建议 CI 里把 warning 也当失败处理,加--max-warnings=0:
{ "scripts": { "lint:ci": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0" } }本地开发允许 warning 存在,CI 严格零容忍,这个组合比较务实。
如果你在项目里用 Cline MCP 或类似 AI 编码助手,想让它在生成代码时也遵守同一套规则,需要把三件套配全:Base URL、Key、Model ID。以接入一个兼容 OpenAI 协议的服务为例,配置文件里要写清楚:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的密钥", "modelId": "claude-3-5-sonnet" }Base URL 指向 API 入口,Key 用于鉴权,Model ID 决定用哪个模型。三者缺一,请求就会失败。配好后,AI 生成的代码在保存时同样会被 ESLint 检查,形成闭环。
4. 验证一次完整请求:从报错到修复的全过程
光看配置不够,我们走一遍真实流程。假设src/utils.js里有这样一段代码:
// src/utils.js var count = 0; function add(a, b) { if (a == b) { return a + b; } return a - b; } const unused = 42;运行npm run lint,输出:
/Users/me/project/src/utils.js 2:1 error Unexpected var, use let or const instead no-var 5:7 warning Expected '===' and instead saw '==' eqeqeq 11:7 error 'unused' is assigned a value but never used no-unused-vars ✖ 3 problems (2 errors, 1 warning)逐条解读。no-var是 error,因为var有变量提升和函数作用域问题,现代代码统一用let/const。eqeqeq是 warning,==会做隐式类型转换,1 == "1"为 true,容易埋 bug。no-unused-vars是 error,声明了unused却没用,属于死代码。
修复动作:把var改成let,==改成===,删掉unused。改完再跑:
$ npm run lint > eslint . --ext .js,.jsx,.ts,.tsx # 无输出,退出码 0无输出就是通过。你可以用echo $?确认退出码是 0。如果还有 warning,命令仍会输出但退出码为 0;加了--max-warnings=0后,warning 也会让退出码变 1。
再验证一次自动修复。故意把分号去掉:
const name = "eslint"跑npm run lint:fix,ESLint 会自动补上分号,文件变成const name = "eslint";。但如果你把no-unused-vars的变量留着,--fix不会删它,因为删代码有风险,工具不替你做决定。
这一步的意义在于:你亲眼看到报错、理解规则、手动或自动修复、再验证通过。这个循环跑通一次,后面遇到任何规则报错,你都知道该去哪查、怎么改。
5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth
实际使用中,报错不全是代码风格问题,还有一类是环境或接入配置导致的。下面几个是我遇到频率最高的。
401 Unauthorized。通常出现在你调用某个 API 服务时,Key 无效或没带上。检查三处:Key 是否复制完整(有没有多余空格)、请求头里是否带了Authorization: Bearer sk-xxx、Key 是否过期。如果是 AI 编码助手报 401,去控制台重新生成一个 Key,替换配置文件里的值。
local proxy failed。这个报错一般和本地网络配置有关。先确认你的请求地址写对了,Base URL 不要有多余路径。然后检查是否有本地服务占用了端口,或者环境变量里残留了旧的代理设置。把HTTP_PROXY、HTTPS_PROXY这类环境变量清掉再试。如果用的是公司网络,确认目标地址在允许列表内。
reading 'choices'。这是解析响应时的典型错误,意思是代码期望响应里有choices字段,但实际拿到的结构不对。常见原因:请求根本没成功(返回的是错误对象),或者你用的模型接口和客户端期望的协议不一致。排查方法:把原始响应打印出来看。在 Node 里可以这样:
const res = await fetch(url, options); const text = await res.text(); console.log("raw response:", text);看到原始内容,就知道是鉴权失败、模型名写错,还是返回格式不匹配。
OAuth 相关报错。如果你接入的服务走 OAuth 流程,报错通常是 token 过期或回调地址不匹配。检查redirect_uri是否和注册时填的一致,token 是否需要刷新。有些客户端会自动刷新,有些需要你手动处理 401 后重新走授权。
排查这类问题的通用思路:先看原始响应,再对照配置三件套(Base URL、Key、Model ID),最后确认网络可达。不要一上来就改代码,八成是配置问题。
6. 把 ESLint 变成可维护的流程:从规则到习惯
配置跑通只是开始,真正难的是让它长期可维护。我见过太多项目,ESLint 配了一次就没人管,规则越加越多,最后npm run lint要跑两分钟,大家干脆绕过。
几个实用建议。第一,规则分级。把“会导致 bug”的设成 error,比如no-unused-vars、no-undef;把“风格偏好”的设成 warn,比如semi、quotes。这样 CI 严格,本地宽松,不至于寸步难行。
第二,定期清理规则。每季度看一次rules里有没有已经过时或和 Prettier 冲突的项。ESLint 和 Prettier 的职责要分清:ESLint 管逻辑和潜在问题,Prettier 管格式。如果两个都管格式,就会互相打架。可以用eslint-config-prettier关掉所有和格式相关的 ESLint 规则。
第三,把 lint 前置到提交钩子。用 husky + lint-staged,只检查本次改动的文件:
{ "lint-staged": { "*.{js,jsx,ts,tsx}": ["eslint --fix", "git add"] } }这样提交前自动修一遍,CI 里再全量检查兜底。
第四,文档化。在项目 README 里写清楚:怎么跑 lint、规则为什么这么定、遇到报错去哪查。新人接手时不用猜。
最后,如果你在团队里推动这件事,别一次性加几百条规则。先从eslint:recommended开始,跑一周,看哪些报错最多,再针对性加规则。渐进式落地比一刀切更容易被接受。
ESLint 本身不复杂,复杂的是让它融入团队习惯。配置是死的,流程是活的。把上面这些跑一遍,你就有了一套能长期用的代码检查流程。