【从 0 到 1 搭建 Vue 组件库框架】3. 集成 lint 代码规范工具

2,886 阅读14分钟

导航

导航:0. 导论

上一章节: 2. 在 monorepo 模式下集成 Vite 和 TypeScript - 下

下一章节:4. 定制组件库的打包体系

本章节示例代码仓:Github

引言

记得我刚参加工作不久时,前期负责的一项工作就是清理代码检查服务扫出的代码规范问题。当时公司的代码检查平台无比地难用,所有检查出的规范错误问题都显示在网页上,本地开发环境中没有任何的提示,大家需要参考规范文档“盲改”代码,提交后重新扫描,一遍一遍地减少错误。

当时我对这种情况非常痛心,一方面因为缺少 IDE 的提示,这样的修改策略效率低下;另一方面,由于缺少强硬的措施,Clean Code 的理念也难以深入人心,修改规范无非是应付一时工作的形式主义,往后大家还会本能地往里提交不规范的代码,下次到了检查窗口期,还得再花费无谓的工作量去应付检查。

于是,我开始认真研究以前轻度接触过的前端 Lint 工具,并将它们一步一步在我们的项目中落地。在 Lint 系列工具与命令行、IDE、CI 流程高度结合后,完全消灭了不符合规范的增量代码,存量代码也随着迭代一点一点地规范化。从此,我们前端组再也没有花精力在应付所谓 Clean Code 检查的工作。

许多人认为代码规范的核心在于人,而工具起不到根本作用;或者认为已有的“屎山”已经积重难返,规范工具无力回天。在我看来,也许是因为没有正确、完善地使用工具。

本期我们会给自己的组件库接入全套完善的 Lint 工具链,但是这一期的分享对于普通项目也具有极高的参考性,任何前端项目都可以按照这样的方法实现代码风格的规范化。 在实践之前,读者可以先关注下面的问题,看看有没有覆盖自己的疑惑。这些问题的答案将在下面的实践中逐步清晰:

  • 如何集成 ESLint,控制代码风格?
  • ESLint 如何与 VueTypeScript 配合使用?
  • 如何集成 StyleLint,控制样式风格?
  • StyleLint 如何与 VueSCSS 配合使用?
  • Prettier 应该如何使用?
  • 如何集成 commitlinthusky,控制提交信息风格?
  • 如何实现增量 Lint 检查?更好地应对积重难返的“屎山”项目。

ESLint

ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。

使用 ESLint 需要我们在项目的根目录下添加 .eslintrc.js.eslintrc.json 文件,在其中导出配置对象。下面是从官网中摘录的一个典型的配置文件,稍作了一点修改,它结合了 typescript-eslint 实现了对 TypeScript 的支持,我们以它为例子帮助大家简单地理解重要的配置字段,并梳理 ESLint 的工作思路。

{
  "root": true,

  // 继承已有配置对象
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],

  // 如何理解代码
  "parser": "@typescript-eslint/parser",
  "parserOptions": { "project": ["./tsconfig.json"] },

  // 添加哪些规则
  "plugins": [
    "@typescript-eslint"
  ],

  // 已添加规则的开启 / 关闭
  "rules": {
    "@typescript-eslint/strict-boolean-expressions": [
      2,
      {
        "allowString" : false,
        "allowNumber" : false
      }
    ]
  },

  // 对特殊文件应用特殊配置
  "overrides": [
    {
      "files": ["*.vue"],
      "rules": {
        // 所有 .vue 文件除了应用上面的公共规则配置外,还需应用的独特规则配置。
      },
    },
  ],
}
  1. ESLint 如何理解代码?

parserparserOptions 选项与 ESLint 如何理解我们的代码相关。这里分析器 @typescript-eslint/parser 负责解析 TypeScript 语言,将代码转化为 AST 语法树,便于进行分析。而 parserOptions 可以对解析器的能力进行详细设置。

  1. ESLint 如何判断代码是否规范?

ESLint 提供了 自定义规则 的接口,开发者需要遵照接口,根据分析器的 AST 产物,实现规范检查逻辑,再将实现的多条规范聚合为 plugin 插件的形式。plugin 字段指定了 ESLint 应用什么规则集,具有理解哪些规范的能力。

  1. 规则的启用与禁用

有了规则集,能够理解规范,不代表 ESLint 就要对不规范的内容做出响应,还需要进一步在 rules 字段中对这些规则进行开启或者关闭的声明,只有开启的规则才会生效。

  1. 继承已有配置

面对琳琅满目的规则集,我们完全在项目中配置是不可取的。因此社区逐渐演进出了许多配置预设,让我们可以一键继承,从而减少绝大多数手动配置的工作量。例如例子中的 eslint:recommendedplugin:@typescript-eslint/recommended 就代表继承了 eslinttypescript-eslint 的推荐配置。

  1. 配置的重写 如果我们希望某些文件应用一些独特的配置,可以使用 overrides 字段实现。overrides 的每个成员对象都需要指定目标文件,除了应用所有父级配置之外,还要应用成员对象中声明的独有配置。ESLint 支持文件级别的重写。

规则集的选型

经过了多年的发展,ESLint 已经有了许多成熟的规则集,这些规则集都是成熟团队的实践。它们通过 plugin 实现了很多个性化的规则,又内置了海量的 rules 配置预设。用户只需简单的几行代码继承,就相当于拥有了这些优秀团队的优秀实践。

这里给大家推荐一些我了解过的规则集:

采用什么样的规则集并不是最重要的,重要的是“正确引入,严格执行”,引导团队成员同一规范,写出整洁、可维护的代码。 我个人倾向于使用 Airbnb 规则集,这套规则集在 Github 上当下拥有最多的 star 数,特点是非常严格,需要根据自己项目的情况调整或关闭一部分限制。大部分公司在制定自己内部的编码规范时,都会高度地参考这套规则,并通常沿用其中 80% 以上的内容。因此学习 Airbnb 的规则也自然就适应了公司内的大部分规则。

依赖安装

首先我们需要安装 eslint 核心工具(对 pnpm workspace 指令不熟悉的读者可以回顾:1. 基于 pnpm 搭建 monorepo 工程目录结构)。

pnpm i -wD eslint

由于我们要具备解析 TypeScript 的能力,所以要安装 typescript-eslint 系列工具。同理,为了能够解析 Vue 语法,也要安装 vue-eslint-parser

pnpm i -wD @typescript-eslint/parser @typescript-eslint/eslint-plugin vue-eslint-parser

import 模块引入相关的规则、Vue 相关规则并不包含在默认规则集、typescript-eslint 规则集以及 Airbnb 规则集中,所以我们要额外安装对应的 plugin,引入这些规则集。

pnpm i -wD eslint-plugin-import eslint-plugin-vue

之后安装 Airbnb 规则集,便于我们一键继承。

pnpm i -wD eslint-config-airbnb-base eslint-config-airbnb-typescript

最后给大家推荐 eslint-define-config,这个库能够让在我们编写 .eslintrc.js 配置文件时,提供完善的类型支持,大幅度提升体验,强烈推荐安装!

pnpm i -wD eslint-define-config

配置

我们在根目录建立 .eslintrc.js 文件,作为 ESLint 的配置文件。

// .eslintrc.js
const { defineConfig } = require('eslint-define-config');
const path = require('path');

module.exports = defineConfig({
  // 指定此配置为根级配置,eslint 不会继续向上层寻找
  root: true,

  // 将浏览器 API、ES API 和 Node API 看做全局变量,不会被特定的规则(如 no-undef)限制。
  env: {
    browser: true,
    es2022: true,
    node: true,
  },

  // 设置自定义全局变量,不会被特定的规则(如 no-undef)限制。
  globals: {
    // 假如我们希望 jquery 的全局变量不被限制,就按照如下方式声明。
    // $: 'readonly',
  },

  // 集成 Airbnb 规则集以及 vue 相关规则
  extends: [
    'airbnb-base',
    'airbnb-typescript/base',
    'plugin:vue/vue3-recommended',
  ],

  // 指定 vue 解析器
  parser: 'vue-eslint-parser',
  parserOptions: {
    // 配置 TypeScript 解析器
    parser: '@typescript-eslint/parser',

    // 通过 tsconfig 文件确定解析范围,这里需要绝对路径,否则子模块中 eslint 会出现异常
    project: path.resolve(__dirname, 'tsconfig.eslint.json'),

    // 支持的 ecmaVersion 版本
    ecmaVersion: 13,

    // 我们主要使用 esm,设置为 module
    sourceType: 'module',

    // TypeScript 解析器也要负责 vue 文件的 <script>
    extraFileExtensions: ['.vue'],
  },

  // 在已有规则及基础上微调修改
  rules: {
    'import/no-extraneous-dependencies': 'off',
    'import/prefer-default-export': 'off',

    // vue 允许单单词组件名
    'vue/multi-word-component-names': 'off',

    'operator-linebreak': ['error', 'after'],
    'class-methods-use-this': 'off',

    // 允许使用 ++
    'no-plusplus': 'off',

    'no-spaced-func': 'off',

    // 换行符不作约束
    'linebreak-style': 'off',
  },

  // 文件级别的重写
  overrides: [
    // 对于 vite 和 vitest 的配置文件,不对 console.log 进行错误提示
    {
      files: [
        '**/vite.config.*',
        '**/vitest.config.*',
      ],
      rules: {
        'no-console': 'off',
      },
    },
  ],
});

结合上文中对 ESLint 主要字段的讲解以及注释内容,不难理解这个配置文件的含义。这里我们需要注意一下 parserOptions.project 字段,TypeScript 解析器需要一个 tsconfig 文件来确认解析范围。

我们希望 ESLint 检查能覆盖所有源码文件,但是 tsconfig.json 已经被占用做其他用途(IDE 语言服务),不能够迁就 ESLint,因此我们只得另外建立一个 ESLint 专用的文件 tsconfig.eslint.json,在其中包含所有希望被规范化的源码文件。这也是 typescript-eslint 官方为 monorepo 型工程推荐的一种解决方案(Monorepo Configuration)。

// tsconfig.eslint.json
{
  // eslint 检查专用,不要包含到 tsconfig.json 中
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    // 参考 https://typescript-eslint.io/linting/typed-linting/monorepos
    "noEmit": true
  },
  // 只检查,不构建,因此要包含所有需要检查的文件
  "include": [
    "**/*",
    // .xxx.js 文件需要单独声明,例如 .eslintrc.js
    "**/.*.*"
  ],
  "exclude": [
    // 排除产物目录
    "**/dist",
    "**/node_modules"
  ]
}

对于一些我们不希望应用 ESLint 检查的内容,我们可以通过 .eslintignore 文件将之排除,.eslintignore 的规则与 .gitignore 的规则完全相同。我们排除 ESLint 对依赖目录与产物目录的检查。

# .eslintignore
node_modules
dist

!.eslintrc.js
!.stylelintrc.js
!.prettierrc.js
!.lintstagedrc.js
!.commitlintrc.js

关于 .eslintignore 还有一点需要注意:ESLint 默认忽略对 . 开头文件的检查。 对于配置文件 .eslintrc.js 以及之后的 .stylelintrc.js 等都需要用 ! 反向声明忽略。

之后,我们在 package.json 中加入 eslint 检查的脚本,并尝试执行检查。

// package.json
{
  // 其他配置...
  "scripts": {
+   "lint:script": "eslint --ext .js,.jsx,.ts,.tsx,.vue --fix ./",
    // 其他脚本...
  }
}

pnpm run lint:script

eslint.png

正确配置后,ESLint 能检查出不少错误,包括了 .vue.ts 文件。

不过这样获取的错误信息并不直观,我们会在下文去探索更优的方式,增强 ESLint 的使用体验。

Stylelint

接下来,我们进入 Stylelint 的部分。

Stylelint 是一个强大的 CSS 格式化工具,可以帮助使用者避免语法错误并统一编码风格。

Stylelint 的原理与上面讲到的 ESLint 是一样的,只不过它是 CSS 样式领域的 Lint 工具。在上手了 ESLint 之后,我们理解 Stylelint 并不会有太大的困难。

首先,为项目安装 Stylelint,并结合我们的实际需求安装必要的插件:

pnpm i -wD stylelint
pnpm i -wD stylelint-config-standard-scss stylelint-config-recommended-vue stylelint-config-recess-order stylelint-stylistic

我们在项目根目录建立 .stylelintrc.js,编写配置文件。配置文件的写法与 ESLint 几乎是完全一致的,只有 rules 中规则的开关选项有所不同,在 ESLint 中使用 'warn'(警告) | 'error'(报错) | 'off' 开关规则,在 Stylelint 中使用 any(不同的规则要求的值不同) | null(关闭规则统一) 开关规则。

// .stylelintrc.js
module.exports = {
  // 继承的预设,这些预设包含了规则集插件
  extends: [
    // 代码风格规则
    'stylelint-stylistic/config',
    // 基本 scss 规则
    'stylelint-config-standard-scss',
    // scss vue 规则
    'stylelint-config-recommended-vue/scss',
    // 样式属性顺序规则
    'stylelint-config-recess-order',
  ],
  rules: {
    // 自定义规则集的启用 / 禁用
    // 'stylistic/max-line-length': null,
    'stylistic/max-line-length': 100,
  },
};

同样,.stylelintignore 文件也要忽略产物目录和依赖目录。

# .stylelintignore
node_modules
dist

接着,我们在 button 包源码目录中建立 button.scss 文件,并且在 button.vue 中补充 <style></style> 部分,填写一些测试的 scss 样式,检查 stylelint 能否识别。

/* packages/button/src/button.scss */
.test-class {
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

<script setup lang="ts">
// packages/button/src/button.vue

// 先前的内容。。。
</script>

<template>
  <!-- 先前的内容。。。 -->
</template>

<style lang="scss">
.testClass {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
  user-select: none;
  border: 1px solid transparent;
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
  color: #212529;
  background-color: #e9ecef;
}
</style>

package.json 中加入 stylelint 检查的脚本,准备执行检查。

// package.json
{
  // 其他配置...
  "scripts": {
+   "lint:style": "stylelint --fix ./**/*.{css,scss,vue,html}",
    // 其他脚本...
  }
}

pnpm run lint:style

stylelint.png

配置完成后,Stylelint 成功地在 .vue.scss 文件中检查除了错误。

Prettier

Prettier 是一个固执己见的代码格式化工具。

它是一款只需进行简单配置,就能支持多种语言格式化的工具。由于它专注的方向是代码风格(换行、缩进),并不涉及语法检查,因此很多实践中会让 PrettierLint 系列工具互相配合——将 Prettier 以插件规则集的方式集成到 Lint 中,并关闭原本内置的功能重复的规则。

但是,在我们的组件库中,Prettier 是被边缘化的。 理由如下:

  • ESLintStylelint 本身就有控制代码风格的规则。只不过它们只针对 JS / TS / CSS,不如 Prettier 支持的语言种类多,但我们对其他语言支持的需求度不高。
  • PrettierLint 结合使用时,所有的格式错误都会被标注为统一的 prettier/prettier,没法进一步细分错误。
  • Prettier 比较固执己见,无法对规则进行更细粒度的控制,ESLintStylelint 这方面的潜力更大。

总体上,我比较赞同 antfu 大佬的观点:为什么我不使用 Prettier。因此我们的组件库将使用 Prettier 完成 ESLintStylelint 不支持的文件类型的格式化, 例如 Markdownjsonyaml 等。

如果你还是希望详细了解如何配合使用 ESLintStylelintPrettier,使它们之间可以互相兼容,可以阅读这篇文章: 你不能再说你不会配置ESLint和prettier了

接下来我们将执行以下操作完成 Prettier 的集成:

  • 安装 Prettier
pnpm i -wD prettier
  • 在根目录,创建配置文件 .prettierrc.js,创建 .prettierignore 忽略依赖目录与产物目录:
// .prettierrc.js
module.exports = {
  // 一行最多字符
  printWidth: 100,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 末尾需要有逗号
  trailingComma: 'all',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 标签闭合不换行
  bracketSameLine: true,
  // 箭头函数尽量简写
  arrowParens: 'avoid',
  // 行位换行符
  endOfLine: 'lf',
};

# .prettierignore
node_modules
dist

与 IDE 插件结合

接下来我们要致力于提高 Lint 工具的本地使用体验,让它们与 IDE 结合起来,在编码过程中可以实时展示错误。我们需要安装三个 VSCode 插件:ESLintStylelintPrettier

eslint-ext.png

stylelint-ext.png

prettier-ext.png

接下来,我们要对这些插件进行一些配置,将项目的格式规范要求在 IDE 的层面固定下来。这里采取修改 .vscode 目录下的项目级 IDE 配置的方式(回顾:2. 在 monorepo 模式下集成 Vite 和 TypeScript - 下)。

首先,在 extensions.json 中增加这三个插件,引导新贡献者安装:

// .vscode/extensions.json
{
  "recommendations": [
    // ...
+   "esbenp.prettier-vscode",
+   "stylelint.vscode-stylelint",
+   "dbaeumer.vscode-eslint",
  ]
}

接着进行 settings.json 项目级 IDE 选项的配置:

// .vscode/settings.json
{
  // 已有配置...

  // 关闭 IDE 自带的样式验证
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  // 指定 stylelint 生效的文件类型(尤其是 vue 文件)。
  "stylelint.validate": ["css", "scss", "postcss", "vue"],

  // 启用 eslint 的格式化能力
  "eslint.format.enable": true,
  // eslint 会在检查出错误时,给出对应的文档链接地址
  "eslint.codeAction.showDocumentation": {
    "enable": true
  },
  // 指定 eslint 生效的文件类型(尤其是 vue 文件)。
  "eslint.probe": ["javascript", "typescript", "vue"],
  // 指定 eslint 的工作区,使每个子模块下的 .eslintignore 文件都能对当前目录生效。
  "eslint.workingDirectories": [{"mode": "auto"}],
}

经过配置后,无论是 JS / TS / vue 文件都可以在 IDE 层面提示错误了。

ide-lint.png

ide-lint-vue.png

我们还可以让 IDE 帮我们自动修复错误,调整格式,从而避免大量手动操作。我们继续在 settings.json 中配置:

  • editor.codeActionsOnSave 的相关配置,让 ESLintStylelint自动修复功能在保存文件时触发。 当然,部分复杂的错误无法自动修复,需要人工检视。
  • 将默认的格式化工具设为 Prettier,但是禁用自动格式化,避免格式化与自动修复之间的冲突。自动格式化只对非 ESLintStylelint 目标的文件开启, 例如 jsonyaml
// .vscode/settings.json
{
  // 已有配置。。。

  // 设置默认格式化工具为 Prettier
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  // 默认禁用自动格式化(手动格式化快捷键:Shift + Alt + F)
  "editor.formatOnSave": false,
  "editor.formatOnPaste": false,

  // 启用自动代码修复功能,保存时触发 eslint 和 stylelint 的自动修复。
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  },

  // volar 可以处理 vue 文件的格式化
  "[vue]": {
    "editor.defaultFormatter": "Vue.volar"
  },

  // json、yaml 等配置文件保存时自动格式化
  "[json]": {
    "editor.formatOnSave": true
  },
  "[jsonc]": {
    "editor.formatOnSave": true
  },
  "[yaml]": {
    "editor.formatOnSave": true
  }
}

fix-code-on-save.gif

format-json-on-save.gif

最后,为了更好地与 Lint 插件配合,我们再补充一些 IDE 文本格式相关的配置。

// .vscode/settings.json
{
  // 已有配置...

  // 行尾默认为 LF 换行符而非 CRLF
  "files.eol": "\n",

  // 键入 Tab 时插入空格而非 \t
  "editor.insertSpaces": true,

  // 缩进大小:2
  "editor.tabSize": 2,

  // 自动补充闭合的 HTML 标签
  "html.autoClosingTags": true,

  // 更多格式相关配置...
}

自此,我们的组件库项目在本地开发环境下,就有了从命令行到 IDE 的完善的代码规范能力。接下来只需根据已敲定的团队开发规范,在 .eslintrc.js 以及 .stylelintrc.jsrules 字段中设置规则集即可。

commitlint

除了代码规范之外,Git 提交信息的规范也是值得关注的,相比起随意的提交信息,规范的提交信息能带来以下好处:

  • 定位问题时,提供更加清晰的线索;
  • 团队协作时,降低理解他人代码的成本;
  • 配合一些 CI 自动化工具,能够一键生成清晰的版本更新记录 CHANGELOG

commitlint 工具可以检查 Git 提交信息是否符合规范。通过以下命令可以安装相关依赖。

pnpm i -wD @commitlint/config-conventional @commitlint/cli

之后在根目录创建 .commitlintrc.js,继承默认的 @commitlint/config-conventional 规范集:

// .commitlintrc.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
};

@commitlint/config-conventional 规定了这样的 Git 提交规范:

type(scope?): subject
  • type:本次提交的类型,默认规范集支持以下类型。
    • feat:添加新功能
    • fix:Bug 修复
    • build:构建相关的修改
    • chore:对项目功能没有影响的修改
    • ci:持续集成方面的修改
    • docs:文档的修改
    • perf:性能优化
    • refactor:代码重构
    • revert:代码回退
    • style:样式相关调整
    • test:测试相关代码
  • scope:本次提交涉及哪个子模块,此部分可不填。
  • subject:本次提交的描述信息。

如此配置后的 commitlint,要求我们的提交信息只能是以下形式:

feat(button): add click event.

fix(input): fix the error of v-model.

docs: add README.md for button.

而不能再随意提交:

# 不符合 type(scope?): subject 的格式,缺少 type。
add click event.

通过 husky 集成到 Git hooks 中

commitlint 是无法单独使用的。 因为提交信息发生在 git commit 阶段,而 git commit 时,控制台已经被占用,无法再容我们输入其他命令。

Git 提供了一个叫做 Git Hooks 的功能,它能让我们在特定的重要动作发生时触发自定义脚本。 按照这个说法,我们就可以使用 Git Hookscommit 动作发生的时候执行 commitlint 脚本,判断所提交的信息是否符合规范。Git Hooks 中的 commit-msg 钩子就正好符合我们的需求。

如果需要详细了解 Git Hooks 可以阅读以下文章: 一文带你彻底学会 Git Hooks 配置

当然,直接使用 Git Hooks 会存在着诸多的不便,例如直接在 .git/hooks 中创建的 Git Hooks 脚本是无法上传到远程仓库的(因为 .git 目录不能入仓)。我们使用一款工具 husky 来让 Git 钩子的配置变得更加简单。首先安装依赖项,并初始化 husky

pnpm i -wD husky

npx husky install

执行完初始化命令后,我根目录下出现了 .husky 目录,所有与 Git Hooks 相关的配置后续都会在其中。husky 官方推荐我们将 husky install 命令设置为项目启动前脚本——将 husky install 放入 package.json 中的 scripts.prepare 中,使得每次完成依赖安装后都会执行 husky 的初始化。

// package.json
{
  // 其他配置...
  scripts: {
+   "prepare": "husky install",
    // 其他脚本...
  }
}

接下来我们开始落实 commit-msg 钩子,先执行命令,生成 .husky/commit-msg 文件后,再将执行 commitlint 的命令写入其中:

npx husky add .husky/commit-msg

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

-undefined
+npx --no -- commitlint -e $HUSKY_GIT_PARAMS

到此,commitlint 的集成算是正式完成,可以看到我们的工程阻止了不符合规范的提交信息。

commitlint.gif

lint-staged 实现增量检查

到目前为止,我们所配的 ESLintStylelint 实现的都是全量检查。我们的组件库作为一个新的项目,可以接受全量检查,但是对于很多大项目而言,全量检查的代码规范是无法落地的,存在以下问题:

  • 项目体积过大,全量检查需要扫描的文件过多,导致检查花费的时间太多。如果这样的检查集成到了 CI 门禁中,将会大大降低构建效率。
  • 项目历史有太多不规范的技术债,全量检查扫描出的问题过多,若要集成到 CI 门禁中,将使团队面临巨大的修改工作量和代码变更带来的风险。

为了让 Lint 检查能够在大多数情况下平滑地集成到项目中,我们需要实现增量检查。而 lint-staged 就适用于这样的场景。

lint-staged 包含一个可以执行任意 shell 任务的脚本,在执行脚本时以 暂存区 的文件列表作为参数,并支持按 glob 模式进行过滤。

这里的这个暂存区可能有些难以理解,我们可以暂时这样简单地理解:在 git commit 之前我们往往要先执行 git addgit add 的作用就是将工作区的文件变化推送到暂存区(stage),以便下一步通过 git commit 将暂存区的内容推送到远端版本库。lint-staged 也正如其名,它的目标就是这些暂存区的文件。

如果你想更多地理解暂存区的概念,这里推荐一些博文:

Git三大特色之Stage(暂存区)

深入学习之前先理解 git 暂存区

当然,如果你对 Git 也不太了解,这里也推荐一个交互式的 Git 学习网站:

Learn Git Branching

首先我们安装 lint-staged

pnpm i -wD lint-staged

然后在根目录创建配置文件 .lintstagedrc.jslint-staged 的灵活性就在于支持通过 glob 模式匹配对暂存区的文件列表进行分类过滤,以实现对不同的文件应用不同检查的效果。

// .lintstagedrc.js
module.exports = {
  // 对于 js、ts 脚本文件,应用 eslint
  '**/*.{js,jsx,tsx,ts}': [
    'eslint --fix',
  ],
  // 对于 css scss 文件,应用 stylelint
  '**/*.{scss,css}': [
    'stylelint --fix',
  ],
  // Vue 文件由于同时包含模板、样式、脚本,因此 eslint、stylelint 都要使用
  '**/*.vue': [
    'eslint --fix',
    'stylelint --fix',
  ],
  // 对于其他类型的文件,用 prettier 修复格式
  '**/*.{html,json,md}': [
    'prettier --write',
  ],
};

由于 lint-staged 要处理的是缓冲区中的变化文件,所以我们要利用 Git Hookspre-commit 这个钩子,就能够实现在 commit 发生之前对变化(增量)的文件进行 Lint 扫描,若 Lint 抛出错误,说明此次准备提交的文件存在代码规范的问题,提交失败。这需要我们再次用到 husky

npx husky add .husky/pre-commit

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

-undefined
+npx --no -- lint-staged

接下来我随意修改了几个文件(脚本、样式、vue 都涉及到),这几个文件都存在 lint 错误,执行一次代码提交,看看会发生什么。

change-stage.png

lint-staged-error.gif

在修复了所有问题后,这些文件才能通过 pre-commit 钩子中 lint-staged 的检查,被成功提交。如此,我们本地提交代码规范的增量检查门禁便成型了。

lint-staged-success.gif

增量检查进阶:lint-staged --diff

lint-staged 的强大之处在于它并不是只支持对缓冲区内文件变化的读取,它可以通过 --diff 指令对比工作区与远程版本库的差异,将差异文件列表作为参数执行 Lint 脚本。这一特性可以被利用起来,实现流水线门禁中的增量检查。

由于本章不涉及持续集成相关的实践,因此只是在本地环境模拟演示一下这种场景,若想进一步了解增量检查在流水线中的应用,请关注本系列后续的更新。

我们设想这样一个场景:

  • 我们从 master 分支拉出一个 feat 分支用于新特性的开发。
  • feat 分支开发完成后,需要通过 merge request / pull request 合入 master 分支。
  • 在这次 merge request / pull request 的门禁中,我们需要确认 feat 分支合入 master 分支后,发生变化的文件是否有不合规范的现象。

下面展示操作流程:

我们先确保 master 分支的代码已经集成了所有 Lint 工具链(若 master 分支进度落后需要及时合并最新分支),以 master 分支为基础创建 feat 分支:

git checkout -b feat

feat 分支中,我们改动一部分文件(5 个文件:md、json、ts、scss、vue 文件各一个),之后提交代码。

feat-change.png

git add .
git commit -m 'chore: test lint-staged diff'
git push --set-upstream origin feat

现在我们切换回 master,准备模拟一个 merge request / pull request

git checkout master

目前,远端的 origin/feat 要领先于工作区的 master 分支,工作区 master 分支与远端的 origin/master 分支保持一致。在合入远端分支 origin/feat 后,工作区的 master 分支与远端的 origin/feat 一致,领先于远端的 origin/master

git merge origin/feat

# 结果
Updating a3c6c8d..12be1a3
Fast-forward
 README.md                       | 7 ++++++-
 packages/button/src/button.scss | 1 +
 packages/button/src/button.vue  | 1 +
 packages/button/src/index.ts    | 1 +
 tsconfig.json                   | 5 +++--
 5 files changed, 12 insertions(+), 3 deletions(-)

这个时候,对工作区的 master 和远端的 origin/master 进行 diff,就可以比较出两者之间的文件差异。这个差异正是我们之前在 feat 分支上的变更。

git diff origin/master --name-only

# 结果
README.md
packages/button/src/button.scss
packages/button/src/button.vue
packages/button/src/index.ts
tsconfig.json

那么,我们借助 lint-staged -diff 指令,就可以实现对这部分变化文件的 Lint 检查。

npx lint-staged --diff=origin/master

lint-staged-diff.gif

虽然这部分实践没办法立即受用,但是它为后续持续集成时的 merge request / pull request 门禁实现增量检查打下了基础, 日后我们会再次回顾。最后我们将 lint-staged 指令也加入到 package.json 中。

// package.json
{
  // 其他配置...
  scripts: {
+   "lint-staged": "lint-staged",
    // 其他脚本...
  }
}

结尾与资料汇总

在文章的结尾,让我们来集中梳理一下本章的思路:

  1. 我们首先简单地了解了 ESLint 的功用和原理。ESLint 是对 ECMAScript 代码进行规范化的工具。它通过 parser 解析代码;通过 plugin 聚合多条检查规则的实现逻辑;通过 rules 开启 / 关闭 / 配置 各条检查规则;通过 extends 继承配置预设。
  2. 我们以 Airbnb 规则集为基础,添加了许多周边插件使 ESLint 支持了对 vueTypeScriptimport 语法的支持,最终确定了基本的 .eslintrc.js 配置文件。
  3. StylelintESLint 原理类似,我们也按照类似的方式确定了基本的 .stylelintrc.js 配置文件。我们配好的 Stylelint 具有识别 vue、识别 sass、对 css 属性自动排序的能力。
  4. 对于 Prettier,我们分析了其优劣,不准备将其集成到 ESLintStylelint 中,只是单独负责 jsonmd 等其他文件的处理。
  5. 我们安装了 ESLintStylelintPrettier 相关的 IDE 插件,并做了许多插件相关的编辑器配置,实现了代码规范与 IDE 的高度结合,使 Lint 工具的使用体验得到飞跃。
  6. commitlinthusky 配合使用,可以使我们的 Git 提交信息规范化,不合规的提交信息将在 Git Hookscommit-msg 钩子中被拦截。
  7. 我们尝试使用 lint-staged 工具,配合 husky,在 Git Hookspre-commit 钩子中只对暂存区的代码进行 Lint 检查,实现了本地提交代码规范增量检查门禁。
  8. 进一步探索 lint-staged,发现其 --diff 选项可以获取任意两个分支之间的文件变化列表,由此初步确定了代码合并的场景下进行代码规范增量检查的方案。

本章涉及到的相关资料汇总如下:

官网与文档:

ESLint

Stylelint

Prettier

commitlint

husky

lint-staged

typescript-eslint

vue-eslint-parser

eslint-config-vue

eslint-config-import

eslint-define-config

Airbnb TypeScript 规则集

腾讯 AlloyTeam 的规则集

StandardJS 规则集

stylelint-config-standard

stylelint-config-standard-scss

stylelint-config-standard-less

stylelint-config-recommended-vue

stylelint-config-recess-order

stylelint-stylistic

VSCode 插件 ESLint

VSCode 插件 Stylelint

VSCode 插件 Prettier

Git Hooks

Git 暂存区介绍

分享博文:

为什么我不使用 Prettier

你不能再说你不会配置ESLint和prettier了

一文带你彻底学会 Git Hooks 配置

Git三大特色之Stage(暂存区)

深入学习之前先理解 git 暂存区

Learn Git Branching - 最好的 Git 交互式学习平台