Skip to content

ESlint - 统一代码风格

单双引号、缩进、逗号等编码风格的统一十分有必要,下面使用 ESLint + Prettier + Husky 与 Lint Staged 配置 Nuxt 实现风格统一与代码优化,让代码更简洁,比如不允许未使用的变量,也可以指让代码更严谨,比如不允许未声明的全局变量,进一步提升工程的约束能力。

TIP

项目中需要预先安装typescript依赖,且已经初始化 Git

配置基本的 ESLint

ESLint 初始化

执行下列命令,通过 ESLint 自带的初始化功能:

npx eslint --init

# 或
npm init @eslint/config

根据自己的需求回答一系列问题即可,如:

✔ How would you like to use ESLint? · problems
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser, node
✔ How would you like to define a style for your project? · prompt
✔ What format do you want your config file to be in? · JavaScript
✔ What style of indentation do you use? · tab
✔ What quotes do you use for strings? · single
✔ What line endings do you use? · windows
✔ Do you require semicolons? · No 
✔ Would you like to install them now? · Yes
✔ Which package manager do you want to use? · yarn

如果选择了使用 TypeScript,它会自动为你安装@typescript-eslint一系列工具。按上述回答问题后最终安装了这些依赖:

"devDependencies": {
  "@typescript-eslint/eslint-plugin": "^5.36.2",
  "@typescript-eslint/parser": "^5.36.2",
  "eslint": "^8.23.0",
  "eslint-plugin-vue": "^9.4.0"
}

配置.eslintrc.js

同时在项目根目录会自动生成.eslintrc.js文件,打开并修改文件,结果如下:

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:@typescript-eslint/recommended',
  ],
  overrides: [],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint'],
  rules: {
    'vue/script-setup-uses-vars': 'error', // 防止 <script setup> 使用的变量 <template> 被标记为未使用
    'vue/custom-event-name-casing': 'off', // 为自定义事件名称强制使用特定大小写
    'vue/attributes-order': 'off',
    'vue/one-component-per-file': 'off',
    'vue/html-closing-bracket-newline': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'vue/attribute-hyphenation': 'off',
    'vue/require-default-prop': 'off',
    'vue/html-self-closing': [
      'error',
      {
        html: {
          void: 'always',
          normal: 'never',
          component: 'always',
        },
        svg: 'always',
        math: 'always',
      },
    ],
    'vue/no-v-html': 'off',
  },
}

查看更多 ESlint-plugin-vue 规则

配置.eslintignore

创建.eslintignore文件,根据习惯忽悠部分文件。这里只让 ESLint 检查核心代码文件,包括 jsjsxtstsx文件:

*.json
*.html
*rc.js
*.svg
*.woff
*.ttf
*.css

.nuxt
.husky

.eslintrc.js
.prettierrc.js
package.json
tsconfig.ts
README.md
node_modules
*.sh
*.md
.vscode
.idea
dist
build
/public
/docs
.husky
.local
/bin
Dockerfile

package.json中的scripts中添加以下命令:

{
  "scripts": {
    "eslint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --cache",
    "eslint:fix": "npm run eslint -- --fix"
  },
}

此时,执行npm run eslint即是仅检查,而eslint:fix则是检查同时尽可能修复错误。

配置 Prettier

安装

yarn add prettier eslint-config-prettier --save-dev

配置.prettierrc.js

创建 Prettier 配置文件.prettierrc.js,常用配置项:

module.exports = {
  // 单行最多 80 字符
  printWidth: 80,
  // 一个 Tab 缩进 2 个空格
  tabWidth: 2,
  // 每一行结尾不需要有分号
  semi: false,
  // 使用单引号
  singleQuote: true,
  // 在对象属性中,仅在必要时才使用引号,如 "prop-foo"
  quoteProps: "as-needed",
  // 在 jsx 中使用双引号
  jsxSingleQuote: false,
  // 使用 es5 风格的尾缀逗号,即数组和对象的最后一项成员后也需要逗号
  trailingComma: "es5",
  // 大括号内首尾需要空格
  bracketSpacing: true,
  // HTML 标签(以及 JSX,Vue 模板等)的反尖括号 > 需要换行
  bracketSameLine: false,
  // 箭头函数仅有一个参数时也需要括号,如 (arg) => {}
  // 使用 crlf 作为换行符
  endOfLine: "crlf",
};

阅读关于更多Prettier 配置

为了避免和 ESLint 冲突,我们还需要通过eslint-config-prettier禁用掉部分 ESLint 规则,修改 ESLint 配置:

// .eslintrc.js
module.exports = {
  extends: [
    'prettier', // 新增这一行
  ],
}

配置忽略文件.prettierignore:

node_modules
build
dist
output
.nuxt
.husky

.eslintrc.js
.prettierrc.js
tsconfig.json
package.json
tsconfig.ts
README.md


.local
**/*.svg
**/*.sh

/public/*

# 如果不希望 prettier 检查代码文件的话
*.ts
*.tsx
*.jsx
*.js

package.json中的scripts中新增如下命令:

{
  "scripts": {
    "prettier": "prettier --check .",
    "prettier:fix": "prettier --write .",
    "lint": "npm run eslint && npm run prettier",
    "lint:fix": "npm run eslint:fix && npm run prettier:fix"
  },
}

如上,npm run prettier是仅检查,而prettier:fix才是进入修改。同时增加了lintlint:fix来一次性执行两个工具。

Vue ESlint 配置

安装:

yarn add -D vue-eslint-parser @vue/eslint-config-typescript

修改.eslintrc.js:

{
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
}

Nuxt ESLint配置

安装:

yarn add  -D @nuxtjs/eslint-config @nuxtjs/eslint-config-typescript

修改.eslintrc.js:

extends: [
  '@nuxtjs/eslint-config-typescript'
]

TIP

安装了@nuxtjs/eslint-config-typescript后,该插件规则要求函数名称和调用它的左括号之间有空格,为防止和 ESlint 冲突,最好关闭func-call-spacing规则,即: 'func-call-spacing': 'off'

配置 Husky 与 Lint Staged

配置 Husky 与 Lint Staged 让每次提交代码时都自动执行一次格式化,就能确保所有人提交上去的代码风格一致。

Husky

安装:

npx husky-init && yarn

添加 Git Hooks:

npx husky add .husky/pre-commit './node_modules/.bin/lint-staged'

此时根目录下自动生成.husky目录,其内有一个pre-commit文件,文件内容为:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm test
./node_modules/.bin/lint-staged

TIP

根据自己喜好修改npm test,这里修改为yarn lint:fix

从上述文件中可以看出,实际上要执行的就是lint-staged这个命令,而 Lint Staged 的作用即是找出添加到暂存区(git add)的文件,然后执行对应的lint,下面将它添加到项目里:

lint-staged

安装:

yarn add -D lint-staged

package.json中增加如下配置,实现对暂存区的核心代码文件,先使用 ESLint 格式化,再使用 Prettier 格式化,而对于其他文件,统一使用 Prettier 进行格式化。

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --cache --fix",
      "prettier --write --list-different"
    ],
    "*.{json,md,html,css,scss,sass,less,styl}": [
      "prettier --write --list-different"
    ]
  }
}

至此,当git commit -m 'first commit'时就会对代码进行统一格式化。

.eslintrc.js配置示例

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    '@nuxtjs/eslint-config-typescript',
  ],
  overrides: [],
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint'],
  rules: {
    'vue/script-setup-uses-vars': 'error', // 防止 <script setup> 使用的变量 <template> 被标记为未使用
    'vue/custom-event-name-casing': 'off', // 为自定义事件名称强制使用特定大小写
    'vue/attributes-order': 'off',
    'vue/one-component-per-file': 'off',
    'vue/html-closing-bracket-newline': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'vue/attribute-hyphenation': 'off',
    'vue/require-default-prop': 'off',
    'vue/html-self-closing': [
      'error',
      {
        html: {
          void: 'always',
          normal: 'never',
          component: 'always',
        },
        svg: 'always',
        math: 'always',
      },
    ],
    'vue/no-v-html': 'off',
    'vue/multi-word-component-names':'off', // 关闭驼峰命名规则
    
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-var-requires': 'off',
    '@typescript-eslint/no-empty-function': 'off',
    'no-use-before-define': 'off',
    '@typescript-eslint/no-use-before-define': 'off',
    '@typescript-eslint/ban-ts-comment': 'off',
    '@typescript-eslint/ban-types': 'off',
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      },
    ],
    'no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      },
    ],
    'space-before-function-paren': 'off',
    'func-call-spacing': 'off', // 关闭函数名称和调用它的左括号之间的空格
  },
}

ESlint 常用规则

根据自己的需求调整

rules: {
    // 运算符两侧需要有空格,并增加对枚举类型支持
    'space-infix-ops': 'off',
    '@typescript-eslint/space-infix-ops': ['error', { int32Hint: false }],

    // 关键字前后有一个空格,并增加了对函数调用的泛型类型参数的支持。
    'keyword-spacing': 'off',
    '@typescript-eslint/keyword-spacing': 'error',

    // 指定类型时应该正确添加空格
    '@typescript-eslint/type-annotation-spacing': 'error',

    indent: 'off',
    '@typescript-eslint/indent': ['error', 2],

    // 调用函数时,函数名与括号之间没有空格,并增加了对函数调用的泛型类型参数的支持
    'func-call-spacing': 'off',
    '@typescript-eslint/func-call-spacing': 'error',

    // 逗号前面没空格,后面有空格
    'comma-spacing': 'off',
    '@typescript-eslint/comma-spacing': 'error',

    // 函数声明时,对于命名函数,参数的小括号前无空格;对于匿名函数和 async 箭头函数,参数的小括号前有空格
    // 增加了对函数调用的泛型类型参数的支持
    'space-before-function-paren': 'off',
    '@typescript-eslint/space-before-function-paren': [
      'error',
      {
        named: 'never',
        anonymous: 'always',
        asyncArrow: 'always',
      },
    ],

    // interface 和 type 里的成员统一使用分号(;)进行分割,单行类型的最后一个元素不加分号
    '@typescript-eslint/member-delimiter-style': 'error',

    // 强制使用分号
    semi: 'off',
    '@typescript-eslint/semi': 'error',

    // 字符串字面量使用单引号包裹
    quotes: 'off',
    '@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true }],

    //  用逗号分割多行结构,始终加上最有一个逗号(单行不用)
    'comma-dangle': 'off',
    '@typescript-eslint/comma-dangle': ['error', 'always-multiline'],

    // 对于非空代码块,采用 Egyptian Brackets 风格
    // 增加对 enum、interface、namespace、module 的支持
    'brace-style': 'off',
    '@typescript-eslint/brace-style': [
      'error',
      '1tbs',
      {
        allowSingleLine: true,
      },
    ],

    // 不要使用 new Array() 和 Array() 创建数组,除非为了构造某一长度的空数组
    'no-array-constructor': 'off',
    '@typescript-eslint/no-array-constructor': ['error'],

    // 禁止定义没有使用的变量
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': [
      'warn',
      {
        vars: 'all',
        args: 'after-used',
        ignoreRestSiblings: true,
      },
    ],

    // 禁止部分值被作为类型标注,需要对每一种被禁用的类型提供特定的说明
    // 1. 不使用大写的原始类型,应该使用小写的类型
    // 2. 对于对象类型,应使用 Record<string, unknown>,而不是 object
    // 3. 对于函数类型,应使用入参和返回值被标注的具体类型
    '@typescript-eslint/ban-types': 'warn',

    // 不允许不必要的类型标注,但允许类的属性成员进行额外标注
    '@typescript-eslint/no-inferrable-types': 'error',

    // 不允许与默认约束一致的泛型约束
    // 在 TS 3.9 版本以后,对于未指定的泛型约束,默认使用 unknown ,在这之前则是 any
    '@typescript-eslint/no-unnecessary-type-constraint': 'error',

    // 不允许非空断言与空值合并同时使用
    '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'warn',

    // 不允许非空断言与可选链同时使用
    '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',

    // 如果索引仅用于访问正在迭代的数组,则首选 for...of 而不是 for 循环遍历数组
    '@typescript-eslint/prefer-for-of': 'warn',

    //  重载的函数写在一起
    '@typescript-eslint/adjacent-overload-signatures': 'warn',

    // 具有默认值的函数参数应该被放置到参数列表右边
    '@typescript-eslint/default-param-last': 'warn',

    // 对于枚举成员值,只允许使用普通字符串、数字、null、正则,而不允许变量复制、模板字符串等需要计算的操作
    '@typescript-eslint/prefer-literal-enum-member': 'warn',

    // 不允许对同一模块重复导入,类型可重复导入
    'no-duplicate-imports': 'off',
    '@typescript-eslint/no-duplicate-imports': 'warn',

    // 禁止使用 module 来定义命名空间
    '@typescript-eslint/prefer-namespace-keyword': 'error',

    // 接口中的方法使用属性的方式定义。使用属性去定义接口中的方法,可以获得更严格的检查
    '@typescript-eslint/method-signature-style': 'error',

    // 不允许定义空的接口,允许单继承下的空接口
    '@typescript-eslint/no-empty-interface': 'warn',

    // 禁止使用容易混淆的非空断言
    '@typescript-eslint/no-confusing-non-null-assertion': 'error',

    // 不允许额外的非空断言
    '@typescript-eslint/no-extra-non-null-assertion': 'error',

    // 使用 as 进行类型断言而不是 <>。在 .tsx 文件中写组件时会存在冲突
    '@typescript-eslint/consistent-type-assertions': 'warn',

    // 禁止使用 tslint:<rule-flag> 等相关注释,tslint 已经不再维护了
    '@typescript-eslint/ban-tslint-comment': 'error',

    // 禁止使用其他 @ts 规则,除非提供必要的说明。
    '@typescript-eslint/ban-ts-comment': [
      'warn',
      {
        'ts-expect-error': 'allow-with-description',
        'ts-ignore': 'allow-with-description',
        'ts-nocheck': 'allow-with-description',
      },
    ],
  },