husy, lint-staged, eslint, prettier 统一前端代码风格

1,592 阅读10分钟

前言

前段时间接手了一个新项目,发现里面的代码书写风格和我的书写风格非常不同,比如 js 文件的缩进是 2 个空格,但是 css 的缩进是 4 个空格。还有 jsx 的写法也很奇怪:

<Modal
  title={intl.get("export")}
  onRequestClose={this.closeExportHitModal}>
  hello world
</Modal>

而官方的写法是:

<Modal
  title={intl.get("export")}
  onRequestClose={this.closeExportHitModal}
>
  hello world
</Modal>

还有些文件中,一行代码写了很多内容,完全可以拆成两行或者多行展示,读起来会清晰很多。总之,整个项目看起来很混乱,各种书写风格混杂在一起,这样非常不利于项目的维护。现在就我一个人在做这个项目,那最好的方式就是我按照一种统一的书写风格来改造整个项目,然后后面如果有人来接手的话就可以跟着这个节奏和风格写代码。但是问题是我没法保证所有人都和我想的一样,最好的方式就是通过工具来约束大家的书写风格,这样写的时候不用考虑风格的问题,最后提交的时候风格统一就行了。 

目前已知的 js 的代码风格和校验工具很多,lint 类的工具就有 jslint, jshint, jscs 和 eslint 等;代码风格类的工具,目前比较热门的就是 prettier。而实际上,这两种工具在使用的时候,处理的事务是有重叠的。所以如何选型,如何使用,如何划分各个工具之间的界限,如何处理冲突等等这些问题都会冒出来在我的脑海里。别急,我们一个一个来看。

预期达到的效果

在看具体的工具之前,我们先整理一下自己内心的需求,理出一个清晰的思路和目标,然后再朝着这个目标出发。

自动执行执行而不是手动运行

接手这个项目的时候,项目中已经在使用 eslint 了,但是是一个单独的 npm script,通过执行这个 script 来实现的,并不是自动化的在 git commit 的时候实现的。所以我期望可以实现通过 git 的 hooks 实现自动化检查和代码风格美化。同时,如果 eslint 有报错的情况下,终止 commit,并且打印出报告,告知代码写作者哪些文件在哪个位置出现了什么错误。这就需要用到 git hooks 相关的工具 husky。

对增量代码做处理而不是项目中已有的所有代码

同时接手的时候,项目已经有一定的规模了,我不希望现在就对整个项目的所有代码都进行代码的检验和风格的统一处理。因为在没有测试的情况下,这样有可能会造成一些无法预知的问题。所以只希望对增量代码对自动化的检验和风格统一的处理。这就需要用到另外一个工具:lint-staged。

lint 工具用那个?

js 的lint 工具很多,我应该用哪个呢?我期望是可配置的,并且支持 jsx 语法,且能不断升级的,因为 js 的语法变化很快,如果只是支持旧的语法的话,很快用起来就不好用了。所以我需要比较不同的 lint 工具,并且选择最终符合预期的。

最终期望效果

基于以上的各个期望,最终想要实现的综合效果就是:

在 git commit 的时候,通过 git hooks 的方式对增量的 commit 代码进行检查、校验和美化。自动化 lint 的时候,如果发现有错误,要可以展示错误信息,并且终止 commit,方便代码写作者修复错误。如果没有错误则直接美化后提交代码。

工具详述

每个工具看起来都很简单,但是实际上用起来还有很多坑的。现在来一一说明一下。

eslint

stackoverlow 中有个回答对理解 lint 有帮助:

Linting is the process of running a program that will analyse code for potential errors.

所以 lint 的过程就是发现可疑问题,并且解决这些可疑的过程。js 有很多的 lint 工具。为什么用 eslint 而不是其他的 lint 工具呢?大家可以参考一下这篇文章

总的来说:eslint 可配置,可扩展,插件化。通过插件化的方式可以兼容 jsx,还有很多 js 的新语法。eslint 是所有 js lint 工具中最强大和可信赖的。

prettier

乍一看,似乎 eslint 里已经有了代码风格相关的配置项,比如 max-len,里面有很多相关的配置项,可以配置一行最多的代码数量,还有 tabWidth 等等。那为什么还需要 prettier 呢?prettier 官网给了解释

Linters have two categories of rules: Formatting rules: eg: max-len, no-mixed-spaces-and-tabs, keyword-spacing, comma-style... Prettier alleviates the need for this whole category of rules! Prettier is going to reprint the entire program from scratch in a consistent way, so it's not possible for the programmer to make a mistake there anymore :)

Code-quality rules: eg no-unused-vars, no-extra-bind, no-implicit-globals, prefer-promise-reject-errors... Prettier does nothing to help with those kind of rules. They are also the most important ones provided by linters as they are likely to catch real bugs with your code!

解释一下就是,linters 提供两种类型的检查,一种是关于代码风格相关的格式化规则,另外一种是代码质量相关的规则。而 prettier 只关心前一种类型的规则,也就是和代码风格相关的格式化规则。为什么有了 eslint 还需要 prettier。因为 eslint 的规则都是可配置的,可配置项太多了。如果你想真正的按照自己的方式用的顺手的话,需要一个一个 api 去查看,用起来太费劲。所以 prettier 是一个 opinionated code formatter,就是非常主观的,甚至有点武断的一个格式化工具。

prettier 的官网中 why prettieroption phylosophy 中对为什么这么做也做出了解释,摘录几段:

By far the biggest reason for adopting Prettier is to stop all the on-going debates over styles. It is generally accepted that having a common style guide is valuable for a project and team but getting there is a very painful and unrewarding process. People get very emotional around particular ways of writing code and nobody likes spending time writing and receiving nits.

The more options Prettier has, the further from the above goal it gets. The debates over styles just turn into debates over which Prettier options to use.

也就是说大家都愿意统一代码风格,但是问题是大家对于该用哪个风格又有着喋喋不休的争论,而要达成一致实际上又是一件非常吃力不讨好的事情。所以 prettier 就是 opinionated 状态,不愿意添加很多配置项来讨好任何一方。所以 prettier 留给用户的可配置空间是非常有限的。愿意用就用,不用拉到。

既然 prettier 和 eslint 在功能上有重叠,那么在使用的时候就必然要有冲突,如何处理冲突,如何划定界限,如何让两者能很好的融合共同生效,我们在后面,如何配置 prettier 和 eslint 的这个部分有详细说明。实际上 prettier 也给出了方案

husky

为了实现和 git hooks 的联动,我们需要用到 husky。用起来很简单,在 package.json 里配置一个 husky 的项:

"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
}

pre-commit 后面可以是任意的命令,比如 npm run lint 类似这样的可以。因为我用了 lint-staged 所以就配置了 lint-staged 命令。

husky 有一个小坑,就是我配置了 husky,但是用的时候发现 hooks 不生效,找了半天原因原来是和版本有关。

husky 的 changelog 里有说明。

(husky 3.0.0)Breaking change husky requires now Git >= 2.13.2

husky 从 3.0.0 开始就需要 git 版本 >= 2.13.2 了。所以如果你发现 husky 失效了,先看下 husky 和 git 的版本要求是否相符。解决办法可以是升级 git 版本,也可以是降级 husky。目前项目用到的 husky 非常简单,就是上面一个配置项,而且基于大多数项目组员的 git 版本情况。我选择降级 husky 到 v2.7.0。

lint-staged

lint-staged 就是用来 lint staged 文件。

Run linters against staged git files and don't let 💩 slip into your code base!

而 lint-staged 的配置也很简单,在 package.json 里配置:

{
  "lint-staged": {
    "*": "your-cmd"
  }
}
  • 替换为要检查的文件,your-cmd 替换为你要执行的命令。根据我们的需求,我们的配置项为

    "lint-staged": { "*.{js,jsx}": [ "eslint --fix", "git add" ] }

也就是说先用 eslint 检查一下,然后把该修复的直接修复了,然后 git add,然后再 commit。如果 eslint --fix 的过程中发现 error,就直接报错并且跳出,取消 commit。

我们可以对不同的文件类型做不同的校验,例如:

stackoverflow.com/questions/5…

  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "*.{md}": [
      "prettier --write",
      "git add"
    ]
  }

整合所有工具

以上介绍了所有的工具,其中 husky 和 lint-staged 用法已经很清楚了,只需要在 package.json 中配置以下内容就可以了。

 "husky": {
   "hooks": {
     "pre-commit": "lint-staged"
   }
 },
 "lint-staged": {
   "*.{js,jsx}": [
     "eslint --fix",
     "git add"
   ]
 }

表示,在 commit 的时候,执行 lint-staged,就是对于已经添加到 staged 的 js/jsx 文件执行 eslint --fix 的命令,如果通过则 git add 然后 git commit 。如果没有通过则显示错误信息然后停止 commit。

但是 eslint 和 prettier 如何融合呢?prettier 的官网给出了方案

Whatever linting tool you wish to integrate with, the steps are broadly similar. First disable any existing formatting rules in your linter that may conflict with how Prettier wishes to format your code. Then you can either add an extension to your linting tool to format your file with Prettier - so that you only need a single command for format a file, or run your linter then Prettier as separate steps.

第一步就是禁用 linter 中的所有和 prettier 有冲突的代码格式化规则。然后,可以通过两种方式来使用 prettier,一种是通过给 linter 添加扩展来实现在 linter 中使用 prettier;另外一种是将 lint 和 prettier 拆开成两步,先使用 linter,然后再用 prettier 处理 linter 后的代码。

文章中也给出了如何 eslint 融合的方案。照着使用就可以了。

第一步:禁用 eslint 中 formatting rules

如果是手动一个一个去禁用那就太麻烦了,直接使用 eslint-config-prettier就可以了。

eslint-config-prettier is a config that disables rules that conflict with Prettier. Add it to your devDependencies, then extend from it within your .eslintrc configuration. Make sure to put it last in the extends array, so it gets the chance to override other configs.

第二步:在 eslint 中使用 prettier

eslint-plugin-prettier is a plugin that adds a rule that formats content using Prettier. Add it to your devDependencies, then enable the plugin and rule.

也就是说用 eslint-plugin-prettier这个插件可以在 eslint 中支持 prettier 的应用。但是官网直接给出了以上两步的一个综合解决方案。

eslint-plugin-prettier exposes a "recommended" configuration that configures both eslint-plugin-prettier and eslint-config-prettier in a single step. Add both eslint-plugin-prettier and eslint-config-prettier as developer dependencies, then extend the recommended config:

同时安装 eslint-config-prettier 和 eslint-plugin-prettier,然后在 eslint 中配置:

{
  "extends": ["plugin:prettier/recommended"]
}

这样就可以了,不需要单独配置 config 和 plugins 了。

最后,我的 eslint 配置用的是 .eslintrc,配置内容如下,可以参考:

 {
   "parser": "babel-eslint",
   "parserOptions": {
     "ecmaFeatures": {
       "legacyDecorators": true
     }
   },
   "extends": ["standard", "standard-react", "plugin:prettier/recommended"],
   "plugins": ["babel", "react", "promise"],
   "env": {
     "browser": true
   },
   "globals": {
     "__DEV__": false,
     "__TEST__": false,
     "__PROD__": false,
     "__COVERAGE__": false
   },
   "rules": {
     "no-unused-vars": "error",
     "react/prop-types": ["warn"],
     "camelcase": ["warn"]
   }
 }

安装的 package 包括:

 "babel-eslint": "^9.0.0",
 "eslint": "^6.4.0",
 "eslint-config-prettier": "^6.2.0",
 "eslint-config-standard": "^6.0.0",
 "eslint-config-standard-react": "^4.0.0",
 "eslint-plugin-babel": "^4.0.0",
 "eslint-plugin-prettier": "^3.1.0",
 "eslint-plugin-promise": "^3.0.0",
 "eslint-plugin-react": "^6.0.0",
 "eslint-plugin-standard": "^2.0.0",

以上配置仅供参考。

结语

规范仅仅通过人为的培训或者文档来约束,实际上不可靠的。只有通过这种自动化的工具才能实现真正的可靠地统一。这个其实是我做这件事获得的最大的收获,所以在未来的工作中,能通过工具约束的尽量通过工具约束,而不要依靠默契和口头约定。