搭建指南
适用于 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.json 的 scripts 中添加以下脚本,然后执行 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 --write在eslint --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/"
}
}
十、迁移到新项目的步骤
- 安装依赖(参考第三节,按项目类型选择)
- 复制配置文件到项目根目录:
.prettierrc.json、.prettierignore、eslint.config.js、.commitlintrc.cjs - 更新
package.json:添加prepare、lint-staged、lint、format脚本及lint-staged配置 - 初始化 Husky:
npm run prepare - 创建钩子文件:
.husky/pre-commit和.husky/commit-msg - 验证:
# 验证 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 的 ref、computed)?
使用了
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": [
],
}