前端代码格式化与校验

1,290 阅读15分钟

前言

虽然我们团队已经制定了统一的前端代码规范,并且也输出了相应的技术规范文档。但是,即便有这些文档也很难确保我们今后就能写出符合团队规范的代码。所以,我们还需要在实际项目中引入一些第三方库和插件,来对我们的项目代码进行约束,统一代码风格,并能够提供对一部分问题代码进行自动修复的能力。

下面会给大家介绍一下这些常用的第三方库和他们的工作原理,并且要如何在我们的项目中对它们进行整合。


ESLint

ESLint 是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具。因为JavaScript是一门动态的弱类型语言,缺少编译过程,有些本可以在编译过程中就发现的错误,只能等到运行时才发现。ESLint 相当于为语言增加了编译过程,在代码运行前进行静态分析,能够及时找到出错的地方,帮助你写出更加健壮的代码。


ESLint 运行原理

ESLint 默认使用 espree 解析器将 JavaScript 代码解析成一个 AST(抽象语法树),然后通过深度遍历 AST 的所有节点,会触发监听了对应节点的自定义规则的回调函数。

JavsScript 解析成 AST 的结果可以在 astexplorer 网站查看。


ESLint 配置

ESLint 可以通过 JavaScript 注释或者在项目中指定配置文件的方式来进行配置。

配置文件优先级:

  1. .eslintrc.js
  2. .eslintrc.yaml
  3. .eslintrc.yml
  4. .eslintrc.json
  5. .eslintrc
  6. package.json 中的 eslintConfig 字段

常用的配置参数:

// .eslintrc.js
module.exports = {
  env: {},
  globals: {},
  parser: 'espree',
  parserOptions: {},
  plugins: [],
  extends: [],
  rules: {},
  overrides: {}
};

env (Environments)

指定脚本的运行环境。每种环境都有一组特定的预定义全局变量。例如:配置 env: { node: true },就可以正确读取 node 环境下的 globalprocess 等全局变量,否则 eslint 会提示变量未定义。

你也可以通过注释的方式来指定环境:

/* eslint-env node, mocha */

常用的环境变量

  • browser - 浏览器环境中的全局变量
  • node - Node.js 全局变量和 Node.js 作用域
  • es6, es2020, es2021等 - 启用 ES6+的新特性
  • jquery - jQuery 全局变量

globals (全局变量)

配置全局变量,key 值为对应的变量名称。值设置为 writable 表示允许重写变量,readonly 表示不允许重写变量。

{
  globals: {
    var1: 'writable',
    var2: 'readonly'
  }
}

你也可以通过注释的方式来指定全局变量:

/* global var1:writable, var2:readonly */

parser (解析器)

ESLint 默认使用 Espree 作为其解析器,你也可以指定一个不同的解析器。解析器的作用是将源代码解析成 AST (抽象语法树),可以更方便地进行代码规则校验以及代码转换。


parserOptions(解析器配置)

默认情况下,ESLint 支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持。

可用的选项有:

  • ecmaVersion - 指定使用的 ECMAScript 版本
  • sourceType - 默认值为 script,如果代码是 ECMAScript 模块,可以指定为 module
  • ecmaFeatures 额外的语言特性:
    • globalReturn - 允许在全局作用域下使用 return 语句
    • impliedStrict - 启用全局 strict mode (如果 ecmaVersion 是 5 或更高)
    • jsx - 启用 JSX

plugins (插件)

默认情况下,ESLint 中的规则只会对 JS 进行校验,如果我们想对 Vue 进行校验的话,就需要增加 eslint-plugin-vue 插件。插件一般通过 npm 安装,在配置插件时,可以使用 plugins 关键字来存放插件的列表,插件名称可以省略 eslint-plugin- 前缀。

{
  plugins: ['vue'];
}

extends (扩展)

ESLint 中的规则很多,但是默认都不会自动开启,我们需要在 rules 中设定这些规则开关,这个环节非常繁琐。因此 ESLint 设计了 extends 这个字段,用于继承预先定义好的校验规则,一般通过 npm 安装。例如:eslint-config-prettier,扩展名称可以省略 eslint-config- 前缀。

{
  extends: ['eslint:recommended', 'prettier']
}

内置的扩展:

  • eslint:recommended - eslint 推荐的扩展内容,报告了一些常见的问题,具体内容可以查看在 规则页面 中被标记为 √ 的规则。
  • eslint:all - 启用当前安装的 eslint 中所有的核心规则,不推荐使用。

rules (规则)

ESLint 附带大量的校验规则,要改变一个规则设置,必须将规则 ID 设置为下列值之一:

  • 'off'0 - 关闭规则
  • 'warn'1 - 开启规则,使用警告级别的错误(不会导致程序退出)
  • 'error'2 - 开启规则,使用错误级别的错误(当被触发时,程序会退出)
{
  rules: {
    curly: 'error',
    quotes: ['error', 'single'],
    // 插件规则
    'plugin1/rule1': 'error'
  }
}

你也可以使用注释的方式来指定规则:

/* eslint quotes: ["error", "double"], curly: "error" */

或使用块注释临时禁止规则出现警告:

/* eslint-disable */ 或; /* eslint-disable no-console */
console.log('foo');

/* eslint-disable-next-line */ 或; /* eslint-disable-next-line no-console */
console.log('foo');

overrides (覆盖)

一般用于覆盖指定文件校验规则。

{
  overrides: [
    {
      files: ['bin/*.js'],
      rules: {
        quotes: ['error', 'single']
      }
    }
  ];
}

ESLint 项目整合

这里演示的是如何在 vue 项目中整合 eslint,在使用 vue-cli 脚手架初始化项目(vue create)时需要勾选 babel 和 eslint,如果没有的话需要在项目中执行 vue add babelvue add eslint

注意:这里使用的脚手架 @vue/cli 版本号是 5.x,他在初始化项目时会自动帮我们安装 core-js @babel/core,@babel/eslint-parser,@vue/cli-plugin-babel,@vue/cli-plugin-eslint,eslint 和 eslint-plugin-vue 等依赖。如果 @vue/cli 是 4.x 版本安装的则是 babel-eslint ,该依赖已经被 @babel/eslint-parser 所替代,并且已经停止维护了。


1. 安装依赖

npm install -D eslint@7 eslint-config-huaer

2. 配置 eslint

// .eslintrc.js
module.exports = {
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  // 这里使用了 eslint-config-huaer 和 eslint-plugin-vue 的扩展
  extends: ['huaer', 'plugin:vue/recommended']
};

3. 新增 .eslintignore 文件

dist/*
node_modules/*
public/*

4. 安装插件

想要在项目中直接标记和显示错误信息,还需要在 vscode 的扩展中安装 eslint 插件。

ESLint


相关依赖说明

@babel/eslint-parser

用于替代 babel-eslint(已不再维护),作用是使 JS 能够使用一些还处于实验性质的语法,例如:装饰器等,也可以作为 Typescript 的 解析器来使用。


@vue/cli-plugin-eslint

如果使用 vue-cli 构建项目时选择了 eslint 选项,那么 cli 会安装 @vue/cli-plugin-eslint,这个包会向 vue-cli-service 注入 lint 命令,可以在 vue.config.js 中配置 lintOnSave 字段决定是否在保存修改时执行规则检测并修复部分检测不通过的代码。


eslint-config-huaer

为公司项目定制的 eslint 扩展,类似于 eslint-config-airbnb 和 eslint-config-standard 等,里面包含了一系列符合我们团队规范的 rules 规则,并且关闭了所有与格式相关的规则,只用 ESLint 来检查它更擅长的逻辑错误。


eslint-plugin-vue

Vue 官方提供的 eslint 插件,对 vue 组件文件制定了一系列推荐的规范。


Stylelint

StyleLint 是一个强大的、现代化的 CSS 检测工具, 与 ESLint 类似, 它通过定义一系列的编码风格规则帮助我们避免在书写样式时出现错误。


Stylelint 配置

Stylelint 通过 配置文件 来完成查找和加载你的配置对象,从当前工作目录开始,将按以下顺序查找尽可能的来源:

  • package.json 中的 stylelint 属性
  • .stylelintrc 文件(可以是 JSON,YAML 或 JS 格式的)
  • stylelint.config.js 文件

一旦发现它们中的任何一个,将不再继续进行查找。


Stylelint 项目整合

Stylelint 的配置参数与 ESLint 比较类似,因为篇幅原因不再做过多的描述,感兴趣的可以查看 官方配置,这里直接说明要如何在我们的项目中整合 Stylelint。


1. 安装依赖

npm install -D stylelint@13 stylelint-config-standard@22 stylelint-order

注意:stylelint 需要安装 13.x 版本, stylelint-config-standard 需要安装 22.x 版本,否则可能会出现一些其他问题


2. 配置 stylelint

// .stylelintrc.js
module.exports = {
  extends: ['stylelint-config-standard'],
  plugins: ['stylelint-order'],
  rules: {
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['global']
      }
    ],
    'selector-pseudo-element-no-unknown': [
      true,
      {
        ignorePseudoElements: ['v-deep']
      }
    ],
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: [
          'tailwind',
          'apply',
          'variants',
          'responsive',
          'screen',
          'function',
          'if',
          'each',
          'include',
          'mixin'
        ]
      }
    ],
    'no-empty-source': null,
    'named-grid-areas-no-invalid': null,
    'unicode-bom': 'never',
    'no-descending-specificity': null,
    'font-family-no-missing-generic-family-keyword': null,
    'declaration-colon-space-after': 'always-single-line',
    'declaration-colon-space-before': 'never',
    'rule-empty-line-before': [
      'always',
      {
        ignore: ['after-comment', 'first-nested']
      }
    ],
    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
    'order/order': ['custom-properties', 'declarations'],
    'order/properties-order': [
      /* 定位布局 */
      'position',
      'top',
      'right',
      'bottom',
      'left',
      'z-index',
      'float',
      'clear',
      'columns',
      'columns-width',
      'columns-count',
      'column-rule',
      'column-rule-width',
      'column-rule-style',
      'column-rule-color',
      'column-fill',
      'column-span',
      'column-gap',
      'display',
      'grid',
      'grid-template-rows',
      'grid-template-columns',
      'grid-template-areas',
      'grid-auto-rows',
      'grid-auto-columns',
      'grid-auto-flow',
      'grid-column-gap',
      'grid-row-gap',
      'grid-template',
      'grid-template-rows',
      'grid-template-columns',
      'grid-template-areas',
      'grid-gap',
      'grid-row-gap',
      'grid-column-gap',
      'grid-area',
      'grid-row-start',
      'grid-row-end',
      'grid-column-start',
      'grid-column-end',
      'grid-column',
      'grid-column-start',
      'grid-column-end',
      'grid-row',
      'grid-row-start',
      'grid-row-end',
      'flex',
      'flex-grow',
      'flex-shrink',
      'flex-basis',
      'flex-flow',
      'flex-direction',
      'flex-wrap',
      'justify-content',
      'align-content',
      'align-items',
      'align-self',
      'order',
      'visibility',
      'overflow',
      'overflow-x',
      'overflow-y',
      'clip',
      'zoom',
      'table-layout',
      'empty-cells',
      'caption-side',
      'border-spacing',
      'border-collapse',
      'list-style',
      'list-style-position',
      'list-style-type',
      'list-style-image',
      '-webkit-box-orient',
      '-webkit-box-direction',
      '-webkit-box-decoration-break',
      '-webkit-box-pack',
      '-webkit-box-align',
      '-webkit-box-flex',
      /* 盒模型 */
      'margin',
      'margin-top',
      'margin-right',
      'margin-bottom',
      'margin-left',
      '-webkit-box-sizing',
      '-moz-box-sizing',
      'box-sizing',
      'border',
      'border-width',
      'border-style',
      'border-color',
      'border-top',
      'border-top-width',
      'border-top-style',
      'border-top-color',
      'border-right',
      'border-right-width',
      'border-right-style',
      'border-right-color',
      'border-bottom',
      'border-bottom-width',
      'border-bottom-style',
      'border-bottom-color',
      'border-left',
      'border-left-width',
      'border-left-style',
      'border-left-color',
      '-webkit-border-radius',
      '-moz-border-radius',
      'border-radius',
      '-webkit-border-top-left-radius',
      '-moz-border-radius-topleft',
      'border-top-left-radius',
      '-webkit-border-top-right-radius',
      '-moz-border-radius-topright',
      'border-top-right-radius',
      '-webkit-border-bottom-right-radius',
      '-moz-border-radius-bottomright',
      'border-bottom-right-radius',
      '-webkit-border-bottom-left-radius',
      '-moz-border-radius-bottomleft',
      'border-bottom-left-radius',
      '-webkit-border-image',
      '-moz-border-image',
      '-o-border-image',
      'border-image',
      '-webkit-border-image-source',
      '-moz-border-image-source',
      '-o-border-image-source',
      'border-image-source',
      '-webkit-border-image-slice',
      '-moz-border-image-slice',
      '-o-border-image-slice',
      'border-image-slice',
      '-webkit-border-image-width',
      '-moz-border-image-width',
      '-o-border-image-width',
      'border-image-width',
      '-webkit-border-image-outset',
      '-moz-border-image-outset',
      '-o-border-image-outset',
      'border-image-outset',
      '-webkit-border-image-repeat',
      '-moz-border-image-repeat',
      '-o-border-image-repeat',
      'border-image-repeat',
      'padding',
      'padding-top',
      'padding-right',
      'padding-bottom',
      'padding-left',
      'width',
      'min-width',
      'max-width',
      'height',
      'min-height',
      'max-height',
      /* 文字排版 */
      'font',
      'font-family',
      'font-size',
      'font-weight',
      'font-style',
      'font-variant',
      'font-size-adjust',
      'font-stretch',
      'font-effect',
      'font-emphasize',
      'font-emphasize-position',
      'font-emphasize-style',
      'font-smooth',
      'line-height',
      'text-align',
      '-webkit-text-align-last',
      '-moz-text-align-last',
      '-ms-text-align-last',
      'text-align-last',
      'vertical-align',
      'white-space',
      'text-decoration',
      'text-emphasis',
      'text-emphasis-color',
      'text-emphasis-style',
      'text-emphasis-position',
      'text-indent',
      '-ms-text-justify',
      'text-justify',
      'letter-spacing',
      'word-spacing',
      '-ms-writing-mode',
      'text-outline',
      'text-transform',
      'text-wrap',
      '-ms-text-overflow',
      'text-overflow',
      'text-overflow-ellipsis',
      'text-overflow-mode',
      '-ms-word-wrap',
      'word-wrap',
      '-ms-word-break',
      'word-break',
      /* 视觉效果 */
      'color',
      'background',
      'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader',
      'background-color',
      'background-image',
      'background-repeat',
      'background-attachment',
      'background-position',
      '-ms-background-position-x',
      'background-position-x',
      '-ms-background-position-y',
      'background-position-y',
      '-webkit-background-clip',
      '-moz-background-clip',
      'background-clip',
      'background-origin',
      '-webkit-background-size',
      '-moz-background-size',
      '-o-background-size',
      'background-size',
      'outline',
      'outline-width',
      'outline-style',
      'outline-color',
      'outline-offset',
      'opacity',
      'filter:progid:DXImageTransform.Microsoft.Alpha(Opacity',
      "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
      '-ms-interpolation-mode',
      '-webkit-box-shadow',
      '-moz-box-shadow',
      'box-shadow',
      'filter:progid:DXImageTransform.Microsoft.gradient',
      "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
      'text-shadow',
      /* 过渡动画 */
      '-webkit-transition',
      '-moz-transition',
      '-ms-transition',
      '-o-transition',
      'transition',
      '-webkit-transition-delay',
      '-moz-transition-delay',
      '-ms-transition-delay',
      '-o-transition-delay',
      'transition-delay',
      '-webkit-transition-timing-function',
      '-moz-transition-timing-function',
      '-ms-transition-timing-function',
      '-o-transition-timing-function',
      'transition-timing-function',
      '-webkit-transition-duration',
      '-moz-transition-duration',
      '-ms-transition-duration',
      '-o-transition-duration',
      'transition-duration',
      '-webkit-transition-property',
      '-moz-transition-property',
      '-ms-transition-property',
      '-o-transition-property',
      'transition-property',
      '-webkit-transform',
      '-moz-transform',
      '-ms-transform',
      '-o-transform',
      'transform',
      '-webkit-transform-origin',
      '-moz-transform-origin',
      '-ms-transform-origin',
      '-o-transform-origin',
      'transform-origin',
      '-webkit-animation',
      '-moz-animation',
      '-ms-animation',
      '-o-animation',
      'animation',
      '-webkit-animation-name',
      '-moz-animation-name',
      '-ms-animation-name',
      '-o-animation-name',
      'animation-name',
      '-webkit-animation-duration',
      '-moz-animation-duration',
      '-ms-animation-duration',
      '-o-animation-duration',
      'animation-duration',
      '-webkit-animation-play-state',
      '-moz-animation-play-state',
      '-ms-animation-play-state',
      '-o-animation-play-state',
      'animation-play-state',
      '-webkit-animation-timing-function',
      '-moz-animation-timing-function',
      '-ms-animation-timing-function',
      '-o-animation-timing-function',
      'animation-timing-function',
      '-webkit-animation-delay',
      '-moz-animation-delay',
      '-ms-animation-delay',
      '-o-animation-delay',
      'animation-delay',
      '-webkit-animation-iteration-count',
      '-moz-animation-iteration-count',
      '-ms-animation-iteration-count',
      '-o-animation-iteration-count',
      'animation-iteration-count',
      '-webkit-animation-direction',
      '-moz-animation-direction',
      '-ms-animation-direction',
      '-o-animation-direction',
      'animation-direction',
      /* 杂项 */
      'content',
      'quotes',
      'counter-reset',
      'counter-increment',
      'resize',
      'cursor',
      '-webkit-user-select',
      '-moz-user-select',
      '-ms-user-select',
      'user-select',
      'nav-index',
      'nav-up',
      'nav-right',
      'nav-down',
      'nav-left',
      '-moz-tab-size',
      '-o-tab-size',
      'tab-size',
      '-webkit-hyphens',
      '-moz-hyphens',
      'hyphens',
      'pointer-events'
    ]
  },
  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts']
};

3. 新增 .stylelintignore 文件

dist/*
node_modules/*
public/*

4. 安装插件

想要在项目中直接标记和显示错误信息,还需要在 vscode 的扩展中安装 stylelint 插件。

Stylelint


相关依赖说明

stylelint-config-standard

stylelint-config-standard 是 stylelint 官方提供的扩展,内置了大量推荐的 CSS 相关规则。


stylelint-order

stylelint-order 是与 CSS 属性顺序相关的插件,支持自定义属性的书写顺序(例如:先写定位属性,再写盒模型),并且能够帮助你自动修正属性的顺序。


Prettier

Prettier 是一个代码格式化工具,它可以在我们按保存时自动帮我们格式化代码。其实 eslint 和 stylelint 已经内置了一些与代码格式相关的规则,那我们为什么还需要 Prettier 呢?因为 Prettier 不仅能识别 JS 和 CSS 文件,它还支持 JSON,MD 等多种格式。想要统一代码格式,Prettier 显然更加专业。


Prettier 项目整合

1. 安装依赖

npm install -D prettier eslint-config-prettier eslint-plugin-prettier stylelint-config-prettier

2. 配置 prettier

// .prettierrc.js
module.exports = {
  // 一行最多 120 字符
  printWidth: 120,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾不需要有逗号
  trailingComma: 'none',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 标签的反尖括号需要换行
  bracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // vue 文件中的 script 和 style 内需要缩进
  vueIndentScriptAndStyle: true,
  // 换行符使用 lf
  endOfLine: 'lf',
  // 格式化内嵌代码
  embeddedLanguageFormatting: 'auto'
};

3. 修改 eslint 和 stylelint 配置

为了防止 eslint 和 stylelint 的规则与 prettier 发生冲突,需要安装并配置 eslint-config-prettierstylelint-config-prettier

// .eslintrc.js
module.exports = {
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  extends: ['huaer', 'plugin:vue/recommended', 'prettier'],
  plugins: ['prettier'],
  rules: {
    'prettier/prettier': 'warn'
  }
};
// .stylelintrc.js
module.exports = {
  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
  plugins: ['stylelint-order'],
  rules: {
    ...
  }
}

4. vscode 设置

为了实现保存时自动格式化代码,还需要在项目根目录创建一个 .vscode 文件夹,并在它的内部新增 settings.json 配置文件,该文件的设置内容会覆盖 vscode 全局的设置。

配置内容:

// settings.json
{
  "files.eol": "\n",
  "editor.tabSize": 2,
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  "eslint.format.enable": true,
  "eslint.alwaysShowStatus": true,
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "html", "vue"],
  "stylelint.validate": ["css", "postcss", "less", "sass", "scss", "vue"],
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false
}

注意:.gitignore 文件需要移除 .vscode,为了做到项目统一设置,.vscode 文件也要一并上传


5. 安装插件

在 vscode 扩展中新增 prettier 插件。

Prettier


相关依赖说明

eslint-config-prettier

关闭了 ESLint 中一些不必要的规则以及可能与 Prettier 冲突的规则 。


eslint-plugin-prettier

以 ESLint 规则的方式运行 Prettier,通过 Prettier 找出格式化前后的差异,并以 ESLint 问题的方式报告差异,同时针对不同类型的差异提供不同的 ESLint fixer。


stylelint-config-prettier

关闭了 Stylelint 中一些不必要的规则以及可能与 Prettier 冲突的规则 。


husky 和 lint-staged

为了防止一些项目成员手动关闭校验或者因为没有安装相关插件导致提交了不规范的代码,我们还需要安装 husky 和 lint-staged,它们可以在 git commit 时对提交的代码进行拦截,实现自动格式化和校验,如果校验不通过则阻止此次的提交。


什么是 githooks

Git Hooks 就是 Git 在执行特定事件(如 commit、push、receive 等)时触发运行的脚本,类似于”钩子函数“,没有设置可执行的钩子时将被忽略。

初始化本地 git 仓库后,会自动在项目目录下生成 .git/hooks 文件夹,该文件夹内有一些以 .sample 结尾的钩子示例脚本,如果想启用对应的钩子,只需要手动删除文件的 .simple 后缀即可。

.git/hooks

但是,我们通常不手动去改动 .git/hooks 里的文件内容,因为 .git 文件一般不会上传到远程服务器,这样也无法做到团队规范的统一。


husky

husky 的作用就是会自动帮你在 .git/hoods 目录内生成对应的钩子文件和 shell 脚本,从而开启 githooks 的能力。


husky 的安装

npm install -D husky@4

这里安装的是 4.x 版本,新版本 husky 的配置过程会相对比较繁琐。


husky 的配置

你可以直接在 package.json 内写配置信息,也可以使用 .huskyrc.huskyrc.jshusky.config.js 文件进行配置。

  1. package.json 内新增配置内容
{
  "husky": {
    "hooks": {
      "pre-commit": "echo \"git commit trigger husky pre-commit hook\""
    }
  }
}
  1. .huskyrc.js 内进行配置
module.exports = {
  hooks: {
    'pre-commit': 'echo "git commit trigger husky pre-commit hook"'
  }
};

以上配置后会在 git commit 执行前在控制台打印一段信息。


lint-staged

在上传代码时,我们通常不需要对整个项目都执行 lint 检查,因为之前上传过的文件应该是符合 lint 规范的,我们只需要对当前新增或者修改的文件执行检查即可。lint-staged 的作用就是能够让 lint 工具只检查在暂存区(git add .)的文件。


lint-staged 的安装

npm install -D lint-staged

lint-staged 的配置

// package.json
{
  "lint-staged": {
    "src/**/*.{js,vue}": ["prettier --write .", "eslint --fix ."]
  }
}

这里 lint-staged 的配置是暂存区中的文件,只要是在 src 目录下的 js、vue 文件都执行 prettier 和 eslint 的命令。

配合 husky 提供的 pre-commit 钩子,就可以实现在 commit 提交前先进行代码格式化和检查,如果检查报错就自动拒绝 commit。


husky 和 lint-staged 的组合配置

// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.md": [
      "prettier --write"
    ],
    "*.{js,jsx,ts,tsx}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.{css,scss,less,styl}": [
      "prettier --write",
      "stylelint --fix"
    ],
    "*.vue": [
      "prettier --write",
      "eslint --fix",
      "stylelint --fix"
    ]
  },
  "devDependencies": {
    "eslint": "^8.9.0",
    "husky": "^4.3.8",
    "lint-staged": "^12.3.4",
    "prettier": "^2.5.1",
    "stylelint": "^13.13.1",
    ...
  }
}

常见问题

自动格式化失效

请检查 vscode 打开的是否是项目的根目录,因为编辑器默认读取的是当前目录下的配置信息。


参考资料