公司前端项目ESLint规则集统一化

背景

公司前端目前有 30 多个项目,其中一部分采用 React 技术栈,另一部分采用 Vue 技术栈。由于这些项目最初由不同的团队成员分别创建,导致即便是同一技术栈,不同项目的 ESLint 规则配置差异比较大。

比如:某些 React 项目的 ESLint 配置规则非常严格,包含大量规则集;而另一些 React 项目仅配置了最基础的规则,只是为了应付公司检查。于是就出现了这样的情况:

  • 在 A 项目提交代码时,husky 触发的 ESLint 检查很宽松,几乎不用改多少代码。
  • 在 B 项目提交代码时,ESLint 检查非常严格,往往需要花很长时间修改。

这种差异让部分成员觉得缺乏公平性:同样写代码,有人几乎没成本,有人却要反复调整。为了兼顾 公平性和代码质量的有效检查,有必要在公司内部统一 ESLint 配置规则。

那么问题来了:如何统一? 思路并不唯一,本文采用的方案是:针对 Vue 和 React 项目分别定制一套 ESLint 规则集,并发布到公司私有 npm 仓库,在各项目中统一安装和使用。下面我们分步骤介绍这一方案的实现

第一步 创建私有npm仓库

一说起创建私有npm仓库, 一搜技术文章,几乎清一色的都会说用Verdaccio实现。其实gitlab也能做。这里有极狐gitlab为例,说一下如何创建公司私有的npm仓库。在要开发和使用公司私有npm包的项目下, 新建一个.npmrc文件, 配置这两行内容,这两行内容的含义是,分别指明去哪下载/发布 和 怎么鉴权。

bash 复制代码
## 指定 @{公司英文名} 作用域的仓库地址
@{公司名}:registry=https://{gitlab仓库域名}/api/v4/projects/project/{项目id}/packages/npm/

# 使用 token 自动认证
//{gitlab仓库域名}/api/v4/projects/{项目id}/packages/npm/:_authToken={gitlab中配置的个人访问凭证}
  • 第一行:仓库地址
    指定当安装/发布 @公司名/* 的包时,不去默认的 npmjs.org,而是去 GitLab 中对应项目下的 npm Registry。
  • 第二行:鉴权信息
    这里的写法只能是 //host/path,而不能写成 https://,因为 .npmrc 的语法要求协议在 registry 中定义,鉴权部分只写 //... 即可。

配置完成后,执行 npm publishpnpm add 时,就会自动完成鉴权。

接下来,可以写一个简单的 npm 包进行验证,确认上传和安装都正常。

npm测试包目录结构如下:

  • .npmrc里面配置的内容就是上面的两行
  • index.js中写了一个简单的hello,world导出函数
  • package.json中定义了包名和入口文件
js 复制代码
{
  "name": "@juejin/my-test-package",
  "version": "0.0.1",
  "description": "A simple test npm package",
  "main": "index.js",
  "scripts": {
    "test": "node index.js"
  },
  "author": "Your Name",
  "license": "MIT"
}

执行npm publish之后,去极狐Gitlab特定仓库的部署==>软件包库菜单查看,可以看到包已经成功上传。

然后把.npmrc的配置添加到业务项目的.npmrc文件中,在业务项目下执行

perl 复制代码
pnpm add @xxx/my-test-package

在node_modules文件夹下查看, 可以发现正常安装。

第二步 制作ESLint规则包

首先ESLint该选择ESLint8还是ESLint9呢?

ESLint v8

  • 稳定性:目前大多数项目(特别是 Vue CLI、Create React App、Vite 脚手架)默认还是用 v8,生态支持最好。
  • 插件兼容性 :几乎所有插件(eslint-plugin-vueeslint-plugin-react@typescript-eslint 等)都第一时间支持 v8。
  • 配置方式 :传统的 .eslintrc.* 文件(JSON/JS/YAML)为主,比较熟悉。
  • 生命周期:虽然 v9 已经发布,但 v8 仍然会维护一段时间(安全补丁)。

适合:已有项目、依赖多、团队追求稳定性。

ESLint v9

  • 新配置系统 :完全切换到 Flat Config,用 eslint.config.js 代替 .eslintrc.*,更清晰、支持按文件匹配配置。
  • 内置配置 :提供 @eslint/js,替代 eslint:recommended
  • 更现代的设计 :从根本上解决了 .eslintrc 扩展层层嵌套、难以管理的问题。
  • 生态迁移中:多数插件已支持 v9,但仍有一些插件和脚手架没完全更新。比如老项目如果依赖某些工具链,可能会遇到兼容性问题。

适合:新项目、想尝鲜或愿意跟进最新标准的团队。

考虑了一下,还是要着眼未来, 选择ESLint9,好处是配置更清晰、未来趋势(避免未来二次迁移),而且最新的eslint v9.34.0版本, 支持多线程linting, 执行速度比较快。对同一项目, 使用单线程,和开启2核和4核的平均耗时分别是

  • 单线程: 45.234 s ± 1.123 s
  • 多线程(2核): 24.567 s ± 0.891 s
  • 多线程(4核): 15.123 s ± 0.567 s
js 复制代码
Benchmark 1: npx eslint src/ --concurrency off
  Time (mean ± σ):     45.234 s ±  1.123 s    [User: 44.1 s, System: 1.2 s]
  Range (min ... max):   43.891 s ... 47.234 s    10 runs

Benchmark 2: npx eslint src/ --concurrency 2  
  Time (mean ± σ):     24.567 s ±  0.891 s    [User: 47.2 s, System: 1.8 s]
  Range (min ... max):   23.234 s ... 25.987 s    10 runs

Benchmark 3: npx eslint src/ --concurrency 4
  Time (mean ± σ):     15.123 s ±  0.567 s    [User: 58.4 s, System: 2.1 s]
  Range (min ... max):   14.234 s ... 16.123 s    10 runs

因为项目有两种技术栈, 所以对每种技术栈, 各自搜集一套ESLint规则

React版:

选择一些社区与生态认可度比较高的规则集

  • eslint:recommended / @eslint/js
    → ESLint 官方团队提供,保证基础语法错误都能被检测(未定义变量、重复定义等)。
    📊 NPM 下载量:超过 每周 3000 万,几乎所有项目必备。
  • @typescript-eslint/recommended
    → TypeScript 官方维护的 ESLint 插件,专门覆盖 TS 类型相关的最佳实践。
    📊 NPM 下载量:每周 1500 万+ ,在所有 TS 项目中广泛使用。
  • plugin:react/recommended
    → 由 React 官方团队成员维护 (eslint-plugin-react),包含 propTypes、JSX 语法错误等检测。
    📊 下载量:每周 1700 万+ 。几乎所有 React 项目都会装。
  • plugin:react-hooks/recommended
    → Facebook React 团队自己写的规则集,强制遵守 Hooks 规则(useEffect 依赖完整性、Hook 调用顺序)。
    📊 React 官方文档明确要求安装这个插件。
  • plugin:import/recommended
    → 解决 import/export 顺序、模块未解析的问题,保证跨项目依赖更稳健。
    📊 在 Node.js + React 项目里极常见。
  • plugin:prettier/recommended
    → 结合 Prettier 格式化,避免 ESLint 与 Prettier 冲突。社区里几乎是 标配

这些插件和规则集由 React 官方 或 业界主流公司维护的,不是个人爱好,所以可靠性、流行度都比较高。

js 复制代码
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import importPlugin from "eslint-plugin-import";
import prettier from "eslint-plugin-prettier";

/**
 * 配置全局变量
 */
const browserGlobalsConfig = {
  languageOptions: {
    globals: {
      ...globals.browser,

      /** 追加一些其他自定义全局规则 */
      wx: true
    }
  }
};

// TypeScript 文件配置
const tsFiles = {
  files: ["src/**/*.{ts,tsx}"],
  languageOptions: {
    parser: tseslint.parser,
    parserOptions: {
      ecmaVersion: 2024,
      sourceType: "module",
      ecmaFeatures: { jsx: true }
    }
  },
  rules: {
    "no-undef": "off"
  }
};

// React 配置
const reactConfig = {
  files: ["src/**/*.tsx", "src/**/*.jsx"],
  plugins: { react, "react-hooks": reactHooks }, // 应用全局规则,并覆盖特定的规则
  rules: {
    ...reactHooks.configs.recommended.rules,
    ...react.configs.recommended.rules,
    "react/react-in-jsx-scope": "off",
    "no-undef": "off"
  },
  settings: {
    react: { version: "detect" }
  }
};

const importConfig = {
  files: ["src/**/*.{ts,tsx,js,jsx}"],
  plugins: { import: importPlugin },
  rules: {
    ...importPlugin.configs.recommended.rules,
    "@typescript-eslint/no-require-imports": "off"
  },
  settings: {
    "import/resolver": {
      alias: {
        map: [
          ["@", "./src"],
          ["@@", "./src/.umi"],
          ["@@test", "./src/.umi-test"]
        ],
        extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".less"]
      }
    }
  }
};

const prettierConfig = {
  files: ["src/**/*.{ts,tsx,js,jsx}"],
  plugins: { prettier },
  rules: { "prettier/prettier": "warn" }
};

// 全局规则
const commonRules = {
  rules: {
    "no-unused-vars": "off",
    "react/prop-types": "off",
    "no-useless-escape": "off",
    "react/display-name": "off",
    "react-hooks/exhaustive-deps": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/ban-ts-comment": "off"
  }
};

// 导出最终配置
export default [
  // 全局忽略
  {
    ignores: [
      "**/node_modules/**",
      "**/.umi*/**",
      "**/dist/**",
      "**/build/**",
      "scripts/**",
      "config/**",
      "public/**",
      "mock/**",
      "**/*.d.ts"
    ]
  },
  // js推荐配置
  js.configs.recommended,
  // ts推荐配置
  ...tseslint.configs.recommended,
  // 浏览器全局变量
  browserGlobalsConfig,
  tsFiles,
  reactConfig,
  importConfig,
  prettierConfig, // 将通用规则放在最后,确保它能覆盖之前的所有配置
  commonRules
];

Vue3版

1. eslint-plugin-vue: flat/recommended

推荐理由:

  • Vue 官方维护,覆盖最核心的 Vue 3 代码风格、语法规则。
  • 包含对 script setup、组合式 API 的支持。
  • 能帮你避免常见的坑,比如未注册的组件、未使用的 ref、无效的 v-for key 等。

2. @vue/eslint-config-typescript

推荐理由:

  • Vue 官方团队维护,专门适配 TypeScript + Vue 3
  • 内置了 @typescript-eslint,并帮你处理 .vue 文件里 <script lang="ts"> 的解析。
  • 避免了你手动折腾 parser 配置,拿来即用。

3. @vue/eslint-config-prettier/skip-formatting

推荐理由:

  • 禁用掉和 Prettier 冲突的风格类规则(比如缩进、换行)。
  • 避免 ESLint 和 Prettier 打架,让 ESLint 专注语法/逻辑问题,Prettier 专注格式化。
  • 如果你团队里已经统一用 Prettier,就必加。

4. eslint-plugin-import (可选)

推荐理由:

  • 帮你检查 import/export 是否正确。
  • 避免未使用的 import、重复导入、错误的相对路径。
  • 在 Vue3 + Vite 项目里尤其有用,结合 alias (@/) 可以避免路径拼错。
js 复制代码
import js from "@eslint/js";
import globals from "globals";
import vuePlugin from "eslint-plugin-vue";
import skipFormatting from "@vue/eslint-config-prettier/skip-formatting";
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";

/**
 * 配置全局变量
 */
const browserGlobalsConfig = {
  languageOptions: {
    globals: {
      ...globals.browser,

      /** 追加一些其他自定义全局规则 */
      wx: true
    }
  }
};
export default defineConfigWithVueTs(
  js.configs.recommended,
  // TypeScript 推荐规则
  vueTsConfigs.recommended,
  browserGlobalsConfig,
  ...vuePlugin.configs["flat/recommended"],
  {
    files: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
    languageOptions: {
      parserOptions: {
        parser: "@typescript-eslint/parser"
      }
    },
    rules: {
       "vue/event-name-casing": "off",
      "vue/prop-name-casing": "off",
      "vue/singleline-html-element-content-newline": "off",
      "vue/max-attributes-per-line": "off",
      "vue/multi-word-component-names": "off",
      "vue/html-closing-bracket-newline": [
        "warn",
        {
          singleline: "never",
          multiline: "never"
        }
      ],
      "vue/first-attribute-linebreak": [
        "warn",
        {
          singleline: "ignore",
          multiline: "ignore"
        }
      ],
      "vue/html-self-closing": [
        "error",
        {
          html: { void: "always", normal: "never", component: "always" }
        }
      ]
    }
  },
  skipFormatting
);

规则集ts类型定义文件types.d.ts内容如下:

js 复制代码
// 从 eslint 包导入官方的 Config 类型
type EslintFlatConfig = import("eslint").Config;

// 声明一个常量,它的类型是 EslintFlatConfig 类型的数组
declare const createVue3TsEslintConfig: EslintFlatConfig[];

// 将这个常量作为模块的默认导出
export default createVue3TsEslintConfig;

第三步 一键发版

我们采用 pnpm workspace + Changesets 来管理和发布多个 ESLint 配置包。

目录结构大致如下:

js 复制代码
repo-root/
├─ package.json            # 根配置(workspace、脚本)
├─ pnpm-workspace.yaml
├─ .npmrc                  # 指定 scope 的 registry + 鉴权(本地或 CI)
├─ .changeset/             # changesets 配置 & 变更文件
└─ npm-packages/
   ├─ eslint-config-vue3/
   │  ├─ package.json
   │  └─ index.js
   └─ eslint-config-react/
      ├─ package.json
      └─ index.js

3.1 根配置

  1. pnpm-workspace.yaml 采用pnpm的workspace方式对多个npm包进行管理。
js 复制代码
packages:
  - 'npm-packages/*'
  1. 根 package.json
  • version-packages:根据 changeset 生成版本号并更新依赖与 lockfile
  • release:按依赖拓扑顺序构建并发布所有变更包(Changesets 会自动先发被依赖的包)
js 复制代码
{
  "name": "eslint-configs-monorepo",
  "private": true,
  "packageManager": "pnpm@10.12.4",
  "scripts": {
    "changeset": "changeset",
    "version-packages": "changeset version && pnpm install --lockfile-only",
    "release": "pnpm -r build --if-present && changeset publish"
  }
}

pnpm -r build --if-present的含义是给每个子包跑build(如果有)

  1. 原则上,根 .npmrc不能把含 token 的 .npmrc 提交到仓库,但考虑到公司的gitlab仓库只能内网访问, 为了使用方便,提交上去也无妨。
ruby 复制代码
@公司名:registry=https://git.example.com/api/v4/projects/项目ID/packages/npm/
//git.example.com/api/v4/projects/项目ID/packages/npm/:_authToken=YOUR_PERSONAL_ACCESS_TOKEN

3.2 包配置

  • "main": "index.mjs"

    包的入口文件路径。当别人 importrequire 这个包时,会默认从这里加载。

  • "types": "types.d.ts"

    TypeScript 类型声明文件路径。用于在编辑器/IDE 中获得类型提示。

  • "files": [ "index.mjs", "types.d.ts", "README.md" ]

    指定 发布到 npm 仓库时包含的文件。这样可以避免把无关文件(如源码、测试文件)上传,保持包体积小。只会发布列出的文件。

  • "dependencies": 运行这些规则,就需要这些依赖, 将所有用的依赖包,都列举出来

  • peerDependencies: 明确工具包的依赖关系, 避免与业务项目的ESLint版本冲突

  • "publishConfig": { "access": "restricted" }

    控制包发布的访问级别: "public" → 可公开发布到 npmjs.org(所有人可安装);"restricted" → 只能发布到私有仓库(如 GitLab npm Registry / npm 企业版),不能上传到公网。在公司私有仓库场景下,通常用 "restricted",避免意外泄露。

1. vue3规则集的package.json配置

npm-packages/eslint-config-vue3/package.json 内容如下:

js 复制代码
{
  "name": "@company/eslint-config-vue3",
  "version": "1.7.6",
  "description": "vue3 eslint config",
  "author": "xxx Frontend Team",
  "license": "MIT",
  "main": "index.mjs",
  "types": "types.d.ts",
  "files": [
    "index.mjs",
    "types.d.ts",
    "README.md"
  ],
  "dependencies": {
    "@eslint/js": "^9.34.0",
    "@vue/eslint-config-prettier": "^10.2.0",
    "@vue/eslint-config-typescript": "^14.6.0",
    "eslint-plugin-vue": "^10.4.0",
    "globals": "^16.3.0"
  },
  "peerDependencies": {
    "eslint": ">=9.34.0"
  },
  "keywords": [
    "eslint9+",
    "eslint-config",
    "vue3"
  ],
  "publishConfig": {
    "access": "restricted"
  }
}

2. react规则集package.json

npm-packages/eslint-config-react/package.json的内容如下

js 复制代码
{
  "name": "@company/eslint-config-react",
  "version": "1.4.0",
  "main": "index.mjs",
  "types": "types.d.ts",
  "files": [
    "index.mjs",
    "types.d.ts"
  ],
  "license": "MIT",
  "publishConfig": {
    "access": "restricted"
  },
  "dependencies": {
    "@eslint/js": "^9.34.0",
    "eslint-import-resolver-alias": "^1.1.2",
    "eslint-import-resolver-typescript": "^4.4.4",
    "eslint-plugin-import": "^2.32.0",
    "eslint-plugin-prettier": "^5.5.4",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^5.2.0",
    "globals": "^16.3.0",
    "prettier": "^3.6.2",
    "typescript-eslint": "^8.42.0"
  },
  "peerDependencies": {
    "eslint": ">=9.34.0"
  }
}

3.3 初始化 Changesets(一次性)

js 复制代码
pnpm add -D @changesets/cli
pnpm changeset init

会生成 .changeset/config.json,可以保持默认;如果你希望内部依赖只要上游变更就给下游自动打补丁号,可加入:updateInternalDependencies: "patch":当 eslint-config-base 升级时,eslint-config-react 也会自动至少 bump 一个 patch,避免依赖范围不匹配。

js 复制代码
{
  "$schema": "https://unpkg.com/@changesets/config/schema.json",
  "changelog": "@changesets/changelog-github",
  "commit": false,
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch"
}

3.4.写变更(每次发版)

当你修改了包后,创建一个 changeset:

js 复制代码
pnpm changeset

交互式选择受影响的包(先按空格再按回车),选择变更等级(patch/minor/major),写一句描述。

3.5 一键发布

  • version-packages:会把两个包的 version 改掉,并把 workspace:^ 解析为实际 semver。

  • releasechangeset publish):自动安装依赖顺序排序,成功后会打印每个包的发布结果。

js 复制代码
# 生成新版本号 & 更新依赖与 lockfile
pnpm version-packages

# 构建并发布(会按顺序先发 base 后发 react)
pnpm release

单独指定子包发布的命令是:

ruby 复制代码
cd npm-packages/eslint-config-vue3
pnpm publish --access public --registry=https://gitlab.xxx.com/api/v4/projects/xxx/packages/npm/

cd ../eslint-config-react
pnpm publish --access public --registry=https://gitlab.xxx.com/api/v4/projects/xxx/packages/npm/

第四步 安装使用

1. 配置项目使用私有仓库

在你要使用这些 ESLint 配置的项目根目录下创建或修改 .npmrc 文件:

ruby 复制代码
@公司名:registry=https://git.example.com/api/v4/projects/项目ID/packages/npm/
//git.example.com/api/v4/projects/项目ID/packages/npm/:_authToken=YOUR_PERSONAL_ACCESS_TOKEN

这样 npm / pnpm 就会从你的 GitLab 私有仓库拉取 @公司名 作用域下的包。

2. 安装 ESLint 规则包

假设你已经发布了:

  • @公司名/eslint-config-vue3
  • @公司名/eslint-config-react

要配置一个 Vue3 项目规则集, 在项目中执行:

arduino 复制代码
pnpm add -D @公司名/eslint-config-vue3

3. 配置 ESLint 使用这些规则集

在你的项目根目录下 eslint.config.ts 中:

js 复制代码
import vue3ESLintRules from "@公司名/eslint-config-vue3";

export default [
  ...vue3ESLintRules,
  {
    rules: {
      "vue/no-v-html": "off",
      "vue/no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": "off",
      "@typescript-eslint/no-explicit-any": "off",
      "vue/singleline-html-element-content-newline": "off",
      "vue/html-self-closing": [
        "error",
        {
          html: { void: "always", normal: "never", component: "always" }
        }
      ]
    }
  }
];

如果是 React 项目,使用 @公司名/eslint-config-react,配置方法类似。

最后

文章末尾,对本文做一下总结,本文介绍了一种在公司内统一 ESLint 规则的完整方案:

  1. 使用 GitLab npm Registry 搭建私有仓库。
  2. 基于 ESLint v9,分别为 React 和 Vue3 项目设计规则集。
  3. 借助 pnpm workspace + Changesets,实现规则包的管理和一键发布。
  4. 在业务项目中通过安装和引入统一规则包,最终实现公司内部的 ESLint 配置统一。

这样一来,不同团队成员在不同项目中都能遵循相同的规则,不仅避免了"不公平"的问题,也能整体提升公司代码质量和一致性。如果你有同样的业务需求,可以参考一下本文的技术思路和实现方案。本文完

相关推荐
鹏多多3 小时前
使用imaskjs实现js表单输入卡号/日期/货币等掩码的教程
前端·javascript·vue.js
w2vmany3 小时前
postmessage xss初步学习
前端·学习·xss
小张成长计划..4 小时前
前端6:CSS3 2D转换,CSS3动画,CSS3 3D转换
前端·3d·css3
IT_陈寒4 小时前
Vue3性能优化实战:这7个技巧让我的应用加载速度提升50%!
前端·人工智能·后端
西西学代码4 小时前
Flutter---音效模式选择器
前端·html
TLucas4 小时前
Layui连线题编辑器组件(ConnectQuestion)
前端·编辑器·layui
艾小码4 小时前
告别页面呆板!这5个DOM操作技巧让你的网站活起来
前端·javascript
正在学习前端的---小方同学6 小时前
vue-easy-tree树状结构
前端·javascript·vue.js
键盘不能没有CV键10 小时前
【图片处理】✈️HTML转图片字体异常处理
前端·javascript·html