1. 别再手动改代码格式了!Vue3 项目工程化规范搭建实战

前言

最近在从零搭建一个 Vue3 项目,模仿 Art Design Pro 的架构思路自己写。本来想直接开干页面,但想了想,如果一开始不把工程化规范搭建好,后面代码越多越难管。到时候缩进不统一、引号单双混用、提交信息乱七八糟,该起更痛苦。

所以这篇文章记录一下我的工程化搭建过程。目标很简单:让代码规范不靠人自觉,而是靠工具自动执行。

整体思路

工程化规范不是装一个 Prettier 就完事了,它是一条链:

text 复制代码
编辑器层 → .editorconfig + .vscode/settings.json
格式化层 → Prettier
质量层 → ESLint
提交层 → Husky + lint-staged + commitlint
环境层 → .env 多环境

核心原则:能自动做的,绝不靠人手动。

第一步:.editorconfig ------ 统一编辑器基础行为

.editorconfig 是所有工程化的地基。它不依赖任何构建工具,装个编辑器插件就能生效,管的是最基础的格式:缩进、换行、字符集。

  • 在 VSCode 中搜索插件 EditorConfig 插件进行安装

  • 在项目根目录创建 .editorconfig :

ini 复制代码
# EditorConfig is awesome:https://EditorConfig.org

# 根配置文件,阻止向上查找
root = true

# 所有文件适用的通用规则
[*]
# 文件字符集
charset = utf-8
# 换行符:统一使用 LF,解决 windows(CRLF)与 Unix(LF)的冲突
end_of_line = lf
# 缩进风格:空格
indent_style = space
# 缩进大小:2 个空格
indent_size = 2
# 去除行尾空白字符
trim_trailing_whitespace = true
# 文件末尾始终插入一个空行
insert_final_newline = true

# Matches 文件保留行尾空白(Markdown 中行尾双空格表示换行)
[*.md]
trim_trailing_whitespace = false

# Makefile 必须使用 Tab 缩进
[Makefile]
indent_style = tab
insert_final_newline = false

# Windows 批处理脚本使用 CRLF
[*.{bat,cmd}]
end_of_line = crlf

[*.{json,yml,yaml}]
indent_size = 2

第二步:Prettier ------ 统一代码格式

EditorConfig 解决了缩进、换行符、字符集等基础格式,但管不了代码层面的风格:引号用单还是双、要不要分号、多少字符换行。这些由 Prettier 负责。

1. 安装 prettier

bash 复制代码
pnpm add -D prettier

2. 创建配置文件

在项目根目录创建 .prettierrc.mjs

json 复制代码
/**
 * Prettier 配置文件 (.prettierrc.mjs)
 *
 * 核心原则:
 * 1. 此文件集中配置 Prettier 的格式化规则。
 * 2. 未在此处显式配置的缩进、换行符等选项,由 Prettier 从 .editorconfig 读取。
 * 3. 使用 .mjs 格式是为了支持注释,方便维护。
 */

/** @type {import("prettier").Config} */
export default {
  printWidth: 120, // 每行尽量控制在 120 列以内;这是换行参考值,不是硬性上限。
  singleQuote: true, // JavaScript、TypeScript 等适用语法优先使用单引号;JSON 仍按语法要求使用双引号。
  semi: true, // 在语句末尾添加分号。
  trailingComma: 'all', // 多行对象、数组、参数等语法结构末尾尽可能添加逗号。
  arrowParens: 'always', // 箭头函数参数始终保留括号,例如 `(value) => value`。
  proseWrap: 'preserve', // 保留 Markdown 等文本段落现有的换行方式。
  vueIndentScriptAndStyle: false, // Vue 单文件组件中的 script/style 内容不相对标签额外缩进。
};

3. 创建忽略文件

Prettier 默认会格式化项目里所有文件,但有些文件不该被它碰,比如构建产物、依赖目录、锁文件。根目录中创建 .prettierignore:

json 复制代码
# 依赖与构建产物
node_modules
dist
dist-ssr
build
coverage

# 缓存
.cache
.vite

# 锁文件
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lockb

# 本地环境文件
.env
.env.local
.env.*.local

# 系统与编辑器
.DS_Store
.idea

# 文档
*.md

为什么忽略 Markdown?

因为 proseWrap: 'preserve' 已经保留了 Markdown 的换行,但 Prettier 仍可能调整表格对齐、列表缩进等。如果你写的是掘金这类需要精确控制排版的内容,建议直接忽略,避免 Prettier 好心办坏事。

4. 添加脚本

在 package.json 中添加:

json 复制代码
{
    "scripts": {
        "format": "prettier --write .",
        "format:check": "prettier --check ."
    }
}
  • format:格式化所有文件,直接改磁盘内容
  • format:check:只检查不修改,有问题返回非零退出码,用于 CI

5. 与 EditorConfig 的关系

两者有重叠的部分(如缩进、换行符),但职责不同:

  • EditorConfig:管编辑器层面的基础格式,跨编辑器通用,不依赖任何构建工具
  • Prettier:管代码格式化规则,需要手动或通过插件触发

实际使用中,Prettier 会读取 .editorconfig 里它支持的选项(如 end_of_line、indent_size),所以两边保持一致即可。Prettier 不支持的选项(如 charset),由 EditorConfig 单独生效。

6. 验证

执行:

bash 复制代码
pnpm format

然后随便找个文件,故意把缩进改乱、把单引号改成双引号,保存后再跑一次 pnpm format,看是否被自动修正。如果 VS Code 里保存没反应,检查 .vscode/settings.json 中的 editor.defaultFormatter 是否配了 esbenp.prettier-vscode。

第三步:VS Code 配置 ------ 让规范自动执行

前面配置好了 .editorconfig 和 .prettierrc,但如果每次都要手动跑 pnpm format,那和没配一样。.vscode 里的配置,就是让编辑器在保存时自动执行这些规则。

需要提交哪些文件

text 复制代码
.vscode/
├── settings.json      # 工作区设置(必须提交)
└── extensions.json    # 推荐插件(必须提交)

launch.json 和 tasks.json 属于可选,不是每个项目都需要。核心是前两个。

settings.json

创建 .vscode/settings.json:

json 复制代码
{
  // === 编辑器基础 ===
  "editor.formatOnSave": true, // 保存时自动格式化
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit" // 显式保存时应用 Stylelint 自动修复
  },
  "files.eol": "\n", // 与 .editorconfig 保持一致

  // === 默认格式化器 ===
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  // === 按语言指定格式化器(覆盖默认) ===
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

  // === Stylelint ===
  "stylelint.validate": ["css", "scss", "vue", "html"],

  // === 文件排除(让侧边栏更干净) ===
  "files.exclude": {
    "**/.git": true,
    "**/node_modules": true,
    "**/dist": true,
    "**/.turbo": true
  },

  "search.exclude": {
    "**/node_modules": true,
    "**/bower_components": true,
    "**/*.code-search": true
  },

  // === Volar ===
  "vue.server.hybridMode": true, // Volar 2.x 的推荐设置

  // === 关闭 VS Code 内置的样式校验,交给 Stylelint ===
  "css.validate": false,
  "scss.validate": false,
  "less.validate": false
}

注意: settings.json 里别放个人偏好(字体、主题色、字号),那些属于用户级 settings,提交了会影响别人。工作区 settings 只放和项目规范相关的。

extensions.json

创建 .vscode/extensions.json

json 复制代码
{
  "recommendations": [
    // === Vue3 核心 ===
    "Vue.volar", // Vue - Official:Vue3 官方语言支持

    // === 代码质量与格式化 ===
    "dbaeumer.vscode-eslint", // ESLint:代码质量检查
    "esbenp.prettier-vscode", // Prettier:代码格式化
    "stylelint.vscode-stylelint", // Stylelint:CSS/SCSS/Vue 样式检查
    "EditorConfig.EditorConfig", // 读取 .editorconfig,统一缩进与换行符

    // === 开发体验(可选) ===
    "usernamehw.errorlens", // 行内显示错误与警告
    "streetsidesoftware.code-spell-checker", // 拼写检查
    "christian-kohler.path-intellisense", // 路径智能补全
    "mikestead.dotenv", // .env 文件语法高亮
    "gruntfuggly.todo-tree" // 以树状视图汇总 TODO
  ],

  "unwantedRecommendations": [
    // 提示不要使用与 Volar 冲突的扩展;此设置不会自动禁用扩展
    "octref.vetur" // Vetur:Vue2 项目使用的 Vue 语言扩展
  ]
}

别人克隆项目后,VS Code 会提示"是否安装推荐插件",点一下就能装齐,不用手动找。

验证

随便找个文件,把缩进改乱、引号改成双引号,按 Ctrl/Cmd + S 保存,看是否自动格式化。

如果没生效:

  1. 确认已安装 esbenp.prettier-vscode 插件
  2. 确认 .vscode/settings.json 里 editor.defaultFormatter 配的是 esbenp.prettier-vscode
  3. 检查 VS Code 右下角是否提示"格式化程序未安装"或"有多个格式化程序"

第四步:ESLint 10 + TypeScript 类型感知实战 ------ 开启代码质量检查

引言

ESLint10 最大的变化是全面转向 Flat Config,废弃了沿用多年的 .eslintrc 体系。这不只是配置文件格式的调整,而是整个配置模型的重新设计:从"层层继承、隐式合并"变成"显式数组、按序生效"。对于还在用旧版配置的 Vue3 + TypeScript 项目来说,升级到 ESLint10 意味着要重新理解一套规则组织方式。

但 Flat Config 只是形式上的变化,真正决定 ESLint 能不能发现深层问题的,是有没有开启类型感知。

普通 ESLint 只看语法。它把代码解析成 AST,检查变量有没有声明、有没有用 var、有没有多余的转义字符。这些规则不需要知道任何类型信息,速度快,但能发现的问题也停留在表面。

类型感知(Type-Aware Linting)让 ESLint 拿到 TypeScript 的完整类型信息,从而发现那些语法上完全合法、只有类型系统才能暴露的问题。 最典型的是 no-floating-promises:

typescript 复制代码
async function saveEmployee() {
    return api.update(data)
}

function handleSubmit() {
    saveEmployee()  // ⚠️ 没有 await,Promise 悬空
}

saveEmployee() 返回的是 Promise<void>,调用时没有 await 也没有 .catch(),就是一个悬浮 Promise。用户以为保存成功了,实际上可能失败了。普通 ESLint 看不出任何问题,但类型感知会直接报错。

这就是为什么我们在项目里没有停在"配好 ESLint 能用"这一步,而是继续启用了 recommendedTypeChecked 和 projectService。前者让 ESLint 加载需要类型信息的规则集,后者让它复用 TypeScript 的 Project Service,自动为每个文件找到对应的 tsconfig ------ 不再手动维护一份 tsconfig.eslint.json。

接下来,我会从最小可运行配置开始,一步步加上全局变量、TS 支持、Vue 支持、导入检查和 Prettier,最后把类型感知作为压轴一步加进去。每一步都说明为什么加、加了之后解决什么问题。


第 1 步:安装 ESLint 并创建最小可运行配置

1.1 安装依赖

在项目根目录执行:

bash 复制代码
pnpm add -D eslint @eslint/js jiti

这两个包分别是什么:

  • eslint:ESLint 本体,提供 CLI 和核心规则引擎。
  • @eslint/js:ESLint 官方的 Javascript 规则集,导出 configs.recommended 和 configs.all。拆成独立包是为了让核心 eslint 保持轻量,规则集可以单独更新。

为什么不用 npx eslint --init:

--init 会生成一套它认为合适的配置,但你不知道每一行是干什么的。手动从空文件开始,每一步都能讲清为什么。

1.2 创建配置文件

在项目根目录新建 eslint.config.mjs,写入:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js'
import { defineConfig } from 'eslint/config'

export default defineConfig([
    eslint.configs.recommended,
])

逐行解释:

import eslint from '@eslint/js' 引入官方规则集包。它默认导出的是一个对象,里面有 configs 属性。

import { defineConfig } from 'eslint/config' 这是 ESLint 10 提供的类型辅助函数。它不影响运行时行为, 只是让你在编辑器里获得类型提示和自动补全。你可以不用它直接导出数组,但用了之后写配置时会有智能提示。

export default defineConfig([...]) ESLint 10 的 flat config 要求默认导出一个数组 。数组里每一项配置对象,ESLint 会按顺序合并它们。后面的配置可以覆盖前面的。

eslint.configs.recommended这是官方推荐规则集,包含了几十条基础规则,比如no-unused-vars、no-undef、no-debugger。它是纯语法层面的检查,不需要任何额外信息。

为什么文件后缀是 .mjs:

ESLint 10 支持 eslint.config.js、eslint.config.mjs、eslint.config.cjs。用 .mjs 是明确告诉 Node 这是一个 ES Module,可以用 import 语法。如果你的 package.json 里有 "type":"module",用 .js 也行,但 .mjs 更明确、不会出错。

1.3 验证

在 src 下随便写一个文件:

javascript 复制代码
// src/test.js
const a = 1

运行:

bash 复制代码
npx eslint src/test.js

预期结果: 报错 'a' is assigned a value but never used,来自 no-unused-vars 规则。说明 ESLint 跑起来了,推荐规则生效了。

如果没有任何输出,说明没检查到文件,检查路径对不对,或者文件是不被默认忽略了。

第 2 步:配置忽略目录

2.1 为什么需要忽略目录

ESLint 默认会扫描项目里所有 .js、.ts、.vue 等文件。但有些目录不应该被检查:

  • dist:构建产物,是编译后的代码,检查它没有意义,而且会拖慢速度。
  • public:静态资源,通常不包含需要检查的源码。
  • assets:图片、字体等资源目录,ESLint 不认识这些文件,扫描只会浪费时间。

如果不配置忽略,ESLint 会报一堆你根本不想管的错误,而且每次运行都要遍历这些目录。

2.2 修改配置文件

在 eslint.config.mjs 里,eslint.configs.recommended 后面加一项:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js'
import { defineConfig } from 'eslint/config'

export default defineConfig([
    eslint.configs.recommended,
    { ignores: ['**/dist/**','public/**','**/assets/**'] }
])

2.3 逐行解释

{ ignores: [...] }

这就是一个普通的配置对象,但只包含ignores一个字段。

为什么必须单独放一个对象:

这是 flat config 的一个重要设计。ignores 有两种行为:

  • 如果它出现在只包含 ignores 的对象 里,它是全局忽略 ------ 所有配置都不作用于这些文件。
  • 如果它和其他字段(比如 files、rules)出现在同一个对象里,它只对那个配置生效,不是全局忽略。

所以这里必须单独写一个对象,否则 dist 里的文件还是会被其他配置检查。

三个路径模式分别是什么:

  • '**/dist/**':匹配任意层级下的 dist 目录及其所有内容。** 表示任意层级,/** 表示目录下所有文件。
  • 'public/**':匹配根目录下 public 目录的所有内容。这里没用 **/ 前缀,是因为 public 固定在根目录。
  • **/assets/**:匹配任意层级下的 assets 目录。

为什么用 **/ 而不是直接写目录名:

因为 dist 可能出现在子包里(比如 monorepo 里每个包都有自己的 dist)。**/dits/** 能覆盖所有情况,不用一个一个列。

2.4 验证

在 dist 目录下随便建一个文件:

javascript 复制代码
// dist/test.js
const unused = 123

运行:

javascript 复制代码
npx eslint .

预期结果 :dist/test.js 里的 unused 不会 被报 no-unused-vars。如果报了,说明忽略配置没生效,检查 ignores 是不是单独放在一个对象里。

同时确认 src 下的文件仍然 被检查 ------ 比如 src/test.js 里的 const a = 1 还是应该报错。这样才能证明忽略只影响了目标目录,没有把整个项目都跳过。

第 3 步:自定义规则

3.1 为什么需要这一步

eslint.configs.recommended 是官方推荐规则集,但它只覆盖了最基础的几十条规则,而且所有规则一视同仁 ------ 开发和生产一样严格,浏览器和 Node 一样严格。

实际项目里你需要更细的控制:

  • 开发时警告,生产时报错 :console.log 在开发时很常用,但生产环境不该留。
  • 统一代码风格 :强制用 === 而不是 ==,强制 const 而不是 let。
  • 禁用危险写法 :禁止在 return 里写赋值语句,禁止用 new String() 这种包装对象。
  • 允许约定的例外 :_ 开头的参数不检查未使用,== null 允许通过。

这些都需要你自己写规则。

3.2 修改配置文件

在 eslint.config.mjs 里,忽略目录后面加一个配置对象:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js'
import { defineConfig } from 'eslint/config'

export default defineConfig([
    eslint.configs.recommended,
    { ignores: ['**/dist/**','public/**','**/assets/**'] },
    {
        rules:{
            'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
            'no-unused-vars': [
             process.env.NODE_ENV === 'production' ? 'error' : 'warn',{ vars: 'all', args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
             ],
             'no-undef': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
             'no-console': process.env.NODE_ENV === 'production' ? 'error' :'off',
             'accessor-pairs': 'error',
             eqeqeq: ['error', 'always', { null: 'ignore' }],
             'no-label-var': 'error',
             'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
             'no-lone-blocks': 'error',
             'no-multi-str': 'error',
             'no-new-wrappers': 'error',
             'no-return-assign': ['error', 'except-parens'],
             'no-self-compare': 'error',
             'no-sequences': 'error',
             'no-undef-init': 'error',
             'no-unmodified-loop-condition': 'error',
             'no-unneeded-ternary': ['error', { defaultAssignment: false }],
             'no-unreachable-loop': 'error',
             'prefer-const': 'error',
             'no-useless-escape': 'off',
        }
    }
])

3.3 逐段解释

环境变量控制严格度:

javascript 复制代码
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-unused-vars': [
  process.env.NODE_ENV === 'production' ? 'error' : 'warn',
  { vars: 'all', args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'no-undef': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',

这四条规则都有一个共同逻辑:开发时宽松,生产时严格。

  • no-debugger:开发时忘了删 debugger 只是警告,生产环境直接报错,构建就失败。

  • no-unused-vars:开发时变量没用只是警告,生产环境报错。第二个参数时配置对象:

    • vars: 'all':检查所有变量
    • args: 'after-used':只检查最后一个使用参数之后的参数。比如 function(a,b,c) 里如果 b 没用但 c 用了,b 不报错;如果 c 没用,c 报错。1
    • argsIgnorePattern:'^_',varsIgnorePattern: '^_':_ 开头的变量跳过检查。
  • no-undef:开发时警告,生产时报错。未声明的变量在生产环境是严重问题。

  • no-console:开发时完全关闭,允许随便用;生产环境报错,禁止留 console.log。

代码风格规则:

javascript 复制代码
'accessor-pairs': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'no-label-var': 'error',
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'error',
'no-multi-str': 'error',
'no-new-wrappers': 'error',
'no-return-assign': ['error', 'except-parens'],
'no-self-compare': 'error',
'no-sequences': 'error',
'no-undef-init': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
'no-unreachable-loop': 'error',
'prefer-const': 'error',
'no-useless-escape': 'off',

挑几个重点:

  • eqeqeq: ['error','always',{ null: 'ignore' }]:强制用 ===,但 == null 例外。因为 == null 同时匹配 null 和 undefined,是有意为之的简写。
  • no-return-assign:['error','except-parens']:禁止在 return 里赋值,但括号包来的例外。比如 return (a = b) 允许,return a = b 报错。
  • no-unneeded-ternary: ['error',{ defaultAssignment: false }]:禁止 a ? a : b 这种多余的三元,但 a ? a : b 里如果 a 是布尔值,a ? true : false 这种也报错。
  • prefer-const: 'error':声明后不再修改的变量必须用 const。
  • no-useless-escape: 'off':关掉不必要的转义字符检查。因为正则里有时需要写 \/ 这种转义,虽然语法上不必要,但可读性更好。

为什么这些规则不区分环境:

它们都是代码质量问题,不是调试工具。开发和生产都应该遵守,所以统一 'error'。

3.4 验证

验证 no-console 的开发/生产差异:

在 src/test.js 里写:

javascript 复制代码
// src/test.js
console.log('hello')

运行:

bash 复制代码
npx eslint src/test.js

预期结果 :不报错,因为开发环境 no-console 是 off。

然后模拟生产环境:

bash 复制代码
NODE_ENV=production npx eslint src/test.js

预期结果 :报 Unexpected console statement。

验证 prefer-const:

javascript 复制代码
// src/test.js
let a = 1
console.log(a)

运行:

bash 复制代码
npx eslint src/test.js

预期结果 :报 'a' is never reassigned. Use 'const' instead。

验证 eqeqeq 的例外:

javascript 复制代码
// src/test.js
const a = null
if (a == null) console.log('ok') // 不报错
if (a == 1) console.log('bad') // 报错

预期结果 :第一行不报,第二行报 Expected '===' and instead saw '=='

验证 argsIgnorePattern:

javascript 复制代码
function test(_unused,used) {
    console.log(used)
}
test(1,2)

预期结果 :不报 _unused 未使用。

第 4 步:按文件类型区分浏览器和 Node 环境

4.1 这一步要解决什么问题

第 3 步你配了自定义规则,但有个问题没处理:ESLint 不知道你的代码跑在什么环境里。

看两个场景:

javascript 复制代码
// src/utils/request.js
window.location.href = '/login' // 浏览器环境,window 合法

// vite.config.ts
process.env.NODE_ENV // Node 环境,process 合法

如果你不告诉 ESLint 哪个文件跑在哪个环境:

  • 在 src 下写 window,ESLint 报 'window' is not defined
  • 在 vite.config.ts 里写 process,ESLint 也报 'process' is not defined

但这两个写法本身都是对的,只是跑在不同环境里。所以需要按文件路径区分环境。

4.2 安装依赖

bash 复制代码
pnpm add -D globals

globals 包是什么:

纯数据包,导出各种运行环境的全局变量列表:

  • globals.browser:window、document、localStorage、fetch等
  • globals.node:process、__dirname、Buffer、require等
  • globals.es2024:Promise、Symbol、Reflect等

你之前发现 Promise 不报错,是因为 ecmaVersion: "latest" 已经覆盖了 ES 标准全局变量。但 window 和 process 不是 ECMAScript 标准的一部分 ,ecmaVersion 不会自动加。这就是 globals 包真正要解决的问题。

4.3 修改配置文件

在 eslint.config.mjs 里,第 3 步的自定义规则后面,加两个配置对象:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js'
import globals from 'globals'
import { defineConfig } from 'eslint/config'

export default defineConfig([
    eslint.configs.recommended,
    { ignores: ['public/**','**/dist/**','**/assets/**'] },
    
    // 第 3 步:自定规则
    {
        rules: {
            // ... 第 3 步那堆规则
        }
    },
    
    // 第 4 步:浏览器环境
    {
        files: ['src/**/*.{js,ts,tsx,vue}'],
        languageOptions: {
            globals:globals.browser,
        }
    },
    
    // 第 4 步:Node环境
    {
        files:[
            '**/*.config.{js,ts,mjs,cjs,mts,cts}',
            'build/**/*.{js,ts,mjs,cjs,mts,cts}',
            'eslint.config.mjs',
        ],
        languageOptions: {
            globals:globals.node,
        },
        rules: {
            'no-console': 'off',
        }
    }
])

4.4 逐段解释

浏览器环境配置:

javascript 复制代码
{
    files:['src/**/*.{js,ts,tsx,vue}'],
    languageOptions:{
        globals:globals.browser,
    }
}

files 限定这个配置只对 src 下的 .js、.ts、.tsx、.vue 文件生效。globals.browser 把 window、document、localStorage、fetch、setTimeout 等浏览器 API 加进合法全局变量列表。

为什么不用展开运算符:

第 3 步我教你写 ...globals.es2024,但这里写 globals.browser 没有展开。因为 flat config 的 globals 字段接受的是一个对象,globals.browser 本身就是对象,直接赋值即可。展开反而多余。

Node 环境配置:

javascript 复制代码
{
  files: [
    '**/*.config.{js,ts,mjs,cjs,mts,cts}',
    'build/**/*.{js,ts,mjs,cjs,mts,cts}',
    'eslint.config.mjs',
  ],
  languageOptions: {
    globals: globals.node,
  },
  rules: {
    'no-console': 'off',
  },
},

files 匹配三类文件:

  • **/*.config.{js,ts,mjs,cjs,mts,cts}:所有配置文件,比如 vite.config.ts、tailwind.config.js。
  • build/**/*.{js,ts,mjs,cjs,mts,cts}:build 目录下的构建脚本。
  • eslint.config.mjs:ESLint 配置文件自己。你之前报的 process is not defined 就是因为它没被匹配到,加上这一条就解决了。

这些文件跑在 Node 里,需要 process、__dirname、Buffer 等全局变量。

为什么单独关掉 no-console:

构建脚本和配置文件里经常需要 console.log 输出构建信息、警告、错误。第 3 步你在生产环境把 no-console 设成了 'error',如果不再这里关掉,跑构建时 ESLint 会报错。

4.5 验证

验证浏览器环境:

javascript 复制代码
// src/test.js
window.location.href = '/login'
document.title = 'test'

运行:

bash 复制代码
npx eslint src/test.js

预期结果:不报错。

验证 Node 环境:

javascript 复制代码
// vite.config.ts
console.log(process.env.NODE_ENV)

运行:

bash 复制代码
npx eslint vite.config.ts

预期结果 :不报 'process' is not defined,也不报 no-console。

验证环境隔离:

javascript 复制代码
// src/test.js
process.env.NODE_ENV

运行:

bash 复制代码
npx eslint src/test.js

预期结果 :报 'process' is not defined。因为 src 下只加了 globals.browser,没有加 globals.node。这说明环境是精确隔离的。

验证 eslint.config.mjs 自己不再报错:

直接运行:

bash 复制代码
npx eslint eslint.config.mjs

预期结果 :不报 'process' is not defined。因为你已经把 eslint.config.mjs 加进了 Node 环境的 files 里。

第 5 步:TypeScript 基础支持(先不开类型感知)

5.1 这一步要解决什么问题

前 4 步,ESLint 只检查了 JavaScript 语法。但你项目里写的是 .ts 和 .vue 文件,ESLint 默认不认识 TypeScript 的语法,比如:

typescript 复制代码
const name: string = 'test'
function greet<T>(name: T): T { return name }

不加 TypeScript 支持,ESLint 会在 : string 和 <T> 这些地方直接解析失败,报 Parsing error。

typescript-eslint 这个包解决两件事:

  1. 提供 parser:让 ESLint 能解析 TypeScript 语法。
  2. 提供规则集 :recommended、strict、recommendedTypeChecked 等,包含大量 TS 专用规则。

这一步先用 recommended,只做语法层面的 TS 检查。recommendedTypeChecked 需要类型信息,留到第 8 步再加。

5.2 安装依赖

bash 复制代码
pnpm add -D typescript-eslint

typescript-eslint 是什么:

它是 typescript-eslint 项目统一入口包,导出:

  • configs:规则集,比如 configs.recommended、config.strict、configs.recommendedTypeChecked
  • parser:TypeScript 解析器
  • plugin:规则插件

一个包解决所有问题,不需要单独装 @typescript-eslint/parser 和 @typescript-eslint/eslint-plugin。

5.3 修改配置文件

在 esllint.config.mjs 里,Node 环境配置后面加两块:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js';
import globals from 'globals';
import { configs as tseslintConfigs, parser as tseslintParser } from 'typescript-eslint';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  eslint.configs.recommended,
  { ignores: ['public/**', '**/dist/**', '**/assets/**'] },

  // 第 3 步:自定义规则
  {
    rules: {
      // ... 第 3 步那堆规则
    },
  },

  // 第 4 步:浏览器环境
  {
    files: ['src/**/*.{js,ts,tsx,vue}'],
    languageOptions: { globals: globals.browser },
  },

  // 第 4 步:Node 环境
  {
    files: [
      '**/*.config.{js,ts,mjs,cjs,mts,cts}',
      'build/**/*.{js,ts,mjs,cjs,mts,cts}',
      'eslint.config.mjs',
    ],
    languageOptions: { globals: globals.node },
    rules: { 'no-console': 'off' },
  },

  // ⭐ 第 5 步:注入 TS 推荐规则集
  ...tseslintConfigs.recommended.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
  })),

  // ⭐ 第 5 步:TS 文件的 parser 和规则
  {
    files: ['**/*.?([cm])ts', '**/*.?([cm])tsx'],
    languageOptions: {
      parser: tseslintParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        warnOnUnsupportedTypeScriptVersion: false,
      },
    },
    rules: {
      'no-undef': 'off',
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': [
        'error',
        { args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
      ],
      '@typescript-eslint/ban-ts-comment': 'off',
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/prefer-as-const': 'warn',
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-unused-expressions': 'off',
      '@typescript-eslint/no-unsafe-function-type': 'off',
      '@typescript-eslint/no-wrapper-object-types': 'off',
      '@typescript-eslint/no-namespace': 'off',
    },
  },
]);

5.4 逐段解释

引入 typescript-esllint:

javascript 复制代码
import { configs as tseslintConfigs,parser as tseslintParser } from 'typescript-eslint';
  • configs 重命名为 tseslintConfigs,避免和 eslint.configs 混淆。
  • parser 重命名为 tseslintParser,后面配置 parser 时要用。

注入 TS 规则集:

javascript 复制代码
...tseslintConfigs.recommended.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
})),

为什么要 .map():

tseslintConfigs.recommended 是一个数组 ,里面每个配置对象默认作用于所有文件。如果不加 files 限制,这些 TS 规则会作用到 .js 文件上,但 .js 文件没有 TS 的类型语法,规则会误报。

map() 遍历数组,给每个配置对象加上 files 字段,只对 TS 和 Vue 文件生效。

...config 保留原配置的所有字段,只覆盖 files。

为什么这里用 recommended 而不是 recommendedTypeChecked:

recommended 只做语法层面的 TS 检查,不需要 tsconfig,速度快。

recommendedTypeChecked 需要类型信息,是第 9 步才加的。现在先用 recommended,把基础跑通。

TS 文件的 parser 配置:

javascript 复制代码
{
  files: ['**/*.?([cm])ts', '**/*.?([cm])tsx'],
  languageOptions: {
    parser: tseslintParser,
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      warnOnUnsupportedTypeScriptVersion: false,
    },
  },
  ...
}

files 的正则 **/*.?([cm])ts 是匹配:

  • *.ts:普通 TS 文件
  • *.mts:ES Module 格式的 TS
  • *.cts:CommonJS 格式的 TS

?([cm]) 表示 c 或 m 可有可无。这是 typescript-eslint 官方推荐的写法。

parser: tseslintParser 让 ESLint 用 TypeScript 解析器解析这些文件。

parserOptions 里现在没有 projectService ------ 这是故意的,因为还没到类型感知那一步。 projectService 是第 9 步才加的。

为什么关掉 no-undef 和 no-unused-vars:

javascript 复制代码
'no-undef': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [...],
  • no-undef 在 TS 文件里必须关掉 。因为 TS 自己会检查未定义变量,而且 ESLint 的 no-undef 不认识 TS 的类型声明(type、interface),会误报。
  • no-unused-vars 换成 @typescript-eslint/no-unused-vars。TS 版本的规则能正确处理 TS 特有的语法,比如类型导入、泛型参数。

为什么放宽一堆 TS 规则:

javascript 复制代码
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/prefer-as-const': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
'@typescript-eslint/no-namespace': 'off',

这些是 recommended 默认启用的规则,但实际项目里往往需要放宽:

  • ban-ts-comment:禁止@ts-ignore,但实际开发中有时必须用。
  • no-explicit-any:禁止 any,但对接后端接口时经常需要临时用。
  • no-non-null-assertion:禁止!,但某些场景下比 ?. 更合适。
  • no-namespace:禁止 namespace,但某些库的类型声明需要。

这些取舍不是"偷懒",而是实践中的权衡。面试时可以主动讲:哪些规则该严、哪些该松、为什么。

5.5 验证

验证 TS 语法被正确解析:

typescript 复制代码
// src/test.ts
const name: string = 'test'

interface User {
    id: number
    name: string
}

const user: User = { id: 1, name }

console.log(user)

运行:

bash 复制代码
npx eslint src/test.ts

预期结果 :不报解析错误。如果报 Parsing error,说明 parser 没配对。

验证 @typescript-eslint/no-unused-vars 生效:

typescript 复制代码
// src/test.ts
const unused = 123

运行:

bash 复制代码
npx eslint src/test.ts

预期结果: 报 'unused' is assigned a value but never used,来源是 @typescript-eslint/no-unused-vars。

验证 _ 开头的参数不报错:

typescript

scss 复制代码
// src/test.ts
function test(_unused: number, used: number) {
  console.log(used)
}
test(1, 2)

预期结果: 不报 _unused 未使用。

验证 any 不报错:

typescript

arduino 复制代码
// src/test.ts
const data: any = { foo: 'bar' }
console.log(data)

预期结果: 不报 no-explicit-any。因为你在规则里把它关掉了。

第 6 步:Vue 文件支持

6.1 这一步要解决什么问题

第 5 步让 ESLint 能解析 .ts 文件了,但 .vue 文件还没处理。

.vue 文件和普通 .ts 文件不一样,它是一个容器,里面可能有三种东西:

vue 复制代码
<template>
  <div>{{ msg }}</div>
</template>

<script lang="ts" setup>
const msg = 'hello'
</script>

<style scoped>
div { color: red; }
</style>

ESLint 默认不认识 .vue 文件。如果不配 Vue 支持:

  • 直接报 Parsing error,因为 .vue 不是合法的 JavaScript 语法。
  • 即使勉强解析,<template> 里的指令(v-if、v-for)也会被当成错误语法。
  • <script lang="ts"> 里的 TS 语法也没人处理。

6.2 需要哪两个包,各干什么

bash 复制代码
pnpm add -D eslint-plugin-vue vue-eslint-parser

vue-eslint-parser:

它是解析器 ,负责把 .vue 文件拆开:

  • 从 <template> 里提取模板内容。
  • 从 <script> 里提取脚本内容。
  • 从 <style> 里提取样式内容(ESLint 不管样式,这部分忽略)。

拆完之后,它把 <script> 里的内容交给另一个 parser 去解析。如果你的 <script lang="ts">,那就要把 tseslintParser 传给它。

eslint-plugin-vue:

它是规则插件,提供 Vue 专用规则,比如:

  • vue/multi-word-component-names:组件名必须是多个单词。
  • vue/no-unused-vars:模板定义的变量必须被使用。
  • vue/require-default-prop:props 必须有默认值。
  • vue/no-mutating-props:禁止直接修改 props。

它同时提供一个 flat/recommended 配置集,开箱即用。

6.3 修改配置文件

在 eslint.config.mjs 里,TS 配置后加两块:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js';
import globals from 'globals';
import { configs as tseslintConfigs, parser as tseslintParser } from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import * as vueParser from 'vue-eslint-parser';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  eslint.configs.recommended,
  { ignores: ['public/**', '**/dist/**', '**/assets/**'] },

  // 第 3 步:自定义规则
  { rules: { /* ... */ } },

  // 第 4 步:浏览器环境
  {
    files: ['src/**/*.{js,ts,tsx,vue}'],
    languageOptions: { globals: globals.browser },
  },

  // 第 4 步:Node 环境
  {
    files: [
      '**/*.config.{js,ts,mjs,cjs,mts,cts}',
      'build/**/*.{js,ts,mjs,cjs,mts,cts}',
      'eslint.config.mjs',
    ],
    languageOptions: { globals: globals.node },
    rules: { 'no-console': 'off' },
  },

  // 第 5 步:TS 推荐规则
  ...tseslintConfigs.recommended.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
  })),

  // 第 5 步:TS parser
  {
    files: ['**/*.?([cm])ts', '**/*.?([cm])tsx'],
    languageOptions: {
      parser: tseslintParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        warnOnUnsupportedTypeScriptVersion: false,
      },
    },
    rules: {
      // ... 第 5 步那堆规则
    },
  },

  // ⭐ 第 6 步:Vue 插件推荐规则
  pluginVue.configs['flat/recommended'],

  // ⭐ 第 6 步:Vue 文件的 parser 和规则
  {
    files: ['**/*.vue'],
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        parser: tseslintParser,
        extraFileExtensions: ['.vue'],
        sourceType: 'module',
      },
    },
    rules: {
      'no-undef': 'off',
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': [
        'error',
        { args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
      ],
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
    },
  },
]);

6.4 逐段解释

引入两个包:

javascript 复制代码
import pluginVue from 'eslint-plugin-vue';
import * as vueParser from 'vue-eslint-parser';

eslint-plugin-vue 默认导出的是插件对象,可以直接访问它的 configs。

vue-eslint-parser 必须用 import * as 引入。因为它是 CommonJS 模块,导出的是一个命名空间对象,parser 在它的某个属性上。用 import * as 能拿到完整命名空间,这是社区通用的写法。

注入 Vue 推荐规则:

javascript 复制代码
pluginVue.configs['flat/recommended'],

eslint-plugin-vue 提供了多套配置:

  • base:只包含解析相关的基础配置,不含规则。
  • essential:只包含最重要的规则,避免错误。
  • strongly-recommended:在 essential 基础上加更严格的规则。
  • recommended:在 strongly-recommend 基础上再加更多规则,最常用。

这里的 flat/recommended 就是 flat config 版本的 recommended。它内部已经包含了 parser 配置,直接展开即可。

Vue 文件的 parser 配置:

javascript 复制代码
{
    files:['**/*.vue'],
    parser: vueParser,
    parserOptions: {
        parser: tseslintParser,
        extraFileExtensions: ['.vue'],
        sourceType: 'module',
    },
  },
  ...
}

两层 parser 是重点:

  • 外层 parser: vueParser:告诉 ESLint 用 vue-eslint-parser 来解析 .vue 文件。
  • 内层 parserOptions.parser: tseslintParser:告诉 vue-eslint-parser,把<script lang="ts"> 里的内容交给 TS 解析器。

如果不加内层 parser,<script lang="ts"> 里的 const msg: string = 'hello' 会因为 TS 语法解析失败。

extraFileExtensions 是什么:

vue-eslint-parser 默认不处理 .vue 之外的文件类型。当你项目里还有其他自定义后缀(比如 .vue、.nvue)时,需要在这里声明。正常情况下 .vue 是必须加的。

为什么 Vue 文件也要关 no-unsafe-*:

javascript 复制代码
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',

这几个规则在纯 .ts 文件里非常有用,但在 .vue 文件的 <template> 里会大量误报。

原因:<template> 里的类型推断不如纯 TS 精确,比如 v-for 的 item 、props 的 xxx,在模板编译阶段类型容易变成 any,触发这些规则。社区普遍做法是在 Vue 文件里关掉这几个规则。

这是有意识的取舍 ,不是配置错误。面试可以主动讲:"我在纯 TS 文件里严格用 no-unsafe-*,但在 Vue 文件的模板部分关掉,因为模板的类型推断不如 TS 精确,关掉是为了避免噪音。"

6.5 验证

验证 .vue 文件能被解析:

新建 src/App.vue

vue 复制代码
<template>
  <div>{{ msg }}</div>
</template>

<script lang="ts" setup>
const msg: string = 'hello'
</script>

<style scoped>
div { color: red; }
</style>

运行:

bash 复制代码
npx eslint src/App.vue

预期结果: 不报解析错误。如果报 Parsing error,检查两层 parser 是否都配了。

验证 Vue 规则生效:

vue 复制代码
<template>
  <div>test</div>
</template>

<script setup lang="ts">
// 组件名 foo(单词),会触发 vue/multi-word-component-names
</script>

保存文件名为 Foo.vue(单词),运行:

bash 复制代码
npx eslint src/Foo.vue

预期结果: 报 vue/multi-word-component-names 相关错误。

验证 no-unused-vars 在 Vue 里用 TS 版本:

vue 复制代码
<template>
  <div>test</div>
</template>

<script setup lang="ts">
const unused = 123
</script>

运行:

bash 复制代码
npx eslint src/App.vue

预期结果: 报 'unused' is assigned a value but never used,来源是 @typescript-eslint/no-unused-vars。

验证<script lang="ts">的 TS 语法被处理:

vue 复制代码
<script setup lang="ts">
interface User { id: number }
const user: User = { id: 1 }
console.log(user)
</script>

运行:

bash 复制代码
npx eslint src/App.vue

预期结果: 不报解析错误。说明内层 TS parser 生效了。

第 7 步:导入检查和 Prettier

7.1 这一步要解决什么问题

到第 6 步为止,ESLint 能检查语法、规则、Vue 文件,但还有两个问题:

问题一:导入路径没人管。

typescript 复制代码
import { getUser } from '@/api/user'   // 路径对不对?别名 @ 能不能解析?
import { foo } from './foo'            // foo.ts 存在吗?
import { bar } from './bar'            // 有没有循环依赖?

这些 ESLint 默认不管。你需要 eslint-plugin-import-x 来检查。

问题二:ESLint 和 Prettier 会打架。

Prettier 管格式(单引号、分号、换行),ESLint 也有格式规则(比如 quotes、semi)。两者同时开,会互相冲突:Prettier 把代码格式化成单引号,ESLint 报"必须用双引号"。

解决办法是 eslint-config-prettier,它会关掉所有和 Prettier 冲突的 ESLint 规则。

7.2 安装依赖

bash 复制代码
pnpm add -D eslint-plugin-import-x eslint-import-resolver-typescript eslint-plugin-prettier eslint-config-prettier

四个包各干什么:

  • eslint-plugin-import-x:import / export 语句的检查插件。-x 是原 eslint-plugin-import 的维护分支,对 flat config 和 TS 支持更好。
  • eslint-import-resolver-typescript:让 import-x 能解析 TS 的路径别名(@/ 这种)和 .ts/.vue 后缀。
  • eslint-plugin-prettier:把 Prettier 当成 ESLint 规则跑。这样 eslint --fix 能同时修 ESLint 和 Prettier 的问题。
  • eslint-config-prettier:关掉所有与 Prettier 冲突的 ESLint 规则。

7.3 修改配置文件

在 eslint.config.mjs 里,Vue 配置后面加:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js';
import globals from 'globals';
import { configs as tseslintConfigs, parser as tseslintParser } from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import * as vueParser from 'vue-eslint-parser';
import { importX } from 'eslint-plugin-import-x';
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  // ...第 1 到第 6 步的所有配置

  // ⭐ 第 7 步:import-x 推荐规则
  importX.flatConfigs.recommended,
  importX.flatConfigs.typescript,

  // ⭐ 第 7 步:import-x 的 resolver 配置
  {
    settings: {
      'import-x/resolver': {
        typescript: { projectService: true },
        node: true,
      },
      'import-x/extensions': ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.tsx', '.vue'],
    },
  },

  // ⭐ 第 7 步:允许配置文件里用 require
  {
    files: ['**/*.?([cm])js', '**/*.cts'],
    rules: {
      '@typescript-eslint/no-require-imports': 'off',
    },
  },

  // ⭐ 第 7 步:Prettier 必须放最后
  eslintPluginPrettier,
]);

7.4 逐段解释

引入 import-x:

javascript 复制代码
import { importX } from 'eslint-plugin-import-x';

注意这里是 { importX },不是默认导入。eslint-plugin-import-x 导出一个命名对象 importX,里面包含 flatConfigs。

注入 import-x 推荐规则:

javascript 复制代码
importX.flatConfigs.recommended,
importX.flatConfigs.typescript,
  • flatConfigs.recommended:基础导入规则,比如 no-unresolved(路径能不能解析)、no-duplicates(有没有重复导入)。
  • flatConfigs.typescript:针对 TS 的补充规则,比如 no-unresolved 在 TS 项目里的正确行为。

这两个直接展开,不需要 .map() 加 files,因为它们内部已经处理好了。

resolver 配置:

javascript 复制代码
{
  settings: {
    'import-x/resolver': {
      typescript: { projectService: true },
      node: true,
    },
    'import-x/extensions': ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.tsx', '.vue'],
  },
},

typescript: { projectService: true }:

告诉 import-x 用 TypeScript 的 Project Service 来解析路径。这样 @/api/user 这种别名才能正确解析到 src/api/user.ts。

这里为什么可以提前用 projectService:

projectService 在这里不是给 ESLint 的类型感知用的,是给 import-x 的 resolver 用的。两个用途不同,不冲突。ESLint 自己的 projectService 是第 9 步才加。

node:true:同时启用 Node 的解析策略,处理 node_modules 里的包。

import-x/extensions:告诉 import-x 在解析导入时尝试这些后缀。如果你写 import App from './App',它会依次尝试 ./App.js、./App.ts、./App.vue 等。

为什么单独关掉 no-require-imports:

javascript 复制代码
{
    files: ['**/*.?([cm])js','**/*.cts']
    rules: {
        '@typescript-eslint/no-require-imports': 'off',
    }
}

@typescript-eslint/no-require-imports 是第 5 步 recommended 里的规则,禁止用 require()。但配置文件(比如 .cjs)和某些 CommonJS 文件必须用 require。这里只针对 .js 和 .cts 关掉,其他文件(.ts、.mts)仍然禁止。

Prettier 放最后:

javascript 复制代码
eslintPluginPrettier,

eslint-plugin-prettier/recommended 做了两件事:

  1. 把 Prettier 注册为 ESLint 规则,eslint --fix 时能格式化。
  2. 通过 eslint-config-prettier 关掉所有与 Prettier 冲突的 ESLint 规则。

为什么必须放最后:

因为它是"关闭冲突规则"的配置。如果放在前面,后面第5、6步的规则会覆盖它,冲突又回来了,放在最后,确保覆盖前面所有配置。

7.5 验证

验证路径别名解析:

前提:你的 tsconfig.json 里配了 @/* 别名,比如:

json 复制代码
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

在 src/test.ts 里写:

typescript 复制代码
import { something } from '@/api/user'
console.log(something)

运行:

bash 复制代码
npx eslint src/test.ts

预期结果: 不报 Unable to resolve path to module。

验证路径不存在时报错:

typescript 复制代码
import { something } from '@/api/not-exist'
console.log(something)

运行:

bash 复制代码
npx eslint src/test.ts

预期结果: 报 Unable to resolve path to module '@/api/not-exist'。

验证 Prettier 生效:

写一段格式很乱的代码:

typescript 复制代码
const   a=1;const b   =2
console.log(a,b)

运行:

bash 复制代码
npx eslint src/test.ts --fix

预期结果: 文件被格式化成:

typescript 复制代码
const a = 1
const b = 2
console.log(a, b)

(具体格式取决于你的 .prettierrc。) 验证 Prettier 冲突规则被关掉:

随便写个双引号字符串:

typescript 复制代码
const str = "hello"
console.log(str)

运行:

bash 复制代码
npx eslint src/test.ts

预期结果: 不报 quotes 相关错误。因为 eslint-config-prettier 把这条规则关了。

7.6 一个容易踩的坑

如果你运行 npx eslint src/test.ts --fix 后,Prettier 报错说"找不到配置文件",说明你没建 .prettierrc。ESLint 里的 Prettier 规则会读取项目根的 Prettier 配置。

先创建 .prettierrc:

json 复制代码
{
  "semi": false,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none"
}

这个文件是 Prettier 自己的配置,和 ESLint 独立。ESLint 通过 eslint-plugin-prettier 调用 Prettier 时会读取它。

第 8 步:类型感知(压轴)

8.1 这一步要解决什么问题

前 7 步的 ESLint 只看语法 。它能把代码解析成 AST,检查变量有没有声明、有没有用 var、导入路径对不对。但有一类 Bug 语法上完全合法,只有类型信息才能发现。

最典型的是 no-floating-promises:

typescript 复制代码
async function saveEmployee() {
    return api.update(data)
}

function handleSubmit() {
    saveEmployee()  // ⚠️ 没有 await,Promise 悬空
}

saveEmployee() 返回 Promise<void>,调用时没 await 也没 .catch(),就是一个悬浮 Promise。用户以为保存成功了,实际可能失败了。普通 ESLint 看不出任何问题,但类型感知会直接报错。

类似的高价值规则还有:

  • no-misused-promises:在 if 里误用 async 函数。
  • await-thenable:对非 Promise 用 await。
  • no-unnecessary-type-assertion:多余的类型断言。
  • restrict-template-expressions:模板字符串用了非字符串类型。

这些规则只有拿到 TypeScript 的完整类型信息才能跑。

8.2 核心概念:projectService 是什么

类型感知的关键,是让 ESLint 的解析器知道每个源文件属于哪个 tsconfig,从而拿到完整类型信息。

**旧方案(parserOptions.project):

javascript 复制代码
parserOptions: {
    project: './tsconfig.json'
}

要求手动指定 tsconfig 路径。在 monorepo 或多 tsconfig 项目里极其痛苦,经常要单独维护一份 tsconfig.eslint.json,把 ESLint 要检查的所有文件都 include 进去。

**新方案(parserOptions.projectService: true):

这是 TypeScript-eslint v8 稳定的 API。它直接调用 TypeScript 的 Project Service ------ 也就是 VS Code 编辑器内部用的同一套服务 ------ 自动为每个文件找到对应的 tsconfig,不再需要 ESLint 专用的 tsconfig。

对比项 旧 project 新 projectService
配置复杂度 高,需手动指定路径 低,自动检测
monorepo 支持 差 好,支持 project references
与编辑器一致性 低 高,同一套服务
类型信息准确性 取决于手动配置 和 VS Code 一致

8.3 修改配置文件

类型感知需要改两个地方。

改动一:把recommended换成recommendedTypeChecked

第 5 步你写的是:

javascript 复制代码
...tseslintConfigs.recommended.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
}))

改成:

javascript 复制代码
...tseslintConfigs.recommendedTypeChecked.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
})),

recommended 和 recommendedTypeChecked 的区别:

  • recommended:纯语法规则,不需要类型信息,速度快。
  • recommendedTypeChecked:包含 recommended 的所有规则, 再加上需要类型信息的规则。它需要 parserOptions.projectService 才能工作。

改动二:在 TS 和 Vue 的 parserOptions 里加 projectService

第 5 步 TS 文件的配置:

javascript 复制代码
{
    files:['**/*.?([cm])ts','**/*.?([cm])tsx'],
    languageOptions: {
        parser:tseslintParser,
        parserOptions: {
            projectService: true, // 新增
            ecmaVersion: 'latest',
            sourceType: 'module',
            warnOnUnsupportedTypeScriptVersion: false,
        },
    },
    rules: {
        // ... 第 5 步那堆规则
    }
}

第 6 步 Vue 文件的配置:

javascript 复制代码
{
    files: ['**/*.vue'],
    languageOptions: {
        parser: vueParser,
        parserOptions: {
            projectService: true, // 新增
            extraFileExtensions: ['.vue'],
            sourceType: 'module',
        },
    },
    rules: {
        // ... 第 6 步那堆规则
    },
},

注意:projectService 必须同时加在 TS 和 Vue 两处 。因为 TS 文件和 Vue 文件是分开配置的,各自需要自己的 parserOptions。

8.4 完整配置

把第 5、6 步的配置合并后,最终是这样:

javascript 复制代码
// eslint.config.mjs
import eslint from '@eslint/js';
import globals from 'globals';
import { configs as tseslintConfigs, parser as tseslintParser } from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import * as vueParser from 'vue-eslint-parser';
import { importX } from 'eslint-plugin-import-x';
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  eslint.configs.recommended,
  { ignores: ['public/**', '**/dist/**', '**/assets/**'] },

  // 第 3 步:自定义规则
  {
    rules: {
      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
      'no-unused-vars': [
        process.env.NODE_ENV === 'production' ? 'error' : 'warn',
        { vars: 'all', args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
      ],
      'no-undef': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'accessor-pairs': 'error',
      eqeqeq: ['error', 'always', { null: 'ignore' }],
      'no-label-var': 'error',
      'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
      'no-lone-blocks': 'error',
      'no-multi-str': 'error',
      'no-new-wrappers': 'error',
      'no-return-assign': ['error', 'except-parens'],
      'no-self-compare': 'error',
      'no-sequences': 'error',
      'no-undef-init': 'error',
      'no-unmodified-loop-condition': 'error',
      'no-unneeded-ternary': ['error', { defaultAssignment: false }],
      'no-unreachable-loop': 'error',
      'prefer-const': 'error',
      'no-useless-escape': 'off',
    },
  },

  // 第 4 步:浏览器环境
  {
    files: ['src/**/*.{js,ts,tsx,vue}'],
    languageOptions: { globals: globals.browser },
  },

  // 第 4 步:Node 环境
  {
    files: [
      '**/*.config.{js,ts,mjs,cjs,mts,cts}',
      'build/**/*.{js,ts,mjs,cjs,mts,cts}',
      'eslint.config.mjs',
    ],
    languageOptions: { globals: globals.node },
    rules: { 'no-console': 'off' },
  },

  // ⭐ 第 8 步:类型感知规则集
  ...tseslintConfigs.recommendedTypeChecked.map((config) => ({
    ...config,
    files: ['**/*.{ts,tsx,mts,cts,vue}'],
  })),

  // 第 6 步:Vue 插件推荐规则
  pluginVue.configs['flat/recommended'],

  // ⭐ 第 8 步:Vue 文件 + 类型感知
  {
    files: ['**/*.vue'],
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        projectService: true,
        parser: tseslintParser,
        extraFileExtensions: ['.vue'],
        sourceType: 'module',
      },
    },
    rules: {
      'no-undef': 'off',
      'no-unused-vars': 'off',
      '@typescript-eslint/no-unused-vars': [
        'error',
        { args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
      ],
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
    },
  },

  // ⭐ 第 8 步:TS 文件 + 类型感知
  {
    files: ['**/*.?([cm])ts', '**/*.?([cm])tsx'],
    languageOptions: {
      parser: tseslintParser,
      parserOptions: {
        projectService: true,
        ecmaVersion: 'latest',
        sourceType: 'module',
        warnOnUnsupportedTypeScriptVersion: false,
      },
    },
    rules: {
      'no-undef': 'off',
      'no-unused-vars': 'off',
      '@typescript-eslint/ban-ts-comment': 'off',
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/prefer-as-const': 'warn',
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-unused-expressions': 'off',
      '@typescript-eslint/no-unsafe-function-type': 'off',
      '@typescript-eslint/no-wrapper-object-types': 'off',
      '@typescript-eslint/no-namespace': 'off',
      '@typescript-eslint/no-unused-vars': [
        'error',
        { args: 'after-used', argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
      ],
    },
  },

  // 第 7 步:import-x
  importX.flatConfigs.recommended,
  importX.flatConfigs.typescript,
  {
    settings: {
      'import-x/resolver': {
        typescript: { projectService: true },
        node: true,
      },
      'import-x/extensions': ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.tsx', '.vue'],
    },
  },

  {
    files: ['**/*.?([cm])js', '**/*.cts'],
    rules: {
      '@typescript-eslint/no-require-imports': 'off',
    },
  },

  // 第 7 步:Prettier 放最后
  eslintPluginPrettier,
]);

8.5 验证类型感知是否生效

**验证 no-floating-promises:

typescript 复制代码
// src/test.ts
async function saveEmployee() {
    return Promise.resolve('saved')
}

function handleSubmit() {
    saveEmployee() //这里应该报 no-floating-promises
}
handleSubmit()

运行:

bash 复制代码
npx eslint src/test.ts

预期结果:报

test 复制代码
Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator

**验证 no-misused-promises:

typescript 复制代码
// src/test.ts
async function check(): Promise<boolean> {
    return true
}

if (check()) {  // 这里应该报 no-misused-promises
    console.log('ok')
}

运行:

bash 复制代码
npx eslint src/test.ts

预期结果 :报 Expected a non-Promise value

**验证 await-thenable:

typescript 复制代码
// src/test.ts
async function test() {
    const num = 123
    await num // 这里应该报 await-thenable
}
test()

运行:

bash 复制代码
npx eslint src/test.ts

预期结果 :报 Unexpected await of a non-Promise value。

验证 Vue 文件里类型感知也生效:

vue 复制代码
<template>
    <div>{{ msg }}</div>
</template>

<script setup lang='ts'>
async function load() {
    return Promise.resolve('data')
}

function init() {
    load() // 应该报 no-floating-promises
}
init()
</script>

运行:

bash 复制代码
npx eslint src/App.vue

预期结果 :报 no-floating-promises。

8.6 常见问题排查

**问题一:报 Parsing error: Cannot read file 'tsconfig.json'

说明 projectService 找不到 tsconfig。检查:

  1. 项目根目录有没有 tsconfig.json
  2. tsconfig.json 的 include 是否包含你在检查的文件。
  3. 如果是 monorepo,每个包都要自己的 tsconfig.json。

问题二:类型感知规则不报

说明 ESLint 没拿到类型信息。检查:

  1. recommendedTypeChecked 有没有被正确 .map()
  2. parserOptions.projectService 有没有加在 TS 和 Vue 两处。
  3. 运行 npx eslint --debug src/test.ts 看解析器有没有加载 tsconfig。

问题三:Lint 变得非常慢

类型感知需要 TypeScript 分析整个项目,比纯语法检查慢很多。这是它的核心权衡。优化策略:

  • 只在核心业务代码上开类型感知,测试、配置文件用 disableTypeChecked 关掉。
  • CI 里单独跑类型感知任务,开发时用轻量 Lint 保证速度
  • 用 --cache 缓存检查结果,只检查改动的文件。

8.7 面试话术

把这个配置讲成一个有决策、有取舍的工程判断:

"我的项目用 TypeScript 严格模式,但 ESLint 默认只做语法检查。有些异步 Bug ------ 比如悬浮 Promise ------ 语法上完全合法,只有类型系统才能发现。所以我启用了 typescript-eslint 的 type-aware linting。

配置上我用了 v8 新的 parserOptions.projectService,而不是旧的 project 字段。因为 projectService 直接复用 TypeScript 的 Project Service,和 VS Code 编辑器用的是同一套服务,在 monorepo 或多 tsconfig 场景下不用再维护 ESLint 专用的 tsconfig,配置更简单,和编辑器的行为也一致。

代价是类型感知 Lint 会慢一些,因为需要分析整个项目。我的处理是只在核心业务代码上开启,对测试和配置文件用 disableTypeChecked 关掉,CI 里单独跑类型感知任务,保证开发时的反馈速度。"

相关推荐
code_slave(码畜)1 小时前
微服务架构落地:消息队列架构设计(上篇)——异步解耦、削峰填谷,看懂业务事件流转本质
spring boot·spring cloud·微服务·云原生·架构
小坏讲微服务2 小时前
Spring Boot 4 新特性全解析:从上手到生产实战
java·spring boot·后端·架构·springboot4
知野小兔3 小时前
Vuex和Pinia状态管理
vue.js
Psycho_MrZhang4 小时前
多 Agent 研究系统的架构与实践
人工智能·架构
VX_bysjlw9854 小时前
数码设备销售网站设计与实现39138-计算机毕设原创(免费领源码+带部署教程)
java·vue.js·spring boot·mysql·tomcat·mybatis·idea
时空节拍AI数字人4 小时前
数字文旅补贴来了,景区申报要注意什么?
大数据·人工智能·百度·3d·ai·架构·aigc
Dawson Zhu5 小时前
《Agentic Design Patterns》第 8 章导读:记忆管理(Memory Management)
人工智能·语言模型·架构·aigc·agi
code_slave(码畜)5 小时前
微服务架构落地:消息队列架构设计(下篇)——消息堆积、死信治理与集群监控告警实战
spring boot·spring cloud·微服务·云原生·中间件·架构
茶底世界之下5 小时前
预乘 Alpha 图层合成为什么会越叠越暗?
架构·swift