代码规范体系(EditorConfig、ESLint、Prettier、Husky)
代码规范体系(EditorConfig、ESLint、Prettier、Husky)
EditorConfig
属于编辑器层,在最底层统一编辑器行为,解决跨编辑器的格式差异。当你用 VS Code 打开一个文件,缩进是空格还是 Tab、宽度是2还是4,这些基础行为由 EditorConfig 说了算。EditorConfig 是一个跨编辑器的代码风格配置文件。它的设计理念是:无论你用 VS Code、IntelliJ IDEA 还是 Vim,打开同一个项目时,基础编辑风格都自动保持一致。这个文件被大多数主流编辑器(VS Code 需要安装插件)和 IDE 原生支持。
ESLint + TSLint
属于代码质量层,检测代码质量和类型规范,发现 bug 风格的写法。比如使用了 `==` 而不是 `===`、使用了被废弃的 API、变量未声明就使用、import 语句没有排序等。
Prettier
属于格式统一层,强制统一代码风格,消除团队内对格式的争议。缩进用空格还是 Tab、引号用单还是双、行尾要不要逗号——这些视觉层面的问题由 Prettier 说了算。
Husky + lint-staged + 自定义脚本
属于提交管控层,在代码进入仓库前执行全面检查,阻断不符合规范的提交,确保仓库代码质量始终如一。
项目配置的优先级对比
1.Git hooks(Husky)。这是最后一道防线,优先级最高。即使你的编辑器配置完美,git commit 时如果 ESLint 报 Error,整个提交仍然会被阻断。这一层的意义是:强制约束,不能绕过。
2.项目配置文件(.eslintrc.js、tslint.json、.prettierrc、tsconfig.json)。这些文件在特定工具运行时生效,覆盖编辑器的格式化行为。比如你的 VS Code 设置了 `tabSize: 4`,但 `.prettierrc` 配置了 `tabWidth: 2`,那么 Prettier 格式化时输出的仍然是 2 空格。触发方式可以选择格式化文档(选使用prettier)或者.vscode>setting.json中配置"editor.formatOnSave": true 保存代码自动格式化
3.EditorConfig + VS Code 用户设置。安装了 EditorConfig 插件后,项目的 `.editorconfig` 会覆盖 VS Code 的默认设置,但不会覆盖你手动在 VS Code settings.json 中配置的同名项。
4.VS Code 默认设置。当没有任何配置文件时,VS Code 使用自己的默认行为(通常 tab 宽度 4、空格缩进等)。
EditorConfig 与 VSCode
代码规范的生效,始于你打开一个文件的那一刻。在这一刻,决定代码"长什么样"的设置来自两个层面:VSCode 原生设置(settings.json)和 EditorConfig(.editorconfig)
VSCode 提供了两套配置体系:UI 界面配置和 JSON 文件配置。UI 配置就是通过 Cmd + , 打开的设置面板,点一点滑块和勾选框就能改。这种方式直观但无法批量共享给团队。JSON 配置则是通过"打开设置 JSON"入口直接编辑 settings.json 文件,好处是可以提交到 Git,团队成员 clone 后直接使用。
注意:VS Code 不会自动读取 .editorconfig 文件,须安装 EditorConfig插件才能生效。很多人因为没有安装插件,误以为项目的 .editorconfig 配置有问题。
那么,既然 VS Code settings.json 也能配置这些选项,为什么还要用 .editorconfig?
.editorconfig 与编辑器无关。如果团队中有人用 WebStorm、有人用 VS Code、有人用 Vim,通过 .editorconfig 就能统一管理,不需要每个人都去改自己的编辑器设置;
.editorconfig 支持目录层级的差异化配置。你可以在根目录设置全局规则,同时在子目录中用不同的 .editorconfig 覆盖父级规则,这种继承和覆盖机制是 settings.json 不支持的
VSCode配置与EditorConfig 的关系
VSCode 不会自动读取和应用 .editorconfig配置,须安装EditorConfig插件才会生效。
安装 EditorConfig 插件后,项目的 .editorconfig 会覆盖 VSCode默认设置和用户设置
团队成员不需要手动调整 VS Code 设置,只要 clone 项目、安装插件、规范就自动生效
.editorconfig 配置(取自实际项目中使用)
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
root = true 表示这是最顶层的配置,EditorConfig 不会继续向上级目录查找配置。所有文件(`[*]` 表示匹配所有文件类型)统一使用 2 空格缩进、LF 换行符、UTF-8 编码
settings.json 配置
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescriptreact",
"typescript"
],
"typescript.tsdk": "node_modules/typescript/lib"
}
配置说明
| settings.json | .editorconfig(INI 格式) | 解释 |
| editor.insertSpaces:true | indent_style | 控制按 Tab 键是插入空格,不是是制表符 |
| editor.tabSize | indent_size | 控制缩进的宽度 |
| editor.trimTrailingWhitespace:true | trim_trailing_whitespace | 保存文件会自动删除每行末尾的多余空格 |
editor.insertFinalNewline: true | insert_final_newline | 保存文件自动在文件末尾确保有一个空行( POSIX 标准) files.eol // 控制换行符样式。`\n` 表示 LF(Unix/macOS 标准),`\r\n` 表示 CRLF(Windows 标准)。团队必须统一,否则同一个文件在不同人的机器上换行符不同,git diff 会变成全文件变更。 项目统一要求 LF |
| files.encoding | charset | 设置默认文件编码为 utf8,保持跨平台一致性。 |
ESLint(Parser、Plugin、Config、extends)
ESLint 是一个代码质量检测工具,它会在你写代码时或提交代码前,检查代码是否符合预设的规则。ESLint 的检查范围很广,既包括代码格式问题(比如缩进、引号),也包括代码质量问题(比如使用了 `==` 而不是 `===`、使用了被废弃的 API、变量未声明就使用等)。
ESLint 的工作方式可以类比为老师批改作业。老师手里有一份"评分标准"(规则集),学生交作业(代码)后,老师对照标准逐条检查,发现问题就打叉并标注原因。ESLint 就是这个"老师",规则集就是 ESLint 的配置文件。
ESLint 在项目中运行时机:
开发时(实时检测),在使用 VS Code 编写代码时,编辑器实时调用 ESLint 对当前文件进行检测,发现问题会在代码中直接标出红色或黄色下划线,让你编码的同时就知道哪里有问题;
提交前(git pre-commit hook),当执行 git commit时,Husky 触发 pre-commit hook,lint-staged 只对暂存的代码文件运行 ESLint 检测。如果检测到 Error 级别的问题,提交被阻断
ESLint 由四个关键组件构成,理解它们是掌握 ESLint 的基础:
Parser(解析器):ESLint 本身只能处理标准的 JavaScript 代码,但现代前端项目通常使用 TypeScript、Vue 的 SFC 等非标准语法。Parser 的作用是把这些非标准代码翻译成 ESLint 能理解的"抽象语法树"(AST)。常见的Parser 是 `@typescript-eslint/parser`,它专门负责把 TypeScript 代码解析成 ESLint 能处理的格式。
Plugin(插件):ESLint 本身只包含最基础的规则。Plugin 是规则的扩展包,每个插件都包含一组针对特定场景的规则。比如项目使用了两个插件:`@typescript-eslint` 插件提供了 TypeScript 相关的规则,`@mtfe/video-base` 插件则是团队自研的规则包,包含该团队特有的规范要求。
Config(配置/规则):插件提供的是规则"素材库",配置从这个素材库中选择启用哪些规则、每个规则的严格程度如何。ESLint规则有三种级别:off(不检查)、warn(标出但不阻断)、error(标出且阻断提交)。
extends(继承):在团队项目中,通常会有一份公司级或团队级的 ESLint 配置作为基础,项目在此之上进行个性化调整。本项目通过 extends 继承了三个配置:`plugin:@mrn/eslint-plugin/recommended` 来自 MRN 平台团队,`plugin:@mtfe/mtvideo-base/recommended` 来自美团短视频团队,`prettier` 来自 eslint-config-prettier 用来关闭与 Prettier 冲突的规则。
.eslintrc.js(ESLint配置文件)
{
root: true, // 表示这是项目的根配置文件,ESLint 不会向上级目录寻找
parser: '@typescript-eslint/parser', // parser指定使用什么工具来解析代码,如果项目用TypeScript,必须用它对应的解析器
plugins: ['@typescript-eslint', '@mtfe/mtvideo-base'], // plugins声明要加载哪些规则插件
extends: [ // extends:继承已有的配置(按顺序加载,后面的会覆盖前面的)
'plugin:@mrn/eslint-plugin/recommended',
'plugin:@mtfe/mtvideo-base/recommended',
'prettier', // prettier 配置,用于关闭与 Prettier 冲突的 ESLint 规则
],
// rules:在继承的基础上,自定义调整规则
rules: {
'no-restricted-properties': [ //这是一个ERROR 规则,会阻断提交(禁止使用JSON.parse)
'error',
{
object: 'JSON',
property: 'parse',
message: '请使用 @mrn/mtvideo-base-utils的jsonParseSafe代替JSON.parse。直接使用 JSON.parse可能抛错导致崩溃或异常行为',
},
],
'no-eq-null': 'off', // 关闭 no-eq-null 检查,因为 TypeScript 的严格模式已经覆盖了这个场景
'react/prop-types': 'off', // React 的 prop-types 在 TypeScript 下不需要
'@typescript-eslint/member-ordering': 'off', // 关闭成员排序(因为业务代码的成员顺序可能需要按功能组织)
'simple-import-sort/imports': 'error', // import 语句按字母顺序排序
'@typescript-eslint/prefer-readonly': 'error', //prefer-readonly:类成员能用readonly就用readonly
// 以下规则从 error 降级为 warn(代码中有遗留场景,短期内无法全部修复)
'react/no-access-state-in-setstate': 'warn',
'no-nested-ternary': 'warn',
'react/jsx-key': 'warn',
'no-unsafe-finally': 'warn',
'@typescript-eslint/consistent-type-assertions': [
'warn',
{
// 使用 as 断言,不用 <Type> 断言语法
assertionStyle: 'as',
// 禁止对象字面量的类型断言
objectLiteralTypeAssertions: 'never',
},
],
'no-shadow': 'off', //关闭 shadow规则(与 TS 的类型系统可能有冲突)
},
overrides: [// overrides:对特定文件做不同的配置(优先级高于外层 rules)
// *.d.ts 类型声明文件,关闭未使用变量检查
{
files: ['*.d.ts'],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
// *.js 文件不需要 TypeScript 类型检查
{
files: ['*.js'],
parserOptions: {
project: null,
},
extends: ['plugin:@mtfe/mtvideo-base/disableTs'],
},
],
// ignorePatterns:告诉 ESLint 忽略这些文件和目录
ignorePatterns: [
'node_modules/**/*',
'dist/**/*',
'**/Models/**/*', // 自动生成的代码
'cli/**/*', // 命令行脚本
'plugin/**/*',
'src/**/*', // src 目录由 TSLint 负责
],
globals: { // globals:在代码中可以使用的全局变量(不会报未定义错误)
__filename: 'readonly',
__BUILD_TIME__: 'readonly',
__CODE_LINE__: 'readonly',
__dirname: 'readonly',
__jsiExecutorDescription: 'readonly',
},
}
Prettier
Prettier 是一个代码格式化工具(Formatter),它的职责与 ESLint 有部分重叠但定位不同。ESLint 更偏向"代码质量",关注的是代码是否正确、是否安全、是否有潜在bug。Prettier 更偏向"代码风格",关注的是缩进是 tab 还是空格、字符串用单引号还是双引号、行尾是否加逗号等视觉层面的统一。
Prettier 的设计哲学是"做一个固执己见的代码格式化工具"。它只提供少量配置项,强制团队接受统一的格式化风格,不留讨论余地。你不需要纠结"我们团队应该用单引号还是双引号",Prettier 说用单引号就都用单引号,不需要 code review 时因为格式问题争论不休。
为什么ESLint和Prettier需要配合使用
ESLint 本身也有一部分格式化规则(比如 indent、quotes、semi 等),这些规则和 Prettier 的功能是重叠的。如果两个工具的规则不一致(比如 ESLint 要求加分号,Prettier 要求不加分号),那就会产生冲突——ESLint 检测到不加分号会报错,Prettier 格式化后恰好不加分号,然后 ESLint 又报错,形成死循环。
解决方案是使用 `eslint-config-prettier` 这个包,它的作用是关闭ESLint中所有与 Prettier 冲突的格式化规则,让ESLint专注于代码质量检查,Prettier 专注于代码格式化。各司其职,互不干扰。
Prettier配置(.prettierrc)
{
"semi": true,
"singleQuote": true,
"arrowParens": "always",
"trailingComma": "all",
"preferConst": true,
"tabWidth": 2
}
trailingComma: "all"` 表示在 ES5 合法的地方都加尾逗号。这个配置在现代前端项目中非常常见,因为它可以让 git diff 更加清晰——如果一行只添加了一个字段但原行没有逗号,git diff 只会显示新增的那一行,而如果原行末尾没有逗号,则会显示"修改了原行末尾并新增了一行",导致 diff 变得冗长。
arrowParens: "always" 表示箭头函数的参数无论有多少个都加括号。比如 `(x) => x + 1` 而不是 `x => x + 1`。这样做的好处是代码风格统一,减少视觉干扰。
preferConst的实际执行由 ESLint 的prefer-const规则负责,而非 Prettier。
Husky 与 Git Hooks
在理解 Husky 之前,需要先理解 Git Hooks 是什么。Git 允许你在特定的时机自动执行自定义脚本,这些时机包括"提交之前"、"提交之后"、"推送之前"、"推送之后"等。Git Hooks 就是这些"时机点"上挂载的自定义脚本。
Git 自带了一些默认的 Hooks 脚本模板,位于 `.git/hooks/` 目录下,比如 `pre-commit.sample`、`commit-msg.sample`、`post-commit.sample` 等。这些文件默认是禁用的(因为带 `.sample` 后缀),只需要把 `.sample` 后缀去掉,Git 就会在对应时机执行这些脚本。
Husky 的作用是通过 package.json 配置 Git Hooks,而不需要手动编辑 `.git/hooks/` 目录下的脚本文件。
在没有 Husky 的时代,如果你想实现"每次 git commit 前运行 ESLint",你需要手动编辑 `.git/hooks/pre-commit` 文件,写入 shell 脚本。这有几个问题:这些脚本文件通常不会被 git 跟踪,团队成员每次 clone 项目后都需要手动配置;Windows 系统的 shell 脚本语法不同,兼容性差;不同项目可能有不同的 Hooks 配置,不方便管理。
Husky 通过在package.json中声明式地配置 Git Hooks 解决了这些问题。当运行 npx husky install 时,Husky 会自动在 `.git/hooks/` 目录下生成对应的 hook 脚本,并将团队的配置存储在 `.husky/` 目录下(可以被 git 跟踪)。团队其他成员 clone 项目后运行 `yarn`(依赖 postinstall 脚本)会自动安装 Husky Hooks。
package.jsonz中配置husky
"husky": {
"hooks": {
"pre-commit": "yarn lint-staged && node ./cli/check.js",
"post-merge": "yarn"
}
}
当开发者执行 `git commit` 时,Git 检测到 pre-commit hook 存在并执行它。Husky 生成的 pre-commit 脚本运行 `yarn lint-staged && node ./cli/check.js`。lint-staged 找出本次 commit 涉及的源代码文件并对每个文件执行 ESLint 检测和自动修复。如果有任何 Error 级别的问题,lint-staged 以非零状态码退出,git commit 被阻断。如果所有检查通过,git commit 继续执行。
post-merge: yarn 表示当执行 `git pull` 或 `git merge` 合并别人的代码后,Git 自动运行 `yarn` 来安装可能发生变化的依赖。这个 hook 非常重要,因为如果其他人修改了 `package.json`,你需要立即安装新的依赖,否则项目可能因为依赖不一致而无法运行。
lint-staged的作用就是只对本次 commit 涉及的(即 staged 的)文件运行 ESLint,而不是全量检查整个项目。这大大提高了检查速度,也避免了"别人的代码有问题但你被迫无法提交"的情况。
package.json中配置lint-staged
"lint-staged": {
"Reward/**/*.{tsx,ts,jsx,js}": "eslint --quiet",
"src/Message/Publish/**/*.{tsx,ts}": "eslint --quiet",
"src/Components/VideoUgcCreation/**/*.{tsx,ts}": "eslint --quiet",
"src/TagAggregation/**/*.{tsx,ts}": "eslint --quiet",
"src/Message/NewFocus/**/*.{tsx,ts}": "eslint --quiet",
"src/**/*.{tsx,ts}": "eslint --quiet",
"Models/**/*.{js,ts,tsx}": "node cli/check_model.js"
}
配置格式是"glob 模式: 命令"。每行表示匹配该模式的 staged 文件将执行对应的命令。例如,`src/**/*.tsx` 表示 `src` 目录下所有 `.tsx` 文件执行 `eslint --quiet` 检测。`--quiet` 参数表示只显示 Error 级别的问题,不显示 Warning,这样可以减少噪音同时确保严重的格式问题不被放过。
ESLint和TSLint
部分项目中同时存在 ESLint(`.eslintrc.js`)和 TSLint(`tslint.json`)两套工具。这是因为 TSLint 是 TypeScript 官方曾经推荐的检测工具,后来 TSLint 团队宣布停止维护并推荐用户迁移到 ESLint 的 `@typescript-eslint` 插件。美团短视频团队在迁移过程中采用了"双轨制"过渡策略:TSLint 继续承担对 src 目录的代码检测,ESLint 逐步接管更多场景。从 gamevideo 项目的配置中可以看到,ESLint 的 `ignorePatterns` 里排除了 `src/**/*`,表示 src 目录暂时由 TSLint 负责。
TSLint(tslint.json)配置
{
"extends": ["@hfe/mrn-tslint"],
"linterOptions": {
"exclude": [
"node_modules/**",
"Models/**",
"TSApis/**",
"src/Message/Publish/**",
"src/Components/VideoUgcCreation/**",
"src/TagAggregation/**",
"src/Message/NewFocus/**",
"**/*.bak"
]
},
"rules": {
"semicolon": [true, "always", "ignore-bound-class-methods"],
"quotemark": [true, "single", "jsx-double"],
"no-var-keyword": true,
"prefer-const": true,
"render-with-short-circuit-calculations": false,
"no-channel-in-params": false,
"disallow-null-header": false,
"test-style": false,
"render-function-no-setstate": false,
"endup-with-separator": false,
"no-arrowfunction-in-renderfunction": false,
"avoid-onpress-in-view": false,
"ter-indent": false
}
}
自定义检查脚本 cli/check.js
cli/check.js 不是 ESLint 本身,而是一个Node.js 脚本,串联了多个专项检查工具。当执行 `git commit` 时,这个脚本串行执行以下检查:
tsconfig.json(TypeScript 类型检查配置)
{
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"declaration": true,
"target": "esnext",
"module": "esnext",
"sourceMap": true,
"experimentalDecorators": true,
"jsx": "react-native",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["src/assets/*"],
"@src/*": ["src/*"],
"@reward/*": ["Reward/*"]
}
},
"include": [
"types/**/*",
"src/**/*.ts",
"src/**/*.tsx",
"Models/**/*.tsx",
"Reward/**/*.ts",
"Reward/**/*.tsx",
"lib/**/*.ts",
"lib/**/*.tsx",
"declarations.d.ts"
]
}
"strict": true 开启了 TypeScript 的所有严格类型检查,相当于同时设置了 `strictNullChecks`、`strictFunctionTypes`、`noImplicitAny` 等多个检查开关,会大幅提升类型安全性。"skipLibCheck": true 跳过对 `node_modules` 中 `.d.ts` 类型声明文件的检查,可以显著加快类型检查速度。
"noEmit": true 表示只做类型检查,不输出任何文件,因为构建产物由专门的构建工具(metro bundler)处理。
"paths" 配置了路径别名,`@assets/*` 会被解析为 `src/assets/*`,这样就可以用 `import img from '@assets/images/logo.png'` 代替相对路径。
优先级冲突的常见场景:
最常见的冲突场景是 VS Code 格式化与 Prettier 格式化结果不一致。原因通常是 VS Code 的 formatter 设置的不是 Prettier,或者没有安装 Prettier 插件。解决方案是确保 VS Code 配置了 `"editor.defaultFormatter": "esbenp.prettier-vscode"` 并安装 Prettier 插件。
ESLint 检测报错但编辑器没有标红。这通常是因为没有安装 ESLint VS Code 插件,或者 ESLint 插件没有正常工作。可以尝试在 VS Code 中按 `Cmd+Shift+P`,输入 "ESLint: Restart ESLint Server",然后回车重启 ESLint 服务。
第三个常见场景是 git commit 被阻断但本地检测没有报错。可能是因为 lint-staged 只检测 staged 的文件,而完整的 check.js 检测的是整个项目。可以手动运行 `node ./cli/check.js` 查看具体是什么检查失败了。
常用命令参考
yarn lint // 运行 ESLint 检测
yarn fix // 自动修复可修复的 ESLint 问题
yarn cacheClean // 清理缓存
