ESLint + Prettier + Husky + lint-staged + Commitlint 的完整配置

20 阅读8分钟

搭建指南

适用于 Vue / React / 纯 TS 等前端项目,覆盖 ESLint + Prettier + Husky + lint-staged + Commitlint 的完整配置。


一、整体架构

git commit
   │
   ├─► pre-commit 钩子 (Husky)
   │       └─► lint-staged
   │               ├─► prettier --write   # 自动格式化暂存文件
   │               └─► eslint --fix       # 自动修复代码问题,有无法修复的错误则阻断提交
   │
   └─► commit-msg 钩子 (Husky)
           └─► commitlint                 # 校验 commit message 格式,不合规则阻断提交

二、文件位置总览

所有配置文件均放在项目根目录,目录结构如下(.prettierignore 和 eslint.config.js 完全同级,都在根目录下,只是文件浏览器的排序显示问题):

project-root/
├── .husky/
│   ├── commit-msg        # commit message 校验钩子
│   └── pre-commit        # 提交前代码检查钩子
├── src/
├── .commitlintrc.cjs     # Commitlint 配置
├── .prettierignore       # Prettier 忽略文件列表
├── .prettierrc.json      # Prettier 格式化配置
├── eslint.config.js      # ESLint 配置(Flat Config 格式)
└── package.json          # 包含 lint-staged 配置和相关脚本

三、安装依赖

npm install -D \
  eslint \
  prettier \
  eslint-plugin-prettier \
  eslint-config-prettier \
  husky \
  lint-staged \
  @commitlint/cli \
  @commitlint/config-conventional

Vue 项目额外安装:

npm install -D \
  eslint-plugin-vue \
  vue-eslint-parser \
  @typescript-eslint/parser

纯 TypeScript / React 项目额外安装:

npm install -D \
  @typescript-eslint/parser \
  @typescript-eslint/eslint-plugin

四、Prettier 配置

在项目根目录新建 .prettierrc.json

注意:JSON 文件不支持注释,下方注释仅用于说明,实际文件中请删除。

{
    "printWidth": 100,                      // 每行最大字符数,超出则自动换行
    "tabWidth": 4,                          // 缩进宽度:4 个空格
    "useTabs": false,                       // 使用空格缩进,而非 Tab
    "singleQuote": true,                    // 字符串使用单引号
    "semi": false,                          // 语句末尾不加分号
    "trailingComma": "none",               // 对象/数组末尾不加尾随逗号
    "bracketSpacing": true,                 // 对象字面量括号内侧保留空格:{ foo: bar }
    "endOfLine": "auto",                    // 自动识别行尾符(CRLF/LF),避免跨平台冲突
    "arrowParens": "avoid",                 // 单参数箭头函数省略括号:x => x
    "htmlWhitespaceSensitivity": "ignore",  // 忽略 HTML 标签内联空白,格式化不受影响
    "overrides": [
        {
            "files": ["*.scss", "*.less"],          // 匹配 Scss 和 Less 文件
            "options": { "parser": "css" }          // 使用 CSS 解析器格式化
        },
        {
            "files": ["*.ts", "*.tsx"],             // 匹配 TypeScript 文件
            "options": { "parser": "typescript" }   // 使用 TypeScript 解析器
        },
        {
            "files": ["*.js", "*.jsx"],             // 匹配 JavaScript 文件
            "options": { "parser": "babel" }        // 使用 Babel 解析器
        },
        {
            "files": ["*.json"],                    // 匹配 JSON 文件
            "options": { "parser": "json" }         // 使用 JSON 解析器
        }
    ]
}

在项目根目录新建 .prettierignore

# 第三方依赖,不格式化
node_modules
# 构建产物,不格式化
dist
# 静态资源,不格式化
public

五、ESLint 配置

5.1 Vue 项目(Flat Config 格式,ESLint v9+)

在项目根目录新建 eslint.config.js

import globals from 'globals'
import pluginJs from '@eslint/js'
import eslintPluginVue from 'eslint-plugin-vue'
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
import vueEslintParser from 'vue-eslint-parser'

export default [
    {
        files: ['**/*.{js,mjs,cjs,vue}'], // 匹配所有 JS 和 Vue 文件
        languageOptions: {
            globals: { ...globals.browser }, // 注入浏览器环境全局变量(window、document 等)
            parser: vueEslintParser,          // 使用 vue-eslint-parser 解析 .vue 文件
            parserOptions: {
                ecmaVersion: 'latest',        // 支持最新 ECMAScript 语法
                sourceType: 'module',         // 使用 ESM 模块规范
                // Vue 文件内 <script lang="ts"> 由此嵌套解析器处理 TypeScript 语法
                parser: '@typescript-eslint/parser',
                ecmaFeatures: { jsx: true }   // 支持 JSX 语法
            }
        }
    },
    pluginJs.configs.recommended,                 // 启用 ESLint 官方推荐规则集
    ...eslintPluginVue.configs['flat/essential'],  // 启用 Vue 必要规则集(Flat Config 格式)
    {
        ...eslintPluginPrettier, // 集成 Prettier,将格式化规则作为 ESLint 规则执行,并自动关闭冲突规则
        rules: {
            'prettier/prettier': 'error',               // 代码风格不符合 Prettier 配置时报错
            'vue/multi-word-component-names': 'off',    // 允许单字组件名(如 index.vue)
            'vue/no-mutating-props': 'error',           // 禁止在子组件中直接修改 props
            'no-var': 'error',                          // 禁用 var,强制使用 let/const
            'no-unused-vars': 'error',                  // 禁止声明未使用的变量
            'no-console': 'warn',                       // 提醒清除 console 语句
            'no-debugger': 'warn'                       // 提醒清除 debugger 语句
        }
    },
    {
        ignores: ['node_modules', 'dist', 'public'] // 忽略第三方依赖、构建产物和静态资源目录
    }
]

5.2 纯 TypeScript / React 项目(Flat Config 格式,ESLint v9+)

在项目根目录新建 eslint.config.js

import globals from 'globals'
import pluginJs from '@eslint/js'
import tsPlugin from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'

export default [
    {
        files: ['**/*.{js,mjs,cjs,ts,tsx}'], // 匹配所有 JS 和 TS 文件
        languageOptions: {
            globals: { ...globals.browser }, // 注入浏览器环境全局变量
            parser: tsParser,                // 使用 TypeScript 解析器解析所有文件
            parserOptions: {
                ecmaVersion: 'latest',       // 支持最新 ECMAScript 语法
                sourceType: 'module'         // 使用 ESM 模块规范
            }
        },
        plugins: { '@typescript-eslint': tsPlugin } // 注册 TypeScript ESLint 插件
    },
    pluginJs.configs.recommended, // 启用 ESLint 官方推荐规则集
    {
        ...eslintPluginPrettier, // 集成 Prettier,将格式化规则作为 ESLint 规则执行,并自动关闭冲突规则
        rules: {
            'prettier/prettier': 'error',              // 代码风格不符合 Prettier 配置时报错
            'no-var': 'error',                         // 禁用 var,强制使用 let/const
            'no-unused-vars': 'error',                 // 禁止声明未使用的变量
            '@typescript-eslint/no-explicit-any': 'warn' // 使用 any 类型时给出警告
        }
    },
    {
        ignores: ['node_modules', 'dist'] // 忽略第三方依赖和构建产物目录
    }
]

注意eslint-plugin-prettier/recommended 已内置 eslint-config-prettier,会自动关闭与 Prettier 冲突的 ESLint 格式规则,无需手动引入 eslint-config-prettier


六、Husky 配置

6.1 初始化

package.jsonscripts 中添加以下脚本,然后执行 npm run prepare

{
    "scripts": {
        "prepare": "husky"
    }
}

prepare 是 npm 生命周期钩子,在 npm install 完成后自动执行,确保团队成员拉取代码后 Husky 自动激活,无需手动运行。

npm run prepare
# 或直接运行
npx husky init

6.2 创建 pre-commit 钩子

.husky/pre-commit 写入以下内容:

# 提交前执行 lint-staged,对暂存文件进行格式化和代码检查
npm run lint-staged

6.3 创建 commit-msg 钩子

.husky/commit-msg 写入以下内容:

#!/bin/sh
# 校验 commit message 格式
# --no:不自动安装缺失的包,强制使用项目本地安装的 commitlint 版本
# --edit ${1}:读取 Git 传入的临时 commit message 文件进行校验
npx --no -- commitlint --edit ${1}

七、lint-staged 配置

package.json 中添加以下内容:

{
    "scripts": {
        "lint-staged": "lint-staged"
    },
    "lint-staged": {
        "*.{ts,tsx,js,jsx,cjs,mjs,vue}": [
            "prettier --write",
            "eslint --fix"
        ],
        "*.{css,scss,less,json,md,html}": [
            "prettier --write"
        ]
    }
}

重要:将文件类型拆分为两组:

  • 代码文件(.ts/.js/.vue 等)同时执行 Prettier 格式化 + ESLint 修复
  • 样式/配置/文档文件(.css/.scss/.json/.md 等)只执行 Prettier,不执行 ESLint(ESLint 不原生处理这些类型,混在一起会产生警告)

prettier --writeeslint --fix 之前执行,确保 ESLint 接收到已格式化的代码,减少 prettier/prettier 规则误报。


八、Commitlint 配置

在项目根目录新建 .commitlintrc.cjs

module.exports = {
    extends: ['@commitlint/config-conventional'], // 继承约定式提交规范
    rules: {
        // type-enum:限制允许使用的提交类型
        // 规则值说明:[0] = 禁用,[1] = 警告,[2] = 错误(阻断提交)
        'type-enum': [
            2,       // 错误级别:不符合则阻断提交
            'always',
            [
                'feat',      // 新功能
                'fix',       // 修复 bug
                'refactor',  // 代码重构(不含新功能、不修复 bug)
                'perf',      // 性能优化
                'docs',      // 文档修订
                'style',     // 格式调整(不影响代码逻辑,如空格、缩进)
                'build',     // 构建流程、依赖变更
                'test',      // 新增或修改测试
                'chore',     // 其他杂项(不修改 src 或 test 的变更)
                'ci',        // CI 配置变更
                'revert'     // 回退提交
            ]
        ],
        'body-leading-blank': [2, 'always'],   // body 与 subject 之间必须有空行
        'type-case': [0],                       // type 大小写不限制
        'type-empty': [0],                      // type 不强制非空(由 type-enum 保证)
        'scope-empty': [0],                     // scope 可为空
        'scope-case': [0],                      // scope 大小写不限制
        'subject-full-stop': [0, 'never'],      // subject 末尾不强制加句号
        'subject-case': [0, 'never'],           // subject 大小写不限制
        'header-max-length': [0, 'always', 72]  // header 长度不限制
    }
}

commit message 格式

<type>(<scope>): <subject>

<body>(可选,与 subject 之间须有空行)

示例:

feat(auth): 新增用户登录功能
fix(api): 修复分页查询返回数量错误
docs: 更新 README 安装说明

九、package.json 脚本汇总

{
    "scripts": {
        "prepare": "husky",
        "lint-staged": "lint-staged",
        "lint": "eslint --fix src",
        "format": "prettier --write ./src/"
    }
}

十、迁移到新项目的步骤

  1. 安装依赖(参考第三节,按项目类型选择)
  2. 复制配置文件到项目根目录:.prettierrc.json.prettierignoreeslint.config.js.commitlintrc.cjs
  3. 更新 package.json:添加 preparelint-stagedlintformat 脚本及 lint-staged 配置
  4. 初始化 Huskynpm run prepare
  5. 创建钩子文件.husky/pre-commit.husky/commit-msg
  6. 验证
    # 验证 ESLint
    npm run lint
    # 验证 Prettier
    npm run format
    # 验证 Commitlint(会提示格式错误,符合预期)
    echo "wrong format" | npx commitlint
    # 验证正确格式(应通过,无错误输出)
    echo "feat: test" | npx commitlint
    

十一、常见问题

Q: npm install 后钩子没有生效?

检查 package.json 中是否有 "prepare": "husky" 脚本,且 .husky/ 目录下的钩子文件有可执行权限(Linux/Mac 需 chmod +x .husky/*)。

Q: ESLint 报 no-undef 错误,但变量已存在(如 Vue 的 refcomputed)?

使用了 unplugin-auto-import 插件时,需在 ESLint 配置的 globals 中读取插件生成的 .eslintrc-auto-import.json 文件:

import { readFile } from 'node:fs/promises'
const autoImportFile = new URL('./.eslintrc-auto-import.json', import.meta.url)
const autoImportGlobals = JSON.parse(await readFile(autoImportFile, 'utf8'))
// 在 languageOptions.globals 中展开:{ ...globals.browser, ...autoImportGlobals.globals }

Q: Windows 下 Prettier 每次格式化都产生大量变更?

确保 .prettierrc.json 中设置 "endOfLine": "auto",同时在项目根目录的 .gitattributes 中统一行尾处理:

* text=auto eol=lf

Q: commit-msg 钩子报 commitlint: command not found

确认已安装 @commitlint/cli,且钩子文件使用 npx --no -- commitlint --edit ${1} 格式,--no 参数防止 npx 自动下载包,强制使用项目本地安装版本。


VSCODE settings.json 配置

{
    "[html]": {
        "editor.defaultFormatter": "vscode.html-language-features"
    },
    "workbench.tree.indent": 16,
    "liveServer.settings.donotShowInfoMsg": true,
    "editor.lineHeight": 27,
    // vscode默认启用了根据文件类型自动设置tabsize的选项
    "editor.detectIndentation": false,
    // 重新设定tabsize
    "editor.tabSize": 4, // #每次保存的时候自动格式化
    //  #去掉代码结尾的分号
    "prettier.semi": true,
    // 格式化缩进保留4个空格
    "prettier.tabWidth": 4,
    //  #使用带引号替代双引号
    "prettier.singleQuote": true,
    // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid)
    "prettier.arrowParens": "avoid",
    //  #让函数(名)和后面的括号之间加个空格
    "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
    "search.followSymlinks": false,
    "git.autorefresh": true,
    // 格式化stylus, 需安装Manta's Stylus Supremacy插件
    // "stylusSupremacy.insertColons": false, // 是否插入冒号
    // "stylusSupremacy.insertSemicolons": false, // 是否插入分好
    // "stylusSupremacy.insertBraces": false, // 是否插入大括号
    // "stylusSupremacy.insertNewLineAroundImports": false, // import之后是否换行
    // "stylusSupremacy.insertNewLineAroundBlocks": false,
    "editor.multiCursorModifier": "alt",
    "editor.fontWeight": "700",
    "diffEditor.ignoreTrimWhitespace": false,
    "workbench.settings.enableNaturalLanguageSearch": false,
    "git-graph.contextMenuActionsVisibility": {},
    "javascript.updateImportsOnFileMove.enabled": "always",
    "[vue]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[json]": {
        "editor.defaultFormatter": "vscode.json-language-features"
    },
    "[jsonc]": {
        "editor.defaultFormatter": "vscode.json-language-features"
    },
    "[css]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[less]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[scss]": {
        "editor.defaultFormatter": "HookyQR.beautify"
    },
    "less.compile": {
        "less.compile": {
            "compress": true, // 是否删除多余空白字符
            "sourceMap": false, // 是否创建文件目录树,true的话会自动生成一个 .css.map 文件
            "out": "${workspaceRoot}\\src\\assets\\css\\" // 输出css文件目录,false为不输出
        }
    },
    "explorer.compactFolders": false,
    "html.format.unformatted": "",
    "html.format.contentUnformatted": "",
    "explorer.confirmDragAndDrop": false,
    "editor.formatOnType": true,
    "editor.formatOnPaste": true,
    "editor.formatOnSave": true,
    "editor.fontSize": 18,
    "editor.fontLigatures": false,
    "launch": {
        "configurations": [],
        "compounds": []
    },
    "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "editor.wordWrap": "on",
    "[handlebars]": {
        "editor.suggest.insertMode": "replace"
    },
    "editor.cursorBlinking": "expand",
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.largeFileOptimizations": false,
    "editor.accessibilitySupport": "off",
    "editor.cursorSmoothCaretAnimation": "on",
    "editor.guides.bracketPairs": "active",
    "editor.inlineSuggest.enabled": true,
    "editor.showUnused": true,
    "editor.suggestSelection": "recentlyUsedByPrefix",
    "editor.acceptSuggestionOnEnter": "smart",
    "editor.suggest.snippetsPreventQuickSuggestions": false,
    "editor.stickyScroll.enabled": true,
    "editor.hover.sticky": true,
    "editor.suggest.insertMode": "replace",
    "editor.bracketPairColorization.enabled": true,
    "editor.autoClosingBrackets": "beforeWhitespace",
    "editor.autoClosingDelete": "always",
    "editor.autoClosingOvertype": "always",
    "editor.autoClosingQuotes": "beforeWhitespace",
    "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": "explicit",
        "source.fixAll.stylelint": "explicit",
        "source.organizeImports": "never"
    },
    "editor.quickSuggestions": {
        "other": true,
        "comments": true,
        "strings": true
    },
    // "leetcode.endpoint": "leetcode-cn",
    // "leetcode.workspaceFolder": "C:\\Users\\zhengcf01\\.leetcode",
    // "leetcode.defaultLanguage": "javascript",
    // "leetcode.hint.configWebviewMarkdown": false,
    // "leetcode.hint.commentDescription": false,
    // "leetcode.hint.commandShortcut": false,
    "files.autoSave": "afterDelay",
    "files.associations": {
        "*.cjson": "jsonc",
        "*.wxss": "css",
        "*.wxs": "javascript"
    },
    "emmet.includeLanguages": {
        "wxml": "html"
    },
    // "minapp-vscode.disableAutoConfig": true,
    // "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
    "settingsSync.ignoredExtensions": [],
    "interview.updateNotification": 1679448175043,
    "workbench.colorTheme": "One Dark Pro Darker",
    "workbench.iconTheme": "vscode-icons-mac",
    "eslint.enable": true,
    "eslint.run": "onType",
    "interview.workspaceFolder": "C:\\Users\\wingwang\\.FEInterview",
    "json.schemas": [],
    "vue.inlayHints.optionsWrapper": false,
    "workbench.editorAssociations": {
        "*.ttf": "default"
    },
    "workbench.editor.empty.hint": "hidden",
    "tabnine.experimentalAutoImports": true,
    "workbench.settings.applyToAllProfiles": [],
    "git.openRepositoryInParentFolders": "never",
    "liveServer.settings.AdvanceCustomBrowserCmdLine": "",
    "terminal.integrated.defaultProfile.windows": "Windows PowerShell",
    // 控制相关文件嵌套展示
    "explorer.fileNesting.enabled": true,
    "explorer.fileNesting.expand": false,
    "explorer.fileNesting.patterns": {
        "*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
        "*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
        "*.env": "$(capture).env.*",
        "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json,package-lock.json,README.md",
        "Dockerfile": "Dockerfile,.docker*,docker-entrypoint.sh,build-local-docker*,nginx.conf,buildspec.yml",
        "eslint.config.js": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,.ls-lint*,cspell.json,.eslintrc-auto-import.json,.editorconfig",
        "tailwind.config.mjs": "postcss.*",
        "downloadI18n.js": "uploadI18n.js",
    },
    "vue.server.hybridMode": true,
    "typescript.tsdk": "node_modules/typescript/lib",
    "terminal.integrated.scrollback": 10000,
    "vue.autoInsert.dotValue": true,
    "terminal.integrated.profiles.windows": {
        "PowerShell": {
            "source": "PowerShell",
            "icon": "terminal-powershell"
        },
        "Command Prompt": {
            "path": [
                "${env:windir}\\Sysnative\\cmd.exe",
                "${env:windir}\\System32\\cmd.exe"
            ],
            "args": [],
            "icon": "terminal-cmd"
        },
        "Git Bash": {
            "source": "Git Bash"
        },
        "Windows PowerShell": {
            "path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
        }
    },
    "typescript.inlayHints.enumMemberValues.enabled": true,
    "typescript.preferences.preferTypeOnlyAutoImports": true,
    "typescript.preferences.includePackageJsonAutoImports": "on",
    "typescript.updateImportsOnFileMove.enabled": "always",
    "bitoAI.appearance.fontSize (Match with IDE Font)": false,
    "bitoAI.codeCompletion.enableAutoCompletion": true,
    "bitoAI.codeCompletion.enableCommentToCode": true,
    "editor.inlineSuggest.showToolbar": "always",
    "security.workspace.trust.untrustedFiles": "open",
    "deepseek.lang": "cn",
    "marscode.codeCompletionPro": {
        "enableCodeCompletionPro": true
    },
    "remote.SSH.remotePlatform": {
        "sandbox.khmksz.csb": "linux"
    },
    "trae.codeCompletionPro": {
        "enableCodeCompletionPro": true
    },
    "continue.showInlineTip": false,
    "dart.flutterSdkPath": "E:\\flutter",
    "editor.unicodeHighlight.invisibleCharacters": false,
    "editor.unicodeHighlight.ambiguousCharacters": false,
    "scm.alwaysShowRepositories": true,
    "scm.alwaysShowActions": true,
    "claudeCodeChat.wsl.enabled": false,
    "claudeCodeChat.wsl.distro": "Ubuntu",
    "claudeCodeChat.wsl.nodePath": "/usr/bin/node",
    "claudeCodeChat.wsl.claudePath": "/usr/local/bin/claude",
    "update.mode": "manual",
    "chatgpt.cliExecutable": "",
    "claudeCode.preferredLocation": "panel",
    "claudeCodeChat.thinking.intensity": "ultrathink",
    "editor.minimap.enabled": false,
    "git.confirmSync": false,
    "explorer.confirmDelete": false,
    "explorer.confirmPasteNative": false,
    "editor.language.colorizedBracketPairs": [
        
    ],
}