前端 code lint 和代码风格指南

avatar
FE @字节跳动

前言

lint 工具用来检查编程错误,最初是从 C 语言中发展起来的。在 C 语言最初时期,编译器无法捕获一些常见的编程错误,因此开发出了一个叫做 lint 的辅助程序,通过扫描源文件来查找问题。

当我们在 linting 的时候我们到底在干什么?实际上,最终目标是希望代码更加健壮,并且不论团队有多少成员,代码就像同一个人写出来的一样,可读性更强。

可以将众多 linters 的检查目标大致分为三类:

  • programmer errors :主要是对语法的检查,这类错误会影响程序执行的正确性。
  • best practices :其目的主要是为了避免出现让人困惑的代码,即使检查出问题也不一定意味着程序会执行出错,也有可能是正确的,但依然会令人困惑。这一步是避免潜在的错误,以及让代码更加清晰明确。
  • style issues :主要是代码风格方面的检查,例如空格、标点符号、代码外观等等。

前端 linters 分类

JavaScript

下图展示了 JavaScript linters 的进化史:

JSLint

2002 年由 Douglas Crockford 创建,用来进行 JavaScript 语法检查和校验。JSLint 定义了一个比 ECMAScript 编程语言标准更为严格的子集,是一种更高的标准。JSLint 完全是用 JavaScript 编写的。

JSLint 接收 JavaScript 源代码并对其进行扫描。如果发现问题,它将返回一条消息来描述问题以及源代码中的大概位置。这些问题多数时候是语法错误,但不全是语法错误,也可能是代码风格和结构的问题。它不能证明程序是正确的,只是提供了一个方式来帮助发现问题。JSLint 更加关心代码质量,因此即使浏览器可以正常运行的代码,JSLint 也可能不会通过。使用 JSLint 就意味着要欣然接受它所有的建议。

JSLint 可以对 JavaScript 源代码或 JSON 文本进行操作。

JSLint 将会认可 ES6 的一部分优秀的特性,例如 letconst 等等。

评价

优点
  • 使用简单,开箱即用,无需再次配置。
缺点
  • 有限的配置选项,很多规则不能禁用。
  • 规范严格,凡是不认可的风格都会报一个 warning。
  • 扩展性差。
  • 无法根据错误定位到对应的规则。

JSHint

2010 年基于 JSLint 诞生了 JSHint ,它主要解决了 JSLint 过于专断的问题,提供了一些配置以及添加一些 rules 。相较之下更友好,也更容易配置,所以很快就发展了起来,也得到了众多 IDE 和编辑器的支持。

JSHint 扫描用 JavaScript 编写的程序,并报告常见的错误和潜在的错误。 潜在的问题可能是语法错误、由于隐式类型转换导致的错误、变量泄漏等。可以通过指定任意数量的 linting 选项或在源代码中声明指令来控制 JSHint 的行为。

JSHint 附带了一组默认的警告,但这些也是可配置的。可以在配置文件中指定要打开或关闭的 JSHint 选项。 例如,以下文件将启用有关未定义和未使用的变量的警告,并告知 JSHint 一个名为 MY_GLOBAL 的全局变量。

{

  "undef": true,

  "unused": true,

  "globals": {

    "MY_GLOBAL": true

  }

}

但是,由于它是基于 JSLint 开发的,自然原有的一些问题它也继承下来了,比如不易扩展,不容易直接根据报错定位到具体的规则配置等。

评价

优点
  • 可以灵活配置规则,支持配置文件
  • 支持了一些常用类库
  • 支持了基本的ES6 语法
缺点
  • 不支持自定义规则
  • 无法根据错误定位到对应的规则

ESLint

2013年,Nicholas C. Zakas 创建了 ESLint ,提供了更好的 ES6 支持,以及更多的 rules ,尤其是一些代码风格方面的,以及一个灵活的插件系统,可以让开发者创建自己的 rules ,同时可以方便的根据报错定位到具体的规则配置。 规则的错误等级分为三级,可以更加细粒度地控制如何应用规则:

  • "off"0 - 关闭此条规则检查
  • "warn"1 - 警告,不会影响 exit code
  • "error"2 - 错误,exit code 为 1

默认情况下所有规则都是关闭的,"extends": "eslint:recommended" 会打开所有有“√”标记的规则,这些规则只跟着主版本更新,也可以在 npm 中查找以 eslint-config 开头的共享配置,通过 extends 配置项来添加。

ESLint 默认使用 Espree 作为 JavaScript 解析器,可以在 parser 配置项中更改解析器。解析器会将源代码解析成抽象语法树 AST(Abstract Syntax Tree),然后插件会根据这个 AST 来创建一些称为 lint rules 的断言,来描述代码应该是怎样的。

评价

优点
  • 默认规则里面包含了JSLintJSHint的规则,易于迁移
  • 有三种错误等级,可以更细粒度地控制 lint 的行为
  • 灵活的插件扩展机制
  • 可以自定义规则
  • 可以根据错误定位到对应的规则
  • 支持 ES6
  • 支持JSX
缺点
  • 更大的灵活性意味着更复杂的配置
  • 比前面两个慢

TypeScript

TSLint / typescript-eslint

用来检查 TypeScript 的,但是 2019 年已经废弃了,现在使用的是 ESLint,配合 typescript-eslint 。TypeScript 团队也宣布将 TypeScript 代码库从 TSLint 迁移到 typescript-eslint

ESLint 和 TypeScript

ESLint 使用一个 parser 将 source code 转成抽象语法树 Abstract Syntax Tree (AST) 的数据格式,然后插件根据这个 AST 来进行 lint rules 的检查。

TypeScript 是 JavaScript 的静态代码分析器,在基础的 JavaScript 上添加了一些额外的语法。TypeScript 使用一个 parser 将 source code 转成 AST ,然后 TypeScript Compiler 的其他部分使用这个 AST 来执行其他操作,例如给出类型检查后的问题反馈等等。

然而,ESLint 和 TypeScript 使用的是不同格式的 AST ,这就是 typescript-eslint 这个项目存在的主要原因。typescript-eslint 就是为了能够一起使用 ESLint 和 TypeScript 。

TSLint 使用的就是 TypeScript AST 格式,其优点是不需要一个调和 AST 格式之间差异的工具,但是主要缺点是 TSLint 无法重用 JavaScript 生态中围绕 linting 已经做好的工作,而是从头开始重新实现所有的功能,从规则到自动修复功能等等。因此,TypeScript AST 不兼容 ESLint 用户写成并使用的 1000 多条规则。

ESLint 解析 TypeScript:@typescript-eslint/parser

由于 TypeScript 是 JavaScript 的超集,它包含了所有 JavaScript 语法以外,还额外添加了一些语法,例如:

var x: number = 1;

当 TypeScript Compiler 解析这段代码生成 TypeScript AST 时,: number 语法也会出现在语法树中,ESLint 不借助其他工具是无法理解的。但 ESLint 在设计时就考虑到了这些用例。ESLint 不仅仅是一个库,而是由许多重要的移动部件组成。其中一个就是 parser 。ESLint 有一个内置的 parser 叫做 espree ,如果想支持非标准的 JavaScript 语法,只需要提供另外一个 parser 给 ESLint ,它需要将 TypeScript source code 解析为 ESLint 可以兼容的 AST 。 @typescript-eslint/parser 就是这样一个自定义的 ESLint parser 的实现。流程如下:

  1. ESLint 调用 ESLint config 中定义好的 parser ( @typescript-eslint/parser )。
  2. @typescript-eslint/parser 处理所有特定于 ESLint 的配置,然后调用 @typescript-eslint/typescript-estree 。这个包只用来将 TypeScript source code 转为一个适当的 AST 。
  3. @typescript-eslint/typescript-estree 通过调用 TypeScript Compiler 将源代码生成一个 TypeScript AST ,然后将这个 AST 转换为 ESLint 需要的格式。这种 AST 格式不仅仅用于 ESLint,还有更广泛的用途。它有自己的规范,也就是 ESTree@typescript-eslint/typescript-estree 还被其他工具重用,例如 Prettier 的 TypeScript 使用。

ESLint rules 兼容 TypeScript:@typescript-eslint/eslint-plugin

TypeScript 和 ESLint 有类似的目标,因此可能出现 TypeScript 解决的一些问题原本是依赖 ESLint 解决的,两者可能会不兼容,最佳的解决方式是禁用相关的 ESLint 规则,转而交由 TypeScript Compiler 。

由于 TypeScript 是 JavaScript 的超集,即使 AST 进行了转换,最终的 AST 可能还会包含一部分让 ESLint 无法理解的部分,所以有些 ESLint rules 可能无法正常工作。有几种解决方案:要么解决 ESLint rules 的兼容性,要么使用另外的规则,即 @typescript-eslint/eslint-plugin

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: [
    '@typescript-eslint',
  ],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
  ],
};

关于 Babel 和 babel-eslint

Babel 现在支持解析 TypeScript source code 但是不进行类型检查。这是使用 TypeScript Compiler 的一个替代方法。通过插件,它同样也可以支持许多其他 TypeScript Compiler 不支持的语法。

typescript-eslint 是由 TypeScript Compiler 提供支持的。babel-eslint 支持了一些 TypeScript 本身不支持的额外的语法,但是 typescript-eslint 利用类型信息可以支持创建 rules ,而这是 babel 做不到的,因为 babel 没有类型检查。因为它们是由不同的底层工具驱动的独立项目,所以目前不打算将它们一起使用。

其他

stylelint

用来检查样式,帮助避免错误和强制代码风格。可以理解最新的 CSS 语法,从 HTML、 markdown 及 CSS-in-JS 对象和模板中提取内联的样式,可以解析类 CSS 语法,如 SCSS、 Sass、 Less 和 SugarSS。支持插件,支持自定义规则。可以自动修复大多数违反代码风格的问题。并且是完全可配置的,通过在根目录添加配置文件 .stylelintrc.json 来按需配置。

commitlint

commitlint 用来检查 commit message ,帮助团队遵守 commit 约定,统一代码提交风格。支持通过 npm 安装已有的配置,或通过配置文件定义配置。使用 husky 来添加 git hooks :

// package.json

{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

commit-msg 钩子会在一个新的 commit 创建时执行,通过 -E|--env 传递 husky 的 HUSKY_GIT_PARAMScommitlint ,并将其定向到相关的编辑文件。

代码风格

目前主流的代码风格主要有 Airbnb JavaScript Style Guide 、 Google JavaScript Style Guide以及 JavaScript Standard。

  1. AirbnbJavaScript Style Guide(github star 数:100k):对应的 ESLint 配置:eslint-config-airbnb
  2. Google JavaScript Style Guide(25.6k):谷歌内部的 JavaScript 编码标准。eslint-config-google
  3. JavaScript Standard(24.4k):零配置,只需要运行 standard --fix 即可自动格式化代码,还可以进行代码检查。对应的 ESLint 配置:eslint-config-standard
  4. idiomatic.js(17.1k):社区版 JavaScript 编码风格指南。对应的 ESLint 配置:eslint-config-idiomatic

代码格式化

Prettier

目前最主流的是 Prettier ,它是一个专断的代码格式化工具,专注于 style issues。通过解析代码生成 AST,然后再按照自己的规则重新输出格式化后的代码。Prettier 执行的时机可以是在编辑器保存时、在 pre-commit hook 中或使用 CLI 工具在命令行中执行,以确保代码风格的一致。

由于历史原因,Prettier 仍然有一小部分选项,但官方更加推崇更少的配置项,因为其初衷就是终止团队关于代码风格的争论。这些选项一部分是初期添加的,一部分是需求增大导致的,还有一部分则与兼容性有关。选项越多,越有可能在团队内部引发争论。

Prettier 仅仅对 style issues 起作用。Prettier fork 了 recast 项目,并在内部使用了 Philip Wadler 提出的算法,该算法考虑了最大行宽(line width),最大行宽影响了代码的布局和换行,所以决定了最终输出的代码格式,而这是其他现有的代码格式化工具所欠缺的。例如,即便 eslint 会给出超出设定的最大行宽的警告,也无法自动修复。例如如下代码:

foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

最终想要的效果可能是这样:

foo(
  reallyLongArg(),
  omgSoManyParameters(),
  IShouldRefactorThis(),
  isThereSeriouslyAnotherOne()
);

Wadler 算法是一个简单的基于约束的代码布局系统。它“测量”代码,如果代码跨越了最大的行宽,就会中断它。Prettier 禁止所有自定义样式,方法是将源代码解析成 AST 后,使用自己的规则同时考虑最大行宽,并在必要时换行,重新输出格式化的代码。

Prettier 和 linters 有何区别?

linters 有两类 rules :

  1. Formatting rules:例如:max-len, no-mixed-spaces-and-tabs, keyword-spacing, comma-style
  2. Code-quality rules:例如:no-unused-vars, no-extra-bind, no-implicit-globals, prefer-promise-reject-errors

Prettier 承担的是 Formatting rules 的工作,是一个“全自动”的风格指南。

因此,可以使用 Prettier 来进行代码风格的格式化,使用 linters 来检查 bugs!

Prettier 和 linters 配合使用

由于 linters 通常会包含样式相关的规则,使用 Prettier 时,大多数样式规则都是不必要的,而且更糟糕的是,它们可能与 Prettier 冲突!因此可以将 Prettier 用于代码格式问题,将 linter 用于代码质量问题。

对于 ESLint ,可以安装 eslint-config-prettier ,来关闭所有不需要的或者可能会跟 Prettier 冲突的 ESLint rules。

stylelint 同理可以使用 stylelint-config-prettier

最佳实践

linter + code style + code formatter 的组合:ESLint + Airbnb + Prettier 。

这里以一个 TypeScript + React 项目举例:

具体做法:

  1. 安装 ESLint (此时的版本是 8.6.0)
yarn add eslint --dev
  1. 初始化 ESLint 配置
yarn run eslint --init

这个过程中 ESLint 会询问你一些项目信息,逐个回答后 ESLint 会帮你生成适当的配置文件:

配置文件:

// .eslintrc.js

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },

    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
};

eslint-config-airbnb 包含了 React,不包含 TypeScript 的相关 rules,需要手动修改配置文件:

{
    "extends": [
-       "plugin:react/recommended", // 使用 airbnb 中的 React rules
        "airbnb", // 如果不需要 React 则使用 airbnb-base
        "airbnb/hooks", // airbnb 默认未开启 React hooks,有需要可手动添加
+       "plugin:@typescript-eslint/recommended"
    ],
    "plugins": [
-       "react", // airbnb 已经添加了 React
        "@typescript-eslint"
    ],
}
  1. 安装 Prettier
yarn add --dev prettier

# 如果需要配置文件(如果需要忽略文件,则创建一个 .prettierignore 文件)
echo {} > .prettierrc.json
  1. 设置编辑器

最好能在保存时就自动格式化代码,让开发者无需关注代码格式,强迫症必备。

WebStorm 为例:对于 2020.1 及以上的版本:Preferences | Languages & Frameworks | JavaScript | Prettier 然后启用选项 Run on save for files

现在保存时就可以自动格式化代码了。

  1. 配置 ESLint + Prettier

需要安装 eslint-config-prettier ,这个包会关闭所有不需要的或可能和 Prettier 冲突的ESLint rules。

然后将其添加到 ESLint 配置文件中的 extends 配置项的最后,好让它能够覆盖其他配置。这里的 prettier 除了关闭一些核心的 ESLint rules,还会关闭以下插件的部分 rules:

由于这个配置只会关闭 rules,所以跟其他配置一起使用才有意义,注意要放到最后

{
    "extends": [
        "airbnb",
        "airbnb/hooks",
        "plugin:@typescript-eslint/recommended",
        "prettier"
}

注意,还可以使用 eslint-plugin-prettier 插件把 Prettier 当做一个 linter rule 来运行,但这不是官方推荐的做法,主要有以下几个劣势:

  • 编辑器中会出现很多烦人的红色波浪线,Prettier 的哲学就是让人忘记格式化,而不是给出一个提示。
  • 比直接运行 Prettier 要慢。
  • 它们只是一个间接层(one layer of indirection),有些东西可能会出问题。
  1. 添加 Git hooks

保险起见,最好在代码提交前也格式化一下。也就是可以在 pre-commit hook 里运行 Prettier 。

执行以下命令,会依赖 package.json 的 **devDependencies 中的代码检查工具来自动安装并配置 huskylint-staged ,因此需要确保事先已经安装了 PrettierESlint

npx mrm lint-staged

package.json 会自动添加相关配置(默认生成的配置针对 .js 文件的,可以按需改为 .ts.tsx 等),这样在每次提交前,都会通过 lint-stagedhusky 运行 ESLint 和 Prettier 。

{
  "devDependencies": {
    "husky": ">=4",
    "lint-staged": ">=10",
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,ts,tsx,jsx}": "eslint --cache --fix",
    "*.{js,ts,tsx,jsx,css,md}": "prettier --write"
  }
}

关于 lint-staged 的作用:在代码提交之前 linting 才有意义,这样可以确保不会将糟糕的代码上传到仓库中,以及强制统一风格。但是对整个项目运行一个 lint 过程很慢,而 linting 结果可能是无关紧要的。所以,只需要 lint 即将要提交的文件。 lint-staged 包含一个脚本,该脚本会运行任何 shell 任务,将staged files 作为参数,通过一个特定的 glob pattern 进行过滤。

husky(哈士奇)主要作用是添加 git hook(git 在特定的重要动作发生时触发自定义脚本),用来阻止不好的 git commitgit push 等等。