深圳幻海软件技术有限公司 欢迎您!

ESLint 在中大型团队的应用实践

2023-02-28

使用背景代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到,并或多或少会思考过这一问题。随着前端应用的大型化和复杂化,越来越多的前端工程师和团队开始重视JavaScript代码规范。主要解决的问题:对于独立开发者,或者执行力较强、技术场景较为单一的小型团队而言,直接使用ESLi

使用背景

代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到,并或多或少会思考过这一问题。随着前端应用的大型化和复杂化,越来越多的前端工程师和团队开始重视 JavaScript 代码规范。

主要解决的问题:

对于独立开发者,或者执行力较强、技术场景较为单一的小型团队而言,直接使用 ESLint 及其生态提供的一些标准方案,可以用较低成本来实现 JavaScript 代码规范的落地。如果再搭配一些辅助工具(例如 husky 和 lint-staged),整个流程会更加顺畅。

ESLint对工程代码进行静态检查,发现和修复不符合规范的代码。如果想降低配置成本,也可以直接使用开源配置方案,例如 eslint-config-airbnb 或 eslint-config-standard。

一、理解代码检查

代码检查,顾名思义就是检查代码,发生于开发阶段,可有效帮助开发者减少 JavaScript 粗心代码,如语法错误、变量未定义、变量未使用等等问题。除此之外,代码检查还可以约束统一开发人员的代码风格,利于团队协作。

我们再从三个方面来展开分析,加深对代码检查的实践理解。

这三个方面分别为以下三点:

1)代码检查的功能

2)代码检查的类型

3)代码检查的工具

1、代码检查的功能

代码检查这个切面大概可以帮助我们做以下三件事情:

  • 语言语法检查:比如检查出字符串引号或者函数调用括号没有匹配等问题。
  • 编码错误检查:比如检查出开发者在使用一个不存在的变量或者变量定义了却没有使用等问题。
  • 代码风格检查:比如检查出开发者没有使用分号(与所选风格有关)等问题。

2、代码检查的方式

以代码检查发生的不同时间和场景来划分,我把代码检查的方式分类成以下四种:

  • 编码时检查:编写代码时检查,通常表现为由 IDE 自动实时检查并进行代码提示。
  • 编码后检查:编写代码后检查,通常表现为手动调用检查脚本 / 工具进行代码的检查或者代码保存后由 IDE 自动检查当前文件。
  • 构建前检查:构建执行前检查,通常表现为将代码检查作为构建任务的一个前置切面,构建时自动触发代码检查。
  • 提交前检查:git commit 前检查,通常表现为将代码检查作为 git commit 的一个 hooks 任务,代码提交前自动触发代码检查。

理解代码检查的方式很重要,这直接反映对代码检查这个概念本身的掌握程度。

3、代码检查工具

代码检查的实现通常不会仅仅是字符串分析处理,这其中会大量涉及到语法分析。既然涉及到语法,那么就需要对不同的代码使用不同的代码检查工具,通常来说,我们会使用 Eslint 工具来实现对 JavaScript 和 Typescript 代码的检查,使用 stylelint 工具对样式代码进行代码检查。

二、ESLint 特性简介

目前较为通用的方案——ESLint,它是一款插件化的 JavaScript 代码静态检查工具,其核心是通过对代码解析得到的 AST(Abstract Syntax Tree,抽象语法树)进行模式匹配,定位不符合约定规范的代码。

1、插件化

下图简单地描述了 ESLint 的工作过程:

ESLint 的能力更像一个引擎,通过提供的基础检测能力和模式约束,推动代码检测流程的运转。原始代码经过解析器的解析,在管道中逐一经过所有规则的检查,最终检测出所有不符合规范的代码,并输出为报告。借助插件化的设计,不但可以对所有的规则进行独立的控制,还可以定制和引入新的规则。ESLint 本身并未和解析器强绑定,我们可以使用不同的解析器进行原始代码解析,例如可以使用 babel-eslint 支持更新版本、不同阶段的 ES 语法,支持 JSX 等特殊语法,甚至可以借助 @typescript-eslint/parser 支持 TypeScript 语言的检查。

2、配置能力全面、可层叠、可共享

ESLint 提供了全面、灵活的配置能力,可以对解析器、规则、环境、全局变量等进行配置;可以快速引入另一份配置,和当前配置层叠组合为新的配置;还可以将配置好的规则集发布为 npm 包,在工程内快速应用。

3、社区生态较为成熟

开源社区中基于 ESLint 的项目非常多,既有针对各种场景、框架的插件,也有各种 ESLint 规则配置方案,基本可以涵盖前端开发的所有场景。

4、规范配置方案设计

基于 ESLint 的插件化、可层叠配置特性,以及面向各种场景、框架的开源方案,如下图所示的 ESLint 配置架构:

「美团技术团队」

该配置架构采用了分层、分类的结构,其中:

  • 基础层:制定统一的基础语法和格式规范,提供通用的代码风格和语法规则配置,例如缩进、尾逗号等等。
  • 框架支撑层(可选):提供对通用的一些技术场景、框架的支持,包括 Node.js、React、Vue、React Native 等;这一层借助开源社区的各种插件进行配置,并对各种框架的规则都进行了一定的调整。
  • TypeScript 层(可选):这一层借助typescript-eslint,提供对 TypeScript 的支持。
  • 适配层(可选):提供对特殊场景的定制化支持,例如 MRN(美团内部的 React Native 定制化方案)、配合 prettier 使用、或者某些团队的特殊规则诉求。

*具体各个规则如何配置可以查看:https://eslint.org/docs/rules

三、为项目接入 Eslint 实现代码检查

Eslint 是一款插件(检查规则)化的 JavaScript 代码检查工具。概念言简意赅,需要注意的是,概念中说到 eslint 是一个插件化的检查工具,其意思是指 eslint 的设计是把检查工具和检查规则之间解耦了。也就是说,在安装好 eslint 的开发依赖之后,我们还可以并且需要选择安装一个我们中意的检查规则。

1、安装检查工具 eslint

npm install eslint --save-dev
  • 1.

2、安装并配置检查规则

在探讨配置安装检查规则之前,我们有必要先明确一下我们的检查目标是什么。我认为,检查目标自然是构建前的代码,并且是自己 / 自己团队编写的代码(非第三方模块)。毕竟检查的最终目标是为修复服务的,我们只负责修复自己 / 自己团队编写的代码,构建后代码以及第三方代码即使检查不通过我们也不会也不应该由我们去修复。

检查规则在项目中通常有两种表现形式,即:

  • 配置文件中配置的规则:主要形式,通过继承和扩展的方式声明了大量规则;
  • 项目代码中的魔法注释:次要形式,通常是用于作为配置文件中规则的特例;

生成 eslint 配置文件:

对于配置文件,我们通常会使用 npx eslint --init 生成一个 ESLint 配置文件 .eslintc.js,如下示例:

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/essential',
    'standard'
  ],
  parser: require.resolve('vue-eslint-parser'),
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module'
  },
  plugins: [
    'vue'
  ],
  rules: {
    indent: ['off', 2]
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

代码中的魔法注释写法:

除了配置文件中配置规则,eslint 还有一个代码中通过魔法注释打规则补丁的办法,如下示例:

// 屏蔽整行的代码检查
const str1 = "${name} is a coder" // eslint-disable-line
// 屏蔽某一个规则:如此行的no-template-curly-in-string规则
const str1 = "${name} is a coder" // eslint-disable-line no-template-curly-in-string 
  • 1.
  • 2.
  • 3.
  • 4.

*扩展:ESLint 禁用检查

1.禁用代码块

/* eslint-disable */
consle.log("foo");
consle.log("bar");
/* eslint-disable */
  • 1.
  • 2.
  • 3.
  • 4.

2.禁用单行(放在该行代码后面)

consle.log("foo"); // eslint-disable-line
  • 1.

3.禁用下一行

// eslint-disable-next-line
console.log("foo")
  • 1.
  • 2.

4.禁用文件(放在代码最顶部)

/* eslint-disable */
consle.log("foo");
consle.log("bar");
  • 1.
  • 2.
  • 3.

四、Eslint 编码后检查 JS 代码

对于一个工作流程的解释,我还是更倾向于直接演示一个简单的 demo。根据代码检查的逻辑,demo 演示和讲解时我会遵循以下思路,即:

  • 目标问题代码
  • 代码检查规则配置
  • 代码检查操作和结果
  • 修复代码操作

1、目标问题代码

/* eslint-disable */
const noUsedVar = 1;
function fn() {
  console.log('hello')

  cnsole.log('eslint');
}
fn(
fn2();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

以上短短几行代码就可以表示出 eslit 代码检查的三个部分,它们分别是:

  • 语法错误
  • 编码错误:未定义、未使用
  • 编码风格:没有分号

2、代码检查规则配置

通过 eslint --init,根据项目特征来回答问题之后得到的 eslint 配置文件如下:

/* eslint-disable */
module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: [
    'airbnb-base',
  ],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parserOptions: {
    ecmaVersion: 2018,
  },
  rules: {
  },
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

3、代码检查操作和结果

第一轮检查结果先是报了语法错误,在修复语法错误之后,第二轮检查报错了很多编码以及风格上的错误。将两次检查结果关联到问题代码可以得到如下分析:

const noUsedVar = 1; // find program:'noUsedVar' is assigned a value but never used

function fn() {
  console.log('hello') // enforce code style:Missing semicolon(分号)

  cnsole.log('eslint'); // find program:'cnsole' is not defined
}
fn( // syntax error
fn2(); // find program:'fn2' is not defined
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

4、修复代码

根据上述检查结果进行修复。对于代码风格上的不一致导致的错误,通过参数 --fix 即可以自动修复大部分的问题。而对于语法以及编码上的错误则大部分只能是开发者自己手动修复。经过手动修复以及自动修复之后,问题代码可能变为如下模样:

*执行代码:npx eslint --fix 相关地址

import { fn2 } from './test2';

function fn() {
  console.log('hello');

  console.log('eslint');
}

fn();

fn2();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

五、Eslint 规则实现git 提交前检查

探讨 Eslint 如何实现 git 的提交前检查:

  • 实现 hook 任务流:通过 lint-staged 来配合 husky 来实现
  • 实现 git 提交前检查:先执行 eslint 任务而后执行 git add 任务

下面我们进入第一点,git 提交前检查原理:Git Hooks 的探讨。

1、实现 hook 任务流:通过 lint-staged 来配合 husky 来实现

实现步骤如下:

  • 安装 husky 和 lint-staged 模块
  • 配置 husky 的 hook 任务流:如下 package.json 任务
  • 触发任务流:git add -> git commit

1) 安装 husky 和 lint-staged 模块

yarn add husky --dev
yarn add lint-staged --dev
  • 1.
  • 2.

2) 配置 husky 的 hook 任务流:如下 package.json 任务

"scripts": {
    "precommit": "lint-staged"
},
"husky": {
    "hooks": {
        "pre-commit": "yarn precommit"
    }
},
"lint-staged": {
 "*.js":[
        "eslint --fix",
        "git add"
 ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

3) 触发任务流:git add -> git commit

实践发现,与单独的 husky 模块实现单任务相比而言,使用 lint-staged 之后,git commit 命令只有成功执行(有 add 资源并且有提交信息)才会触发 lint stage 中的操作,且这些操作只会对 js 文件有效。

2、实现 git 提交前检查:先执行 eslint 任务而后执行 git add 任务

实现步骤如下:

  • 安装 husky 和 lint-staged 模块
  • 配置 husky 的 hook 任务流:如下 package.json 任务
  • 触发任务流:git add -> git commit

1): 安装 husky 和 lint-staged 模块

yarn add husky --dev
yarn add lint-staged --dev
  • 1.
  • 2.

2): 配置 husky 的 hook 任务流:如下 package.json 任务

"scripts": {
    "precommit": "lint-staged"
},
"husky": {
    "hooks": {
        "pre-commit": "yarn precommit"
    }
},
"lint-staged": {
 "*.js":[
        "eslint --fix",
        "git add"
 ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

3) 触发任务流:git add -> git commit

经过这些开发包的下载以及配置,在我们执行 git commit 之后,就会触发 husky 配置的 pre-commit 的 hook 任务,而这个 hook 任务又把任务交给了 lint-staged 处理,进而通过 lint-staged 实现对 js 文件的代码检查以及自动风格修复后(错误则会中断提交)重新 add,而后再执行 commit 任务,保证了代码在提交前一定经过了代码检查。