工具 – Prettier、ESLint、Stylelint

前言

以前在 Webpack 学习笔记 有稍微介绍过它们。这篇是单独整理版。

 

参考

一文彻底读懂ESLint

你的ESLint真的需要Prettier吗?

搞懂eslint和prettier等的关系

 

简单介绍

Prettier 是一个 formatting 工具,目的是方便统一代码格式,比如使用 single quote 还是 double quote?

它支持许多语言,包括 JS、TS、CSS、Sass、HTML、JSON 等等。

ESLint 是 JS / TS 代码检查器。它用于保证代码质量,通过 2 个方式

  1. 统一格式 (formating),这部分的功能和 Prettier 是一样的。 只是 ESLint 比较 configable。Prettier 是出了名的“固执”,很多规范你只能跟随官方的格式,它不允许你做配置。

  2. code quality,这点是 Prettier 没有的功能。

比如 function declare 了一个 parameter,但 function 内却完全没有调用到。(这通常是因为忘记移除)

这些都会被检测出来。在提交代码前我们就可以进行修改,这样就保证了代码质量。

Stylelint 也是这类检查器,它用于 CSS / Sass

Prettier vs ESLint & Stylelint

Prettier 的 formatting 功能, ESLint 和 Stylelint 都有。但是通常我们是用 Prettier 做 formatting 然后用 ESLint 和 Stylelint 做 code quality control。

而且 Stylelint v15.0 后,它自己阉割了 formatting 的功能,官方也叫你用 Prettier。

所以下面 ESLint 和 Stylelint 都是搭配 Prettier 使用的。

 

Prettier

Command-line

首先全局安装 prettier package (Prettier 基于 Node.js)

npm install prettier --global

创建项目

yarn init

创建一个 index.ts

const value = 'value';

Prettier 支持很多种语言的格式,这里只是随便拿 TS 做例子。

运行 

prettier index.ts

效果

Prettier 不会告诉你,哪个地方格式错了,它只会输出一个格式化后的内容。

注意看, value 从原本的 single quote 变成了 double quote。(没错,Prettier 默认 TS 是用 double quote 的,幸好这个规则是可以配置的,下面会教)

prettier index.ts --write

加上 --write,它会直接修改 index.ts 的内容。

VS Code Prettier 插件

Command-line 的用法不方便,通常 Prettier 都是搭配 IDE 用的,在我们每一次保存文件时,自动跑一次 formatting。

首先安装插件

配置 VS Code

{
  "[typescript]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
  },
}

针对 typescript 文件, 开启保存时自动 format,同时配置 format 时采用 Prettier 插件。(你也可以放全局,不需要针对 TypeScript,因为 Prettier 支持非常多语言)

Config Prettier

Prettier 能配置的东西不多,但还是有几个常用的。

创建 .prettierrc.json5 (注意:它开头有一个点,如果要支持 JSON with Comments 的话,extension 是 .json5,不需要 comment 就 .json 就够了) 

.json5 VS Code 也需要配置

{
  "files.associations": {
    "*.json": "jsonc", // 所有 .json 都是 JSON with Comments
    ".prettierrc.json5": "jsonc" // .prettierrc.json5 是 JSON with Comments
  },
}

然后 .prettierrc.json5 的内容是

{
  "overrides": [
    {
      "files": "*.(ts|js)",
      "options": {
        "singleQuote": true, // Prettier 默认是 double quote,我改成 single quote
        "printWidth": 100, // Prettier 默认最长的代码是 80px width (超出就会换行),我改成 100。
        "arrowParens": "avoid", // (value) => value 去掉不必要的括弧 value => value
        "endOfLine": "auto" // 这个为了解决 LF,CRLF,\r, \r\n 的兼容问题。
      }
    }
  ]
}

针对 .ts 和 .js 配置。

Prettier plugin sort imports

prettier-plugin-sort-imports 插件可以排序 TypeScript 的 import,这样 import 就不会很乱了。

安装

yarn add prettier --dev
yarn add --dev @ianvs/prettier-plugin-sort-imports

提醒:

它有 2 个版本,@trivago 是正版,@ianvs 是 fork 的版本,主要的区别是 @trivago 的正版会排序 CSS import,

而我的项目 CSS 是有 override 概念的,不可以乱排序,所以我用了 @ianvs 的版本,这个版本就不会排序有 side-effect 的 imports。

然后添加配置到 .prettierrc.json5

{
  "overrides": [
    {
      "files": "*.(ts|js)",
      "options": {
        "singleQuote": true, // Prettier 默认是 double quote,我改成 single quote
        "printWidth": 100, // Prettier 默认最长的代码是 80px width (超出就会换行),我改成 100。
        "arrowParens": "avoid", // (value) => value 去掉不必要的括弧 value => value
        "endOfLine": "auto", // 这个为了解决 LF,CRLF,\r, \r\n 的兼容问题。

        // 排序 TypeScript 的 imports 相关配置
        "importOrder": [ // 用正则表达式来匹配路径,然后按 array 的顺序排
          "<THIRD_PARTY_MODULES>", // match 所有 node_modules 的 imports
          "src/module", // match 项目 module
          "Shared/Component/Stooges",
          "Shared/Component",
          "^[.]"
        ],
        "importOrderSortSpecifiers": true, // import { a, b } 内容要不要也排序
        "plugins": ["@ianvs/prettier-plugin-sort-imports"] 
      }
    }
  ]
}

当遇到 side-effiect import,排序就会停止了

所以 Best Practice 是把有 side-effect 的 import 和没有的分开 2 个 group,有 side-effect 的全部在最下面。

另外,文档中有一个把 css 放到最下面的配置

这个配置支队 import styles from 'xx.css' 有效,对 import 'xx.css' 是无效的,因为后者属于有 side-effect。

还有一个小区别,importOrderSeparation options 没有了。原因是 @ianvs 版本更灵活。

 

ESLint

Command-line

全局安装 eslint package

npm install eslint --global

创建项目

yarn init

添加 TypeScript

yarn add typescript --dev
tsc --init

添加一个 index.ts

const a = '';
const b = "";
const yes = null == undefined;

我故意同时用了 single quote 和 double quote,而且还用了不安全的 ==(JS best practice 是用 === 而不是 ==)

待会儿我们看看 ESLint 能检查出问题吗。

添加 eslint

yarn add eslint --dev

init eslint

yarn create @eslint/config

注:eslint v9.0 有个大改版,v9 之前 init eslint command 是 eslint --init,我们这里用的是 v9 之后的新版本。

这时它会问一些相关的问题。

尝试检查

// 指定检查 index.ts
yarn eslint index.ts

// 指定检查所有 .ts 文件
yarn eslint *.ts

效果

error 就表示代码不过关,需要我们修改。

我们可以使用 --fix 自动修复功能

yarn eslint index.ts --fix

不过它的能力有限,只能修复一些初级的代码问题,复杂的依然需要我们自己修改。

VS Code ESLint 插件

安装 VS Code ESLint 插件(微软推出的哦)

现在 index.ts 会出现各种 error 和 warning。

通过 Quick Fix 我们可以查看报错的原因,也可以通过注释 disable 掉这行代码的检测。

通过配置 VS Code 的 codeActionsOnSave 还可以让 VS Code 在保存时自动替我们运行 yarn eslint index.ts --fix。

"[typescript]": {
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
  }
}

Config ESLint & 结合 Prettier

上面我们使用的是 eslint recommended 的代码规范。

市场上还有其它的规范,比如 Google 的、Airbnb 的等等。当然我们也可以自己写规范,或者 override 它们的规范。(ESLint 可以 override 很多配置,不像 Prettier 就只有几个可以配置)

下面是我的规范 eslint.config.mjs

import pluginJs from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';

export default [
  { files: ['**/*.{js,mjs,cjs,ts}'] },
  { 
    languageOptions: { 
      globals: globals.browser,

      // 为了开启下面的 recommendedTypeChecked,这里必须提供 tsconfig.json
      // tsconfig.json 通常也需要添加 files: ['eslint.config.mjs']
      parserOptions: {
        project: 'tsconfig.json', 
      },
    } 
  },

  pluginJs.configs.recommended,

  ...tseslint.configs.strict, // 取代 tseslint.configs.recommended
  ...tseslint.configs.stylistic, // 取代 tseslint.configs.recommended
  ...tseslint.configs.recommendedTypeChecked, // 很多严格的检测
  
  {
    rules: {
      '@typescript-eslint/no-non-null-assertion': 'off', // 允许使用 TypeScript 的 Non-null assertion operator
      '@typescript-eslint/no-empty-function': 'warn', // 当函数没有内容时会警示
      // 当 unused 警惕时,可以用 underscore by pass
      '@typescript-eslint/no-unused-vars': [
        'warn',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
          destructuredArrayIgnorePattern: '^_',
        },
      ],
      '@typescript-eslint/no-unused-expressions': [
        'error',
        {
          allowTernary: true, // 允许 ternary expressions call function. e.g. true ? fn1() : fn2();
          allowShortCircuit: true, // 允许 true && fn();
        },
      ],

      '@typescript-eslint/unified-signatures': 'off', // 允许 overload function

      // .subscribe(async () => {}) <-- Error: no-misused-promises
      // RxJS 的 subscribe 内部没有特别处理 async callback 
      // 若我们传入 async callback,eslint 会认为不合理,不够安全
      // 但这样很不方便,所以我就 by pass 它了
      '@typescript-eslint/no-misused-promises': [
        'error',
        {
          checksVoidReturn: {
            arguments: false,
          },
        },
      ],

      // 允许在 constructor 里使用 static 方法, 比如:
      // constructor() {
      //   this.formControl = new FormControl<string>('', {
      //     validators: Validators.required,
      //   });
      // }
      // Validators.required 是 static,所以可以使用
      '@typescript-eslint/unbound-method': [
        'error',
        {
          ignoreStatic: true
        },
      ],
      
      // 允许 any assign:
      // private readonly hostElement: HTMLElement = inject(ElementRef).nativeElement;
      // 上面会 error,因为 nativeElement 是 any,eslint 不允许 any 拿来做 assignment
      // 要改成
      // private readonly hostElement = inject(ElementRef).nativeElement as HTMLElement;
      // 我个人觉得 any 就是用来 bypass 的,所以应该要可以 assign to whatever,比较方便,当然 eslint 严格也是正确的,因人而异吧
      '@typescript-eslint/no-unsafe-assignment': 'off', 

      'require-await': 'error', // async 函数内一定要有 await
      eqeqeq: ['error', 'always', { null: 'ignore' }], // 强制用 === 而不是 ==, 除了 == null 是允许的
      'no-constant-condition': ['error', { checkLoops: false }], // 不允许 if(true),但是 white(true) 可以
      'no-useless-rename': ['error'], // const { width: width } = dimension <-- 报错.对象解构时 rename 一定要真的换名字 width: width 等于没有丫
      'object-shorthand': ['error'], // const { width: width } <-- 报错, variable name same as object key name is not necessary
      'no-restricted-globals': [
        // 不允许直接用 global,必须加上 prefix window.setTimeout 这样。
        'error',
        'localStorage',
        'location',
        'alert',
        'setTimeout',
        'clearTimeout',
        'setInterval',
        'clearInterval',
        'requestAnimationFrame',
        'cancelAnimationFrame',
        'navigator',
      ],
    },
  },
];

另外,ESLint 还可以结合 Prettier 来使用。这个结合需要特别解释一下,不然会有点乱。

有 2 种结合方式:

  1. 分开用,但是配合执行

    我们项目先添加 Prettier (依据上面我教的方式),然后再添加 ESLint。

    需要特别注意 formatting 的规则一定要一致。比如说,Prettier format 时用 double quote,如果 ESLint 检测时要求 single quote,那肯定是报错的。

    确保双方规则一致后,运行 2 个 commands 就可以了。

    prettier index.ts --write
    yarn eslint index.ts --fix

    配合 VS Code 插件,它会在 save 的时候跑 eslint --fix 然后再跑 prettier --write。(注:它是先跑 eslint --fix 哦)

  2. 把 Prettier 内置到 ESLint 里

    这样的好处是我们只需要运行 1 个 command 就可以了。

    安装额外的插件

    yarn add prettier --dev
    yarn add eslint-config-prettier --dev
    yarn add eslint-plugin-prettier --dev

    然后配置

    接着再加上我们自定义的 prettier rules

当内置 Prettier 之后,其实我们就不需要原本的 Prettier 了。

可以把 on save prettier formatting 关掉。

"[typescript]": {
  "editor.formatOnSave": false, // off prettier formatting 
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit", // 它里面包含 prettier formatting 了
  }
}

有几个点要注意一下

  1. error message 没有那么清楚。

    本来 ESLint 的 formatting error 是很清楚的。

    改成 Prettier 的 formatting 后 error message 被弱化了。

    这是因为 ESLint 有检测的概念,它的 formatting 是先检测哪里有问题,并且给予 error 然后才 --fix。

    Prettier 它没有检测的功能,它是直接 format 输出 formatted 的内容。所有 ESLint 内部是拿 format 前和 format 后的内容做对比,然后返回一个简单的 erorr message。

  2. 现在运行 yarn eslint --fix,它内部会执行 prettier --write。

  3. ESLint 的 Prettier 需要通过在 eslint.config.mjs 里做配置。它可不理 .prettierrc.json 哦。

 

Stylelint

Stylelint 和 ESLint 大同小异,它是负责检测 CSS / Sass 的。

Command-line

首先全局安装 stylelint package

npm install stylelint --global

创建项目

yarn init

添加一个 index.scss

body {
  background-color: #ff0000;
  display: flex;
}

添加 stylelint

yarn add stylelint --dev

init stylelint

yarn create stylelint

它会创建一个 .stylelintrc.json 的配置文件。此时已经可以检测 CSS 了,但还不能检测 Sass。我们需要再添加一些配置。

yarn add stylelint-config-standard-scss --dev

它依赖 postcss。

安装 postcss

yarn add postcss@^8.4.19 --dev

然后换掉 extends

运行检测 

stylelint index.scss

效果

成功报错了。它同样支持用 --fix 自动修复。 

VSCode Stylelint 插件

安装 VS Code ESLint 插件

比起 ESLint 它需要配置比较多东西,

{
  "css.validate": false,
  "scss.validate": false,
  "stylelint.validate": ["css", "scss"],
  "[scss]": {
    "editor.codeActionsOnSave": {
      "source.fixAll.stylelint": "explicit"
    }
  }
}

先关闭 VS Code 原生对 CSS 和 Sass 的检测,改用 Stylelint

同时添加多一个 fixAll.stylelint 到 codeActionsOnSave

这时就可以看到 error 了

save and --fix 的效果

结合 prettier

yarn add prettier --dev
yarn add stylelint-prettier --dev

修改 .stylelintrc.json(注:它不支持 JSON with Comments)

{
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-prettier/recommended"
  ],
  "rules": {
    "declaration-empty-line-before": null,
    "prettier/prettier": [
      true,
      {
        "singleQuote": true,
        "printWidth": 100,
        "endOfLine": "auto"
      }
    ]
  }
}

关闭 VS Code setting [scss] formatOnSave

"[scss]": {
  "editor.formatOnSave": false, // off prettier formatting
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit" // 已经包含 prettier formatting 了
  }
}

效果

CSS Properties Ordering

有时候 CSS 属性的顺序很烦人,如果我们想要统一规范可以借助 stylelint-order 插件

yarn add stylelint-order --dev

修改 .stylelintrc.json

properties-order 我们可以放我们规范的顺序。

当然我们也可以跟随市场的规范,比如 Bootstrap 的顺序插件是 stylelint-config-recess-order

yarn add stylelint-config-recess-order

配置

添加 extends : stylelint-config-recess-order 就可以了。stylelint-order plugin 可以移除。

我的 .stylelintrc.json

{
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-prettier/recommended"
  ],
  "rules": {
    // by default class 只能是 kebab-case,要支持 BEM 就用下面这个正则
    "selector-class-pattern": [
      "^[a-z]+(-?[a-z0-9]+)*(__[a-z0-9]+(-?[a-z0-9]+)?)*(--[a-z0-9]+(-?[a-z0-9]+)?)*$",
      {
        "resolveNestedSelectors": true
      }
    ],
    "scss/dollar-variable-pattern": "^_?[a-zA-Z0-9\\-]+$", // kebab-case 或 _kebab-case
    // 因为 Prettier format 会导致这个 error. 相关 Issue:
    // https://github.com/prettier/prettier-eslint/issues/186
    // https://github.com/prettier/prettier/issues/3806
    "scss/operator-no-newline-after": null,
    // 空行可以帮助将代码分组,提高阅读体验 
    "declaration-empty-line-before": null,
    // 允许重复 selector,因为有时候我是按功能 group properties 的。
    "no-duplicate-selectors": null,
    // 我的规范,组件一定是 .html, .css, .ts 三剑客的,方便以后内部扩展功能,不用修改外部接口.
    "no-empty-source": null,
    "custom-property-pattern": "^_?[a-zA-Z0-9\\-]+$|^[a-zA-Z0-9\\-]+_[a-zA-Z0-9\\-]+$", // kebab-case 或 _kebab-case 或 kebab-case_kebab-case (underscore 前面可以放 namespace 做管理)
    // 它老是乱报错。干脆关了吧。它经常搞错的原因如下
    // https://stylelint.io/user-guide/rules/no-descending-specificity/#:~:text=However%2C%20since%20the%20linter%20does%20not%20have%20access%20to%20the%20DOM%20it%20can%20not%20evaluate%20this
    "no-descending-specificity": null,
    "prettier/prettier": [
      true,
      {
        "singleQuote": true,
        "printWidth": 100,
        "endOfLine": "auto"
      }
    ]
  }
}

 

Work with Web Bundlers (e.g. Webpack, Vite, Rollup)

上面例子中,我们都是以 Command-line 或 VS Code Plugin 的形式去使用 Pretter, ESLint, Stylelint。

还有一种方式是配合 Web Bundlers (e.g. Webpack),当 bundler 打包文件的时候,它会自动跑 eslint --fix 和 stylelint --fix。

对 Webpack 配置 ESLint 和 StyleLint 感兴趣的朋友可以看这篇 Webpack 学习笔记

如果 bundler 不支持 (比如 Angular 的 CLI 对 Stylelint 支持不友好),那就只能在打包之前自己跑 Command-line 了。

 

分享我的做法:

VS Code Plugin 是一定要用的,因为它可以在 on save 的时候就做 prettier --write,eslint --fix 和 stylelint --fix,立竿见影。

VS Code setting:

所有文件都用 Prettier做 formatting

"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",

有例外的话就特别声明

"[cshtml]": {
  "editor.formatOnSave": false,
},

Scss 就用 Stylelint 就好,因为 Stylelint 已经包含了 Prettier。

"[scss]": {
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit"
  }
},

TypeScript 就用 ESLint 就好,因为 ESLint 已经包含了 Prettier。

"[typescript]": {
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
  }
},

VS Code 只能在 on save 的时候做 formatting 和检测,这是不够的,我们依然需要一个可以对整个项目做检测的方案。

最好是使用 Command-line,在 git commit 之前可以做个检测:

  1. ESLint

    在 eslint.config.mjs 添加 ignores 属性,表示这些路径不要检测

    然后运行

    yarn eslint **/*.ts **/*.js --fix
  2. StyleLint

    创建一个 .stylelintignore 文件

    node_modules/**
    .yarn/**
    wwwroot/assets/**
    dist/**

    然后运行

    stylelint **/*.scss **/*.css --fix
  3. 或者在 package.json 

    "scripts": {
      "lint": "eslint **/*.ts --fix && stylelint **/*.scss --fix"
      // 如果上面这句报错 spawn ENAMETOOLONG,可以是试试下面这句
      // "lint": "eslint '**/*.ts' --fix && stylelint '**/*.scss' --fix"
    },

    运行

    yarn run lint

或者如果项目使用 Web Bundler 而且它支持 ESLint 和 StyleLint 也可以交由 Bundler 做。这样在发布或本地运行前可以做个检测,试试确保代码整齐干净。

 

使用 Yarn PnP

上面的例子都是用 Yarn Classic。但其实 Prettier、ESLint、Stylelint 都支持 Yarn PnP。只有 2 点要注意

  1. Command 要前面要加 yarn

    eslint index.ts
    // 改成
    yarn eslint index.ts
  2. Prettier 需要 node_modules folder,当我们运行 command

    yarn prettier index.ts

    它会自动创建 folder,但只是空 folder 哦。

 

总结

要控制 TS、JS、Sass、CSS 的代码质量就必须使用 Prettier、ESLint、Stylelint。

 

posted @ 2023-07-20 18:43  兴杰  阅读(1889)  评论(0编辑  收藏  举报