本文正在参加「金石计划 . 分割6万现金大奖」

在计算机科学中,lint是一种东西程序的名称,它用来符号源代码中,某些可疑的、不具结构性(可能造成bug)的段落。它是一种静态程序剖析东西,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描绘在任何一种计算机程序语言中,用来符号源代码中有疑义段落的东西。——来自维基百科

现状

ESLint 可能是开发中最常用的东西之一了,不过因为各种脚手架东西的盛行,虽然必定程度上提升了开发效率,可是却也将那些构建细节束之高阁。

假如此刻公司需求你从零开始建立一个完好的项目结构,为了标准团队的代码风格,ESLint 该怎么去装备呢?本篇就来说说怎么构建一个 ESLint + React 的实践,而且测验去了解 ESLint 背面不为人知的细节,而关于其他结构,比方 Vue 这种,构建的流程也是相似的。

重读 ESLint

先来看一张概念图来了解 ESLint 的组成:

正儿八经来学一学 ESLint

  • 自顶向下来看,最上面便是 ESLint 的相关生态,比方 Prettier、Husky 等等东西。

  • 再下面一层便是 ESLint 的运用,分为运用装备文件,和运用命令行以及通过 NodeJS API 来运用,这个和其他的构建东西,比方 Webpack、Rollup 是共同的。

  • 接着便是 ESLint 的中心,规矩以及插件,这一层很多人都知道它的重要性,可是却没有真正了解过。

  • 最底层便是 ESLint 处理代码的逻辑。Parser 解析器用来将 JS 代码解析成 AST,默许 ESLint 选用 ESPree 来解析 JS,也能够选用其他的解析器,可是必须契合 ESLint 的接口要求。假如是 TS 的代码,那么就需求能处理 TS 文件的解析器了。

  • Processor 是处理器,用来提取 JS 代码,或许对 JS 代码进行转换。一般处理器作为插件一个特点,暴露给外部运用。

关于开发者来说,装备、插件以及规矩才是需求要点了解的,下面就来看看这些熟悉而又生疏的概念。

ESLint 装备文件

先说装备文件,其格局有下面几种,依照 ESLint 读取装备文件的优先级次序排列:

正儿八经来学一学 ESLint

ESLint 主动去项目下寻找 .eslintrc.* 这种格局的装备文件,假如要运用其他格局的装备文件,则能够运用 --config 选项来指定该文件的途径:

eslint --config myConfig.js ./src

除此之外,比较风趣的一点便是,咱们知道在 json 文件中是不能有注释的,可是在 .eslintrc.json 文件中是能够写注释的,ESLint 会疏忽该注释。需求注意的是,package.json 文件中仍是不能写注释的

首要的装备文件接口类型如下:

interface ESLintConfig {
    root?: boolean; // 表明当前文件是否为根装备文件,用于多装备文件时
    parser?: string; // 解析器,默许为Espree,解析 AST 节点
    env?: Record<string, boolean>; // 代码运转环境
    parserOptions?: ParserOptions; // 解析装备
    extends?: string | string[]; // 承继其他的规矩
    plugins?: string[]; // 插件装备
    rules?: any[]; // 规矩装备
    processor: string; // 处理器,详细可看上一节的解说
    overrides?: any[]; // 针对不同文件,使用不同规矩
    globals: Record<string, boolean | 'off' | 'readonly' | 'readable' | 'writable' | 'writeable'>; // 设置全局变量
    ignorePatterns: string | string[] // 疏忽目录或文件,等同于运用.eslintignore
}

其间有几个选项是需求要点关注的。

首要 env 选项表明代码的运转环境,常用的如,浏览器和 NodeJS 以及 Jest 单元测试,这些环境中存在一些全局变量,比方 document、process 这种,假如运用这些变量,则需求装备 env 特点,否则运用它们 ESLint 会提示报错:该变量未界说。

除了装备 env 特点外,parserOptions 也是常用的装备,有几个特点需求关注一下。

ecmaVersion 特点 指定 JS 的版别,通过笔者重复测验,发现这个特点没什么效果,想比较而言,env 特点中的装备愈加重要。

sourceType 特点指定 JS 的模块类型,默许为 script 脚本,一般项目中则运用 module 值,表明模块化。

最终则是 ecmaFeatures 特点,其包括了 jsx 特点,用来启用 jsx。完好的装备类型如下:

interface ParserOptions {
    ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | "latest" | undefined;
    sourceType?: "script" | "module" | undefined;
    ecmaFeatures?: {
        globalReturn?: boolean | undefined;
        impliedStrict?: boolean | undefined;
        jsx?: boolean | undefined;
        experimentalObjectRestSpread?: boolean | undefined;
        [key: string]: any;
    } | undefined;
    [key: string]: any;
}

然后便是 extends 特点,经常会看到 extends 特点这么装备的:

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

这儿的 recommended 便是在 ESLint 的规矩中,界说的一个特点 recommended,值为 boolean 类型,为 true 表明 extends 会选用该规矩。鄙人一节还会说到该特点。除了 recommended 外,还能够装备 all:

{
    exntends: ['eslint:all']
}

这样便是选用所有的 ESLint 中的规矩,随着 ESLint 的版别迭代,某些规矩可能会有兼容问题,所以这种做法是不引荐的。

其他装备比较简单,就不再逐个赘述了,要点来看看怎么完成规矩和插件,它们之间又是什么关系呢?

ESLint 规矩

ESLint 中的规矩便是对书写代码做了必定的束缚,假如不依照这个束缚来,则 ESLint 中会陈述这个过错,而且假如存在修正过错的办法,也能够主动修正该过错,比方当咱们多加了空格,运用 --fix 的参数则能够修正该过错。

以 max-lines 规矩为例,其大约的结构如下:

正儿八经来学一学 ESLint

首要分为两个部分,meta 表明规矩的元数据信息,create 则是规矩验证的办法。

先从 meta 说起,type 表明规矩的类型,其界说如下

type Type = 'problem' | 'suggestion' | 'layout'

依照问题的优先级次序来了解,problem 表明不运用该规矩,代码可能会导致过错,suggestion 则表明运用该规矩会更好,layout 则是代码风格上的规矩,比方空格、缩进等。

docs 的类型如下:

interface Docs {
    description: string; // 规矩的描绘信息
    recommended: boolean; // 是否引荐运用该规矩,调配eslint:recommended运用
    url: string; // 指定文档的url链接
}

接着 schema,则是装备规矩的选项模式,ESLint 会依据这个模式来验证你填写的规矩选项是否正确。ESLint 运用 ajv这个库来验证 schema。

最终 meta 中的 message 则表明不契合规矩时的报错信息,调配规矩上下文目标 context.report 办法运用,打开 create 办法能够看到下面这句:

// ...省掉
message: {
    exceed: "File has too many lines ({{actual}}). Maximum allowed is {{max}}."
}
// ...省掉
create (context) {
    // ...省掉
    return {
        "Program:exit"() {
                let max = 300;
                // ...省掉
                if (lines.length > max) {
                    const loc = {
                        start: {
                            line: lines[max].lineNumber,
                            column: 0
                        },
                        end: {
                            line: sourceCode.lines.length,
                            column: sourceCode.lines[sourceCode.lines.length - 1].length
                        }
                    };
                    context.report({
                        loc,
                        // 指定对应哪条信息
                        messageId: "exceed",
                        // data 中的数据供给给上面 message 运用
                        data: {
                            max,
                            actual: lines.length
                        }
                    });
                }
            }
        };
    }
}

create 办法返回了一个目标,目标中的每个 key 都是 AST 中的节点,对节点做检查,发现不满足规矩,运用 report 来陈述过错。create 中的参数 context 是 ESLint 供给的上下文目标,其间包括了很多办法,详细能够检查 ESLint 的详细的文档。

上面说到过,能够通过 --fix 来修正过错,这能够通过设置 report 中的参数来完成:

context.report({
    node: node,
    message: "Missing semicolon",
    fix: function(fixer) {
        return fixer.insertTextAfter(node, ";");
    }
});

ESLint 插件

说完了规矩,来说说 ESLint 的另一个中心,插件。

ESLint 中的插件和其他库中的插件对比,比方 Vue 或许 Webpack 中的插件,后者的插件供给了扩展功用,而 ESLint 的插件更像是集成了上面所有装备的东西包,比方,能够在插件中界说规矩 rules,界说解析器 processors,也能够界说 parserOptions 等等。

ESLint 的插件通常命名都是 eslint-plugin-* 这种格局,运用这种格局,在装备文件中设置 plugins 特点时,能够省掉掉前缀的字符串。相似的还有 eslint-config-* 这样的 npm 包,比方 eslint-config-airbnb 这种,那么二者有什么差异呢?

先来看一个比较闻名的插件 eslint-plugin-react ,它最终导出的模块结构时这样的:

module.exports = {
    rules: ...,
    configs: {
        recommended: ...,
        all: ...,
        'jsx-runtime': ...
    }
}

能够看到插件便是对外暴露了一些规矩、装备目标。在 ESLint 中能够看到完好的插件特点如下:

interface Plugin {
    configs?: Record<string, ConfigData> | undefined;
    environments?: Record<string, Environment> | undefined;
    processors?: Record<string, Linter.Processor> | undefined;
    rules?: Record<string, ((...args: any[]) => any) | Rule.RuleModule> | undefined;
}

再来看看别的一个闻名的 ESLint 的装备库 eslint-config-standard,里边的入口文件是这样的:

module.exports = require('./eslintrc.json');

这样,两者的差异就一目了然,假如是关于一些自界说的环境,比方说 Vue、React 这样的环境,运用了 .vue h或许 .jsx .tsx 这样的文件,ESLint 是无法辨认的,所以要对这样的代码进行 Lint,就需求自界说环境,解析文件,规矩等。而假如是运用这些结构,都是详细的事务代码,其实运用 eslint-config-* 更合适,其间包括这些环境下的 ESLint 插件。

那怎么去构建一个 ESLint 的实践呢?

标准代码的实践

以一个 React + TypeScript 的项目为例,首要需求在 VSCode 中下载 ESLint 的插件,能够在写代码的时分,即时发现问题。

接下来,新建一个空项目,eslint-best-practice,选用 pnpm 来下载依赖,先安装 ESLint。然后新建一个 index.js 的文件。得到的目录如下:

正儿八经来学一学 ESLint

安装好今后发现 VSCode 弹出了一个过错提示:

正儿八经来学一学 ESLint

这儿是因为 VSCode 中的 ESLint 的插件更新,曾经的一些装备现在现已不支持了,在 VSCode 中找到 ESLint 的装备项,删掉关于其间相关装备就恢复正常了。

接着来写 .eslintrc.js 的装备文件,也能够用你喜欢的恣意上面说到的格局:

module.exports = {
 env: {
     browser: true,
     node: true,
     es6: true,
     commonjs: true,
     jest: true
 },
 plugin: ['react'],
 parserOptions: {
     sourceType: 'module',
     ecmaFeatures: {
         jsx: true
     }
 },
 extends: [
     'eslint:recommended',
     'plugin:react/recommended',
     'plugin:prettier/recommended',
     'plugin:react-hooks/recommended'
 ]
}

这样一个根据 React 项目的 ESLint 便根本装备好了,仅有缺少的便是关于 TS 的代码 Lint。关于 TS 的 ESLint 的规矩,目前运用的比较多的便是 @typescript-eslint/eslint-plugin 这个包。增加这个包的话,需求对装备文件做一些改动。

改动的原因,便是上面说到的 parser 解析器只能去解析 JS 文件,关于 TS 则无能为力,当然也能够挑选先把 TS 转换成 JS,再去使用 JS 的规矩,可是通过转换后的 JS 代码并不能实时提示编写代码时出现的过错,所以最好的实践,便是增加这个包来单独解析 TS 文件。

根据上面的装备文件,改动如下:

module.exports = {
 env: {
     browser: true,
     node: true,
     es6: true,
     commonjs: true,
     jest: true
 },
 plugins: ['react', '@typescript-eslint/eslint-plugin'],
 extends: [
     'eslint:recommended',
     'plugin:react/recommended',
     'plugin:prettier/recommended',
     'plugin:react-hooks/recommended',
     'plugin:@typescript-eslint/eslint-plugin/recommended'
 ]
}

这么一看,如同更简单了。因为插件 @typescript-eslint/eslint-plugin 中现已包括了 parserOptions 的装备,所以这儿就无需再去装备了,别的需求注意的是,除了安装这个插件,还需求安装 @typescript-eslint/parser 解析器的包。

大部分类型过错 TS 现已帮咱们解决了,所以你可能不想使用这么多的 @typescript-eslint/eslint=plugin 中的规矩,此刻能够挑选运用 overrides 装备,来对 TS、TSX 文件做单独的装备:

module.exports = {
    // ...省掉
    overrides: [
        files: ['*.ts', '.tsx'],
        rules: {
            '@typescript-eslint/no-semi': 'off',
            // ...其他规矩装备
        }
    ]
}

@typescript-eslint 中会有一些规矩,需求配合 TS 的装备文件,否则就会出现如下的过错:

正儿八经来学一学 ESLint

依照这个过错提示,增加 parserOptions.project 的装备即可让规矩收效:

module.exports = {
    // ...省掉其他
    parserOptions: {
        project: './tsconfig.json'
    }
}

综上能够知道,ESLint 的装备灵敏多变,没有哪种装备能够一次装备处处运用的,需求依据自己的需求,装备如下几个要害特点,env、plugins、extends、parserOptions、overrides等,即能够到达适合自己或团队的最佳 ESLint 的装备。

其他东西

假如想要保证在前端团队中所有的代码风格共同,除了 ESLint 外,还需求一些其他东西调配运用,才干更好的保证代码风格的共同性。

prettier 代码格局化

prettier 是一个和 ESLint 相似的东西,仅仅 prettier 更趋向于代码风格的规矩:比方缩进多少、运用tab、仍是空格等等。他仅仅让代码保持共同的风格,并不能起到如 ESLint 这种发现过错、纠正过错的效果。

一般首要的装备项便是以下几项,至于详细运用哪种风格,仍是要看自己的团队:

{
  "arrowParens": "always", // 箭头函数参数只有一个时,也需求增加括号
  "semi": true, // 句子分号
  "singleQuote": true, // 启用单引号
  "jsxSingleQuote": false, // 封闭jsx中运用单引号
  "printWidth": 100, // 每一行代码的长度,超出会换行
  "useTabs": false, // 不运用tab缩进
  "tabWidth": 2, // tab缩进为2个字符
  "trailingComma": "es5" // 目标、数组等结束的逗号是否增加,es5是需求增加结束逗号
}

style lint

别的项目中除了 JS/TS 文件外,还有大量的款式文件,相同也需求保证风格共同。关于款式文件,能够运用 stylelint 东西,它和 ESLint 的装备是相似的,只需求承继一些常用的装备即可:

{
  "extends": ["stylelint-config-standard", "stylelint-config-prettier"], // 常用的根底装备,这样够用了
  "customSyntax": "postcss-less", // 在代码中运用自界说的语法,比方less、scss、CSS in JS这种css语法验证
  "rules": {
    // ...依照团队的需求
  }
}

editorconfig

editorconfig 也是代码风格束缚的东西,调配 VSCode 的扩展插件 EditConfig for VSCode,首要是针对 IDE 东西,能够在编写代码的时分,就能保证共同良好的代码风格,而 prettier 这样的东西,是在执行命令今后才干格局化代码,不能在实时编写时,就保持共同的风格。这也是 editorconfig 和 prettier 的最大的差异。

该东西装备都是迥然不同的,所以项目中用到的话,直接仿制就能够了。在项目的根目录下新建一个 .editorconfig 文件,内容如下:

root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
max_line_length = 100
trim_trailing_whitespace = true

装备很好了解,键值对方式的装备项,[*]表明对所有类型的文件都会使用这个装备,注意,需求下载了上面说到的扩展插件,才干在每次保存文件的时分主动格局化代码。

至此,一个风格共同的项目代码标准就完成了,最终能够增加一个脚原本执行代码 Lint:

prettier . --write && eslint . --fix && stylelint **/*.less

除了这些,也能够挑选运用 Husky 这样的东西,在提交代码前进行格局化。

综上所述,这儿运用了 ESLint + stylelint + prettier 来做代码检查,为了方便其他项目运用,能够利用 ESLint 的共享装备,将这些功用都会集到一个 npm 包中,像是上面说到的 eslint-config-standard 包。

能够创立一个如下结构的项目:

正儿八经来学一学 ESLint

在 index.js 文件中导出 eslint 的装备:

module.exports = require('./eslintrc.js');

这样项目中的 ESLint 装备就能够直接承继该装备了。关于 stylelint、prettier 等则能够在项目中创立一个同名文件,文件中引入对应的装备文件即可,像这样:

module.exports = require('./styleintrc.js');
module.exports = require('./prettierrc.js');

别忘了最终在 package.json 文件中的 main 特点,增加 index.js 文件的途径。

今后无论是哪个项目都能够引证同一个 ESLint 以及其他的格局化装备规矩,到达所有项目,代码风格共同,减少初级的代码过错的目的。

尾声

ESLint 还有很多没有深化的地方,比方整个 ESLint 的架构,分为 cli-engine,init 等等模块,任何一个模块深化研究下去都足够学习很久。

最终总结一下:虽然结构的飞速发展,给开发带来了极大的便当,可是也不要忘掉隐藏在结构、标准背面的技术细节,这些细节才是拉开前端差距的要害。