规范化命名规则

ESLint插件、ESLint
框架
2024-12-23 16:37

我们坚持一件事情的时候,不是因为这样做了会有效果,而是坚信,这样做是对的。

规范化命名规则

背景

在团队协作时,不同成员可能有不同的编码习惯,这可能导致代码风格不统一、文件结构混乱等问题,进而影响代码的可读性、可维护性和项目的整体质量。
为了确保代码库的整洁有序,制定并遵循一套清晰的命名约定和代码风格指南是非常必要的。
在项目开发中,不同的成员会有各式各样的开发风格,比如:

  1. 小驼峰命名(camelCase)
    • 第一个单词首字母小写,后续单词首字母大写。例如:userName, getFullName
  2. 大驼峰命名(PascalCase)
    • 每个单词首字母都大写。例如:UserName, GetFullName
  3. 下划线(snake_case)
    • 所有单词小写,单词之间用下划线连接。例如:user_name, get_full_name
  4. 短横线,也称烤肉串命名法(kebab-case)
    • 所有单词小写,单词之间用短横线连接。例如:user-name, get-full-name
  5. 空间前缀(Namespace Prefixing)
    • 在名称前加上模块或包的前缀,这类常见于一些配置文件中。例如:eslint.config.js, vite.config.ts
  6. 全大写(UPPER_CASE)
    • 所有单词大写,单词之间可以用下划线分隔。例如:MAX_LENGTH, API_KEY

其中,在1-5方式中也是较为常见的,因此为了统一在团队协作的项目中开发风格,以达到见名知其意,通过ESLint 插件来规范命名

ESLint

  • ESLint 是一个用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具,其目标是使代码更加一致并避免错误。

  • ESLint 是完全插件化的。每条规则都是一个插件,你可以在运行时添加更多。你还可以添加社区插件、配置和解析器来扩展 ESLint 的功能。

先决条件

要使用 ESLint,你必须安装并构建 Node.js^18.18.0^20.9.0>=21.1.0)并支持 SSL。(如果你使用的是官方 Node.js 发行版,则始终内置 SSL。)

注意

相比较于以前的版本,ESLint9.x版本中,会有较大的变更,如果是以前的版本,有许多将不会再适用,而且现在也是推荐以9.x版本为主要版本。就我个人而言,9.x通过扁平化的配置,深得我意,一个爽哈哈哈哈!!!

这里关于ESLint的信息就不过多介绍(主要是赖),具体可以去官网进行了解。ESLint 中文网

规则插件

如果将插件作为 npm 包分发,确保导出插件对象的模块是包的默认导出。并且,如果是非官方插件,强烈建议插件名称以eslint-plugin-为前缀(不要问我为什么)。

自定义规则

自定义规则应该满足一下格式

javascript 复制代码
export default {
    meta: {
        type: "suggestion",
        docs: {
            description: "Description of the rule",
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

meta:(object)包含规则的元数据:

  • type:(string)指示规则的类型,为 "problem""suggestion""layout" 之一:

    • "problem":该规则是识别将导致错误或可能导致混淆行为的代码。开发者应将此视为高度优先解决的问题。
    • "suggestion":规则是确定可以以更好的方式完成的事情,但如果不更改代码就不会发生错误。
    • "layout":该规则主要关注空格、分号、逗号和括号,程序的所有部分决定代码的外观而不是代码的执行方式。这些规则适用于 AST 中未指定的部分代码。
  • docs:(object)通常用于文档生成和工具的属性。对于核心规则是必需的,对于自定义规则是可选的。自定义规则可以根据需要在此处包含其他属性。

    • description:(string)提供规则的简短描述。对于核心规则,这是在 规则索引 中使用的。
    • recommended:(boolean)对于核心规则,这指定规则是否由 @eslint/js 中的 recommended 配置启用。
    • url:(string)指定可以访问完整文档的 URL。代码编辑器经常使用它来提供有关高亮的规则违规的有用链接。
  • fixable:(string)如果 命令行 上的 --fix 选项自动修复规则报告的问题,则为 "code""whitespace"

    重要:对于可修复规则,fixable 属性是强制性的。如果未指定此属性,ESLint 将在规则尝试生成修复时抛出错误。如果规则不可修复,则省略 fixable 属性。

  • hasSuggestions:(boolean)指定规则是否可以返回建议(如果省略,则默认为 false)。

    重要:对于提供建议的规则,hasSuggestions 属性是强制性的。如果此属性未设置为 true,ESLint 将在规则尝试生成建议时抛出错误。如果规则不提供建议,则省略 hasSuggestions 属性。

  • schema:(object | array | false)指定 选项,以便 ESLint 可以防止无效的 规则配置。当规则有选项时为强制。

  • defaultOptions:(array)为规则指定 默认选项。如果存在,其配置中的任何用户提供的选项都将递归合并到它们之上。

  • deprecated:(boolean)指示规则是否已被弃用。如果规则没有被弃用,你可以省略 deprecated 属性。

  • replacedBy:(array)如果规则已弃用,请指定替换规则。

create():返回一个对象,其中包含 ESLint 在遍历 JavaScript 代码的抽象语法树(ESTree 定义的 AST)时调用到 “visit” 节点的方法:

  • 如果键是节点类型或 选择器,ESLint 在沿着树向下移动时会调用该访问者函数。
  • 如果键是节点类型或 选择器:exit,则 ESLint 在向上查找树时会调用该访问者函数。
  • 如果键是事件名称,ESLint 会调用 代码路径分析 的处理函数。

context上下文对象

context 对象具有以下属性:

  • id:(string)规则 ID。
  • filename:(string)与源关联的文件名。
  • physicalFilename:(string)对文件进行 linting 时,它会提供磁盘上文件的完整路径,而无需任何代码块信息。当检查文本时,如果未指定,它会提供传递给 —stdin-filename<text> 的值。
  • cwd:(string)cwd 选项传递给 Linter。它是应被视为当前工作目录的目录的路径。
  • options:(array)此规则的 配置选项 数组。此数组不包括规则严重性(请参阅 专用部分)。
  • sourceCode:(object)一个 SourceCode 对象,可用于处理传递给 ESLint 的源(请参阅 访问源代码)。
  • settings:(object)从配置上看是 共享的设置
  • languageOptions:(object) 每个属性的更多详细信息 此处
    • sourceType:('script' | 'module' | 'commonjs')当前文件的模式。
    • ecmaVersion:(number)用于解析当前文件的 ECMA 版本。
    • parser:(object):用于解析当前文件的解析器。
    • parserOptions:(object)为此文件配置的解析器选项。
    • globals:(object)指定的全局变量。
  • parserPath:(string,已删除,使用 context.languageOptions.parser 代替。)配置中 parser 的名称。
  • parserOptions:(已弃用,请使用 context.languageOptions.parserOptions 代替。)为此运行配置的解析器选项(更多详细信息 此处)。

此外,context 对象具有以下方法:

  • getCwd():(已弃用:使用 context.cwd 代替。)返回传递给 Lintercwd 选项。它是应被视为当前工作目录的目录的路径。
  • getFilename():(已弃用:使用 context.filename 代替。)返回与源关联的文件名。
  • getPhysicalFilename():(已弃用:使用 context.physicalFilename 代替。)对文件进行 linting 时,它会返回磁盘上文件的完整路径,而不包含任何代码块信息。当检查文本时,如果未指定,它会返回传递给 —stdin-filename<text> 的值。
  • getSourceCode():(已弃用:使用 context.sourceCode 代替。)返回一个 SourceCode 对象,你可以使用该对象来处理传递给 ESLint 的源(请参阅 访问源代码)。
  • report(descriptor)。报告代码中的问题(参见 专用部分)。

报告问题

编写自定义规则时你将使用的主要方法是 context.report(),它会发布警告或错误(取决于所使用的配置)。此方法接受一个参数,该参数是一个包含以下属性的对象:

  • messageId:(string)消息的 ID(参见 messageIds)(在 message 上推荐)。
  • message:(string)问题消息(messageId 的替代)。
  • node:(可选的 object)与问题相关的 AST 节点。如果存在且未指定 loc,则将节点的起始位置用作问题的位置。
  • loc:(可选的 object)指定问题的位置。如果同时指定了 locnode,则使用 loc 而不是 node 的位置。
    • start:起始位置的对象。
      • line:(number)发生问题的从 1 开始的行号。
      • column:(number)发生问题的从 0 开始的列号。
    • end:结束位置的对象。
      • line:(number)发生问题的从 1 开始的行号。
      • column:(number)发生问题的从 0 开始的列号。
  • data:(可选的 objectmessage占位符 数据。
  • fix(fixer):(可选的 function)应用 fix 来解决问题。

请注意,至少需要 nodeloc 之一。

以上来自官方的介绍,贴这么多,主要是为了记录一下。ESLint插件的创建具体可以去了解创建插件,这里不过多介绍(想了解一个东西,必须亲身体会到,才能是真正的“会”,做人做事亦是如此)

组件命名(component-naming)

在通过框架开发的系统中,少不了与组件打交道,如在react中常见的以.jsx.tsx, 在vue中常见的有.tsx.vue等。

一个组件应该单独负责一个功能,尽量避免减少全部功能揉在一块(非常不喜欢这种模式),这样会显得很臃肿,而且不易维护(当然,如果不想维护,可尽情发挥哈哈哈哈)。

同时,一个组件的命名应该与该功能相对应,如``List表示一个数据列表模块等等,而不应该去命名什么index`这样没有头绪的方式。

react中,组件命名时,一般都是以大驼峰(PascalCase)的命名方式;在vue中为了避免与普通的标签产生歧义,官方也建议组件应该为大驼峰命名。

image-20241223142239991
image-20241223142310404

为了统一组件名命名规则,应该采用统一个命名方式,通过ESLint插件实现这一点

功能需求

  • 可通过配置项来确定那个文件夹下的组件需要指定的规则
  • 对象中键应该是指定的某个文件夹下,可通过glob匹配方式匹配,这里使用第三方库micromatch来解析匹配
  • 对象中的值应该是指定的命名规则

如:

javascript 复制代码
[
        'error',
    // 下面参数为自定义的配置项
        {
          'src/**/*.{jsx,tsx,vue}': 'PASCAL_CASE', // 对组件统一使用大驼峰
        }
      ]

实现

typescript 复制代码
// rules/component-naming.ts
import micromatch from "micromatch";
import {
  COMPONENT_EXTNAME,
  DEFAULT_PATH_PATTERN,
  NAMING_MAP,
} from "../constants";
import { getExtname, getFilenameNotExt, getFullPath } from "../utils";
import type { AST, Rule } from "eslint";
import {COMPONENT_ERROR_MESSAGE} from "../constants/error-message";
export default {
  meta: {
    type: "layout",
    docs: {
      description:
        "Enforce PascalCase for component filenames with .vue or .tsx extension",
      category: "Stylistic Issues",
      recommended: false,
    },
    fixable: null, // or "code" or "whitespace"
    schema: [
      {
        additionalProperties: {
          type: "string",
        },
      },
      {
        type: "object",
      },
    ],
    messages: {
      componentError: COMPONENT_ERROR_MESSAGE // 错误信息ID,下面会用到
    }
  },
  create(context: Rule.RuleContext) {
    return {
      // 抽象语法树的顶级
      Program(node: AST.Program) {
        // 获取配置项
        const rulesOptions = context.options[0] as Record<
          any,
          keyof typeof NAMING_MAP
        >;
        // 通过上下文信息,获取当前文件的完路径
        const filenameWithPath = getFullPath(context);
        // 根据文件路径,获取文件名称,不包含文件后缀
        const basename = getFilenameNotExt(filenameWithPath);
        // 获取扩展名
        const extname = getExtname(filenameWithPath);
        let rules = rulesOptions;
        // 如果没有配置项,使用默认规则(配置项应该配置为一个对象)
        if (!rules || Object.keys(rules).length === 0) {
          rules = { [DEFAULT_PATH_PATTERN]: "PASCAL_CASE" }; // 对组件统一使用大驼峰}
        }
        for (const [filenamePattern, targetPattern] of Object.entries(rules)) {
          // 是否匹配到指定文件
          if (
            micromatch.isMatch(filenameWithPath, filenamePattern) &&
            COMPONENT_EXTNAME.includes(extname.toLowerCase())
          ) {
            const target = NAMING_MAP[targetPattern] ?? NAMING_MAP.PASCAL_CASE;
            // 如果指定的路径下,文件名没有匹配到,导出错误
            if (!micromatch.isMatch(basename, target)) {
              context.report({
                node,
                messageId: 'componentError', // 错误信息ID
                data: { // 错误信息的占位符数据
                  basename: `${basename}${extname}`,
                  target: targetPattern ?? "PASCAL_CASE"
                },
              });
            }
          }
        }
      },
    };
  },
} as unknown as Rule.RuleModule;

其他命名

以下命名规则的实现,与组件命名规则实现类似,这里就不一一赘述,感兴趣可以查看完整代码eslint-plugin-file-naming

  • 文件名命名(filename-naming)

  • 文件夹命名(folder-naming)

  • 禁止index(no-index-naming)

插件测试

ESLint 提供了 RuleTester 实用程序,可以轻松测试插件的规则。

__test__目录下新建component-naming.test.ts测试文件

typescript 复制代码
import { RuleTester } from "eslint";
import componentFile from "../rules/component-naming";
const ruleTester = new RuleTester();
  
ruleTester.run("component-naming", componentFile, {
    valid: [
      {
        code: "const name = 'test';",
        filename: "src/components/Login.vue",
      },
      {
        code: "const name = 'test';",
        filename: "src/components/LoginLog.vue",
      },
    ],
    invalid: [
        ... // 省略部分
      {
        code: "const name = 'test';",
        filename: "src/components/login.log.vue",
        errors: [
          {
            message:
              "Component filename 'login.log.vue' should be in PASCAL_CASE",
          },
        ],
      },
    ],
  });
  // 携带配置
  ruleTester.run(
    "component-naming with options:[{ '**/components/**/*.{jsx,tsx,vue}': 'PASCAL_CASE' }]",
    componentFile,
    {
      valid: [
        ... // 省略部分
        {
          code: "const name = 'test';",
          filename: "src/components/LoginLog.vue",
          options: [{ "**/components/**/*.{jsx,tsx,vue}": "PASCAL_CASE" }],
        },
      ],
      invalid: [
        ... // 省略部分
        {
          code: "const name = 'test';",
          filename: "src/components/login.log.vue",
          options: [{ "**/components/**/*.{jsx,tsx,vue}": "PASCAL_CASE" }],
          errors: [
            {
              message:
                "Component filename 'login.log.vue' should be in PASCAL_CASE",
            },
          ],
        },
      ],
    },
  );

使用tsx或者esno来运行该测试文件即可,如果测试通过,将不会有任何错误信息返回,否则根据错误信息修改插件代码即可。

君与鹤山皆人杰,倘功名、到手还须做

最近更新:

目录