如何制定企业级代码规范与检查

2020-08-19 09:38:35 浏览数 (1)

开篇一张图

前言

如何作出项目的亮点?

  1. 项目中遇到了什么问题?
  2. 解决问题的过程并且如何思考?
  3. 思考之后通过什么方式解决
  4. 最后这一个任务你学到了什么,给团队带来了什么价值,解决了哪些痛点。

就从我的题目说起,本篇文章告诉你针对定制代码规范和检查这个小需求如何做出亮点?看完本文后回顾上面提到的 4 点,感觉下。

本文目标

目标不是一次全部定出来的,在实践和调研过程中会添加一些

  1. 去掉项目中原有的 TSLint ,统一使用 ESLint,但是在 ESLint 中加入 TSLint 检测插件
  2. Prettier 支持的格式化规则全部使用 Prettier,不提供的使用 ESLint,以免冲突(个人认为 Prettier 提供的格式化规则可以满足开发者)。
  3. 代码保存时,支持自动 fix,只对自己控制范围内的 fix,范围外的内容依靠开发者配置或 vscode 自动配置。
  4. 格式化和 ESLint 纳入项目级 git 跟踪,所有开发者统一。
  5. 除了上面的规范与检查实现,了解一些原理,比如 rules 原理?为什么 PrettierESLint 冲突?Prettier 原理?

ESLint

ESLint 的原理就是一款插件化的javascript代码静态检查工具,其核心是对代码解析得到的 AST (Abstract Syntax Tree 抽象语法树)进行模式匹配,定位不符合约定规范的代码。ESLint 是完全插件化的。每一个规则都是一个插件并且可以在运行时添加更多的规则。

社区比较知名的代码规范

  • eslint-config-airbnb
  • eslint-config-standard
  • eslint-config-alloy

如果想降低配置成本,可以直接接入上面的开源配置方案,好多开发者是继承它们的规范,然后在原有基础进行部分修改。我们目前选择的方式不是继承,挑选出了一些适合我们的 ESLint 规则(因为是在原有代码重新建立规范,防止改动过大)。

ESLint 集成

ESLint 使用并不复杂,简单说下 ESLint 的集成。

全局安装
代码语言:javascript复制
yarn add eslint -D
初始化
代码语言:javascript复制
eslint --init

这个时候在项目中会出现一个 .eslintrc.js 的文件。

eslint 自定义配置文件
代码语言:javascript复制
module.exports = {
  parser: {},  //定义ESLint的解析器
  extends: [], // 定义文件继承的子规范
  plugins: [], // 定义了该eslint文件所依赖的插件
  env: {},
  rules: {}    // 规则
};
parser

定义 parser 的解析器,我们常用的解析器应该是 @typescript-eslint/parser

env

通过 env 配置需要启动的环境

代码语言:javascript复制
 env: {
    es6: true, //  支持新的 ES6 全局变量,同时自动启用 ES6 语法支持
    node: true, // 启动 node 环境
    mocha: true,
  },
extend

extend 提供的是 eslint 现有规则的一系列预设。

这里注意的是,“extends”除了可以引入推荐规则,还可以以文件形式引入其它的自定义规则,然后在这些自定义规则的基础上用rules去定义个别规则,从而覆盖掉”extends”中引入的规则。

代码语言:javascript复制
{
    "extends": [
        "./node_modules/coding-standard/eslintDefaults.js",
        // Override eslintDefaults.js
        "./node_modules/coding-standard/.eslintrc-es6",
        // Override .eslintrc-es6
        "./node_modules/coding-standard/.eslintrc-jsx",
    ],
    "rules": {
        // Override any settings from the "parent" configuration
        "eqeqeq": "warn"
    }
}

除了在配置文件中指定规则外,还可以在代码中指定规则,代码文件内以注释配置的规则会覆盖配置文件里的规则,即优先级要更高。平时我们常用的就是 eslint-disable-next-line

忽略检查可以通过在项目目录下建立 .eslintignore 文件,并在其中配置忽略掉对哪些文件的检查。需要注意的是,不管你有没有在 .eslintignore 中进行配置,eslint 都会默认忽略掉对 /node_modules/** 的检查。也可以在 package.json 文件的 eslintIgnore 字段进行配置。

plugins

plugin 则提供了除预设之外的自定义规则,当你在 ESlint 的规则里找不到合适的的时候就可以借用插件来实现了

代码语言:javascript复制
module.exports = {
  parser: '@typescript-eslint/parser', // 解析器
  extends: [
    "./.eslintRules.js",
    'plugin:prettier/recommended',
    "prettier",// 优先 prettier 中的样式规范
    'prettier/@typescript-eslint',
  ], // 继承的规则
  plugins: ['@typescript-eslint'], // 插件

ESLint 重要特性

rules

rules 对应的规则,小伙伴可以去官网查看。找到符合自己项目的规则。

ESLint 规则官网地址

注意:
  1. 在整理总结规则的时候有些是自动检测的规则,就可以不用总结进去了。
  2. ESLint 规则的三种级别
  • "off"或者0,不启用这个规则
  • "warn"或者1,出现问题会有警告
  • "error"或者2,出现问题会报错
rules 工作原理`

首先来看看 eslin t源码中关于 rules 的编写。eslint 中的 rules源码存在于 lib/rules 下。每一个 rules 都是一个 node 模块,用 module.exports 导出一个 meta 对象及一个create 函数。

代码语言:javascript复制
module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

meta 代表了这条规则的 元数据,如这条规则的类别,文档,可接收的参数 schema 等等。create 返回一个对象,其中定义了一些在 AST 遍历访问到对应节点需要执行的方法等等。函数接受一个 context 对象作为参数,里面包含了例如可以报告错误或者警告的 context.report()、可以获取源代码的 context.getSourceCode() 等方法,可以简化规则的编写。

代码语言:javascript复制
function checkLastSegment (node) {
    // report problem for function if last code path segment is reachable
}

module.exports = {
    meta: { ... },
    create: function(context) {
        // declare the state of the rule
        return {
            ReturnStatement: function(node) {
                // 在AST从上向下遍历到ReturnStatement node 时执行
            },
            // 在AST 从下向上遍历到 function expression node 时执行:
            "FunctionExpression:exit": checkLastSegment,
            "ArrowFunctionExpression:exit": checkLastSegment,
            onCodePathStart: function (codePath, node) {
                // 在分析代码路径开始时执行
            },
            onCodePathEnd: function(codePath, node) {
                // 在分析代码路径结束时执行
            }
        };
    }
};

遍历 AST 的过程中会以“从上至下”再“从下至上”的顺序经过节点两次,selector 默认会在下行的过程中执行对应的访问函数,如果需要再上行的过程中执行,则需要添加:exit

TSLint 迁移到 ESLint 集成

背景

在这里会有读者问有现成的 TSLint 不用,为什么要迁移到 ESLint 中集成?

解答下:由于性能问题,TypeScript 官方决定全面采用 ESLint,甚至把仓库(Repository)作为测试平台,而 ESLintTypeScript 解析器也成为独立项目,专注解决双方兼容性问题。

JavaScript 代码检测工具 ESLintTypeScript 团队发布全面采用 ESLint 之后,发布typescript-eslint 项目,以集中解决TypeScriptESLint 兼容性问题。而 ESLint 不再维护 typescript-eslint-parser,也不会在 npm 上做任何发布。TypeScript 解析器转移至 Githubtypescript-eslint/parser

官方都放弃了我们也没必要太坚持,而且通过 ESLint 加上 ts 插件都可以完成检查

集成过程

首先安装依赖:

代码语言:javascript复制
yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

这两个依赖分别是:

  • @typescript-eslint/parserESLint 的解析器,用于解析 typescript,从而检查和规范 Typescript 代码。
  • @typescript-eslint/eslint-plugin:这是一个 ESLint 插件,包含了各类定义好的检测 Typescript 代码的规范。

安装好2个依赖之后,修改之前创建的.eslintrc.js文件,在该文件中加入 TSLint 配置。

代码语言:javascript复制
module.exports = {

    parser:  '@typescript-eslint/parser', //定义ESLint的解析器
    extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
    plugins: ['@typescript-eslint'],//定义了该 eslint 文件所依赖的插件
    env:{                        
        browser: true,
        node: true,
    },
    parserOptions: {
        parser: '@typescript-eslint/parser', // 解析 .ts 文件
        ecmaVersion: 2019,
        sourceType: 'module',
        ecmaFeatures: {
            modules: true,
        },
    },
}
  • typescript 项目中必须执行解析器为@typescript-eslint/parser,才能正确的检测和规范 typescript 代码
  • env 环境变量配置,形如 console 属性只有在 browser 环境下才会存在,如果没有设置支持 browser ,那么可能报 console is undefined 的错误。
  • 上面的配置中 extends 中定义了了文件继承的子规范,使用的 typescript-eslint 默认的推荐规范
  • parserOptions 解析器相关条件配置。

使用自定义的 typescript 规范

上面 extendsplugin:@typescript-eslint/recommended 使用的是插件默认推荐的 typescript 规范。但是会不会有同学不想使用推荐的规范,制定自己或者在推荐的规范中进行一些修改(比如一些老项目,加入规范,改动大,可能暂时忽略某些规范)

使用方式:如果想使用推荐,然后在推荐的基础上进行规范修改,可以直接在.eslintrc.js 文件中的rules对象中添加。

举个例子

代码语言:javascript复制
rules:{
    '@typescript-eslint/adjacent-overload-signatures': 2, // 要求成员重载是连续的
}

具体想修改那些自定义规范,可以去官网查看,这里给出官网地址。

  • TSLint rule 官网

Prettier

无法确定一个让所有人都满意的方案,就很难执行下去!

Prettier 中文的意思是漂亮的、美丽的,是一个流行的代码格式化的工具。

我们都知道 ESLint 本身就带有格式化检查的,我们为什么要是使用它?它有什么优点?使用它要注意那些问题?

优点

Perriter 官网列出几个特点:

  • An opinionated code formatter (译:固执己见的代码格式化程序)
  • Supports many languages(译:支持多种语言)
  • Integrates with most editors(译:与大多数编辑器集成)
  • Has few options(译:没有什么选择)

其中最核心的点是 opinionatedgoogle 翻译过来是固执己见的,在 Pertiter 中,就是说:你必须认同我的观点,按照我说的做。否则你就别用我,硬着头皮用就会处处不爽!

要解决的问题

  1. 使用 Prettier 如何避免与 ESLintTSLint 的格式化冲突?
  2. Prettier 中不提供的格式化规则,ESLint 中提供的可以兼容一起使用吗?

带着两个问题继续往下看

集成

安装模块包

我们来看如何结合 ESLint 来使用。首先我们需要安装三个依赖:

代码语言:javascript复制
yarn add prettier eslint-config-prettier eslint-plugin-prettier -D

对每个依赖进行说明:

  1. prettierPrettier插件的核心代码。
  2. eslint-config-prettier:解决ESLint中的样式规范和Prettier中样式规范的冲突,以 Prettier 的样式规范为准,使 ESLint 中的样式规范自动失效。
  3. eslint-plugin-prettier:将 prettier 作为 ESLint 规范来使用。
创建 .prettierrc 文件

在项目的根目录下创建 .prettierrc.js 文件

代码语言:javascript复制
module.exports =  {
    "printWidth": 120,
    "semi": false,
    "singleQuote": true,
    "trailingComma": "all",
    "bracketSpacing": false,
    "jsxBracketSameLine": true,
    "arrowParens": "avoid",
    "insertPragma": true,
    "tabWidth": 4,
    "useTabs": false  
  };

每个属性的含义可以去 Prettier 中查看。

修改 .eslintrc.js 文件,引入 Prettier

extends 中添加

代码语言:javascript复制
 extends:[ 
    './.eslintRules.js',
    'plugin:prettier/recommended',
    'prettier', // 优先 prettier 中的样式规范
    'prettier/@typescript-eslint',
    // 这里可以加一些prettier不支持,eslint支持的格式化规则,但是个人认为prettier的格式化规则够用了
    ],

关于 Prettier 配置时特殊说明(重点看下)

网上好多在 ESLint 中加入的 Prettier的文章,但是很少有讲清楚的,好多就是把配置文件写一下,然后很多小伙伴配置时候发现 Prettier 的格式化还是和 ESLint中的格式化冲突,ctrl s 保存的时候甚至出现来回切换格式的冲突,不知道小伙伴们遇到过这种情况没。

所以还是知道下原理,extends 中为什么那么写,格式冲突和顺序有什么关系没?

  1. eslint-config-prettier 源码可以看出,它的代码很简单,它实际就是关闭了 eslint 的所有格式化规则。

源码地址:https://github.com/prettier/eslint-config-prettier/tree/master/bin

  1. 我们 yarn add 插件的时候 eslint-config-prettier 模块实际是为 eslint-plugin-prettier 插件服务的,在 eslint-plugin-prettier 的源码中调用了 eslint-config-prettier 中相关的配置,然后执行插件中的代码。
  2. eslint-config-prettierrecommended部分 的源码,源码中也有使用到 eslint-config-prettier(把已有格式化配置关掉),然后自己制定了基础的 recommended 版本,讲到这应该明白为什么在 eslint-plugin-prettier 中有一段最重要的话,需要把它(eslint-config-prettier)放在所有格式化配置的后面。

前面的内容,通过这个插件对前面 ESLint 的配置进行重置。如果想使用一些 Prettier 中不支持的格式化配置,我们把eslint中的格式化加在他们后面写了,也不会有冲突。

  1. prettier/@typescript-eslint 是用来忽略 typescript 中的格式化配置。

这里关于防止 PrettierESLint 冲突,画了一张

另外 eslint-plugin-prettiereslint-config-prettier 的源码都不是很复杂,感兴趣的同学可以去看看,下面是源码地址:

  • eslint-plugin-prettier 官网
  • eslint-config-prettier 官网

Prettier 原理简单说明

不管你写的代码是个什么鬼样子,Prettier 会去掉你代码里的所有样式风格,然后用统一固定的格式重新输出。输出时基本上只考虑一个参数,就是 line length

例如你写的这行代码:

代码语言:javascript复制
foo(arg1, arg2, arg3, arg4);

一行装得下这么多代码,所以就不需要改。

如果你写了下面代码:

代码语言:javascript复制
foo(reallyLongArg(), omgSoManyParameters(),IShouldRefactorThis(), isThereSeriouslyAnotherOne());

太长了,Prettier 就会重新改成这样输出:

代码语言:javascript复制
foo(
  reallyLongArg(),
  omgSoManyParameters(),
  IShouldRefactorThis(),
  isThereSeriouslyAnotherOne()
);

咱们再仔细探究一下这个过程。不管你之前写的代码是什么样,首先必须符合语法规范。Prettier 先把你的代码转换成一种中间状态,叫 AST(Abstract Syntax Tree)

Prettier 提供的 Playground 更直观一些:

上图左侧是手写代码,中间是 AST(去掉了任何代码风格),右侧是重新输出的结果。

Prettier 就是在这个 AST 上重新按照自己的风格输出代码。

这是 Prettier 也搞懂后的最终配置

代码语言:javascript复制
module.exports = {
  parser: '@typescript-eslint/parser', // 解析器
  extends: [
    "./.eslintRules.js",
    'plugin:prettier/recommended',
    "prettier",// 优先 prettier 中的样式规范
    'prettier/@typescript-eslint',
  ], // 继承的规则
  plugins: ['@typescript-eslint'], // 插件
  env: {
    es6: true,
    node: true,
    mocha: true,
  },
  parserOptions: {
    parser: '@typescript-eslint/parser', // 解析 .ts 文件
    ecmaVersion: 2019,
    sourceType: 'module',
    ecmaFeatures: {
      modules: true,
    },
  },
  rules: {
  }, // 规则
};

注意 eslintRulesbase 规则,单独提了出来。

VSCode 自动 fix 配置

因为终极目标是我们在使用eslint格式化并且检查我们自己编写的 javascripttypescript。除了我们要求的代码,如果开发者添加别的代码也应该进行格式化,除非忽略的文件,开发者本地安装什么插件我们管不到,在用户级别配置中 setting.json

代码语言:javascript复制
{
  // 此模式不能使用skipFiles特性,暂时关闭,需要调试其他进程时请在本地打开
  // "debug.node.autoAttach": "on",
  "editor.formatOnSave": true,
  "debug.openDebug": "openOnFirstSessionStart",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "[javascript]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  }
}

lint 校验代码与 与 --fix 参数设置

上面的配置都做完了,如果不是一个新项目是原有的老项目,可能需要做一些改动喽!我们先 Fix一下。

代码语言:javascript复制
npm 脚本中需要有这样的配置
"scripts": {
    "lint": "eslint src",
    "lint:create": "eslint --init"
}

执行命令npx run lint会出现如下的错误:

代码语言:javascript复制
    1:7   error  'lint' is assigned a value but never used  no-unused-vars
    1:14  error  Strings must use doublequote               quotes
    1:22  error  Missing semicolon                          semi

    3 problems (3 errors, 0 warnings)
    2 errors, 0 warnings potentially fixable with the `--fix` option.

这里报了三个错误,分别是:

  • index.js 第1行第7个字符,报错编码规则为 no-unused-vars:变量 lint 只定义了,但是未使用;
  • index.js 第1行第14个字符,报错编码规则为 quotes:编码规范字符串只能使用双引号,这里却使用了单引号;
  • index.js 第1行第22个字符,报错编码规则为 semi:编码规范每行代码结尾必须加分号,这里没有加分号。

设置 --fix 参数

说明:这里给 "lint": "eslint src --fix", 加上 --fix 参数,是 ESLint 提供的自动修复基础错误的功能。

此时运行 npm run lint 会看到少了两条报错信息,并不是说编码规范变了,而是 Eslint 自动修复了基础错误,打开 index.js 文件,可看到字符串自动变成了双引号,并且代码末尾也加上了分号。可惜的是 --fix 只能修复基础的不影响代码逻辑的错误,像 no-unused-vars 这种错误只能手动修改。

总结

本文主要对开篇那张图片中的本地代码检查部分进行了详细讲解,从实践到原理,另外小伙伴们也可以想下我开篇提到的如何做出亮点,希望有所帮助,最后快去制定一个属于自己项目的规范与检查吧!

如果开篇图中后面 CI/CD 部分感兴趣的可以找我讨论,后面会单独写一篇 CI/CD 文章,不然篇幅太长了,欢迎在看转发。

参考文章

  • Prettier 看这一篇就行了
  • ESLint 在中大型团队的应用实践
  • 使用 ESLint Prettier 规范 React Typescript 项目
  • eslint-plugin-prettier 官网
  • Using ESLint and Prettier in a TypeScript Project
  • 十分钟了解eslint配置 && 编写自定义eslint规则

0 人点赞