扔掉 ESLint + Prettier:Biome 全家桶实战

装一套前端工程化链路,十几个 npm 包打底:eslint、prettier、eslint-config-prettier、typescript-eslint、eslint-plugin-import、eslint-plugin-react、eslint-plugin-react-hooks、eslint-plugin-jsx-a11y......config 文件几百行,CI 跑一次 lint 十几秒,ESLint 和 Prettier 版本一打架就红屏。新拉代码的同学第一周经常在配 ESLint。

Biome 干的事就是把这些全收进来------一个二进制,lint、format、import 排序、迁移工具全内置,Rust 写的,速度快得离谱。这篇文章带你从零接 Biome,再从 ESLint + Prettier 迁移过来,顺手用 v2 的 GritQL 插件写一条自定义规则。

Biome 凭什么替代那十几包

一句话:单二进制,零插件依赖,Rust 速度

  • 单二进制 :一个 @biomejs/biome 包,里面带编译好的 Rust 二进制,不依赖 Babel、不依赖 PostCSS、不依赖任何 JS 解析器,装它不用等十几分钟编 node-gyp。
  • lint + format 一体:ESLint 管 lint,Prettier 管 format,两边规则要靠 eslint-config-prettier 调和。Biome 内部一份规则表,format 和 lint 不会互相打架。
  • 速度:Rust 写的,单线程都比 ESLint 多进程快,CI 里大项目从十几秒掉到一两秒很常见。
  • import 排序内置 :不用装 eslint-plugin-import,organizeImports 一个配置项搞定。
  • 迁移工具内置biome migrate eslint 直接读你的 .eslintrc,把规则映射过来。

代价是规则生态没 ESLint 那么多(几百条对几千条),但 90% 的项目 recommended 规则集够用。v2 开始支持用 GritQL 写自定义规则,补上了最后一块短板。截至本文写作时 Biome 稳定版是 2.5.3,v2 系列已经迭代了一阵,配置和插件语法都趋于稳定。

从零接入

新建一个 Vite + React + TS 项目,最小接入 Biome。

bash 复制代码
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install --save-dev @biomejs/biome
npx @biomejs/biome init

init 会生成一个最小 biome.json。2.5.3 跑出来的原始长这样(tab 缩进、双引号、vcs 默认关):

json 复制代码
{
  "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": false
  },
  "files": {
    "ignoreUnknown": false
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  },
  "linter": {
    "enabled": true,
    "rules": {
      "preset": "recommended"
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double"
    }
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  }
}

有几个地方和 v1 不一样,得看清:

  • linter.rules"preset": "recommended" ,不是 v1 的 "recommended": true。这是 v2 的配置 breaking change,老教程里的写法在新版会报错。
  • $schema 的版本号要和装的 Biome 对齐 。装的是 2.5.3,schema 就是 2.5.3,VS Code 的 Biome 扩展会按它做补全和校验。
  • vcs 默认是关的enabled: falseuseIgnoreFile: false)。想让 Biome 读 .gitignore 自动排除 dist、node_modules,得手动开。

我自己的偏好是 space 缩进 + 单引号 + 开启忽略文件,调完是这样(带注释的是改动项):

json 复制代码
{
  "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
  "vcs": {
    "enabled": true,            // 开
    "clientKind": "git",
    "useIgnoreFile": true       // 开:读 .gitignore
  },
  "files": {
    "ignoreUnknown": true,
    "includes": ["**", "!**/dist", "!**/coverage"]
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",     // tab 改 space
    "indentWidth": 2,
    "lineWidth": 100
  },
  "linter": {
    "enabled": true,
    "rules": {
      "preset": "recommended"
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",  // double 改 single
      "semicolons": "asNeeded"
    }
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  }
}
  • vcs.useIgnoreFile:读 .gitignore,dist、node_modules 自动排除,不用手写忽略;files.includes 里再用 ! 前缀补排除项。
  • javascript.formatter:JS 特有的格式化偏好,引号风格、分号去留在这定。
  • assist.actions.source.organizeImports:import 排序开关,放 assist 下而不是 linter,因为它是"辅助修改"不是"报错"。

package.json 加几条脚本:

json 复制代码
{
  "scripts": {
    "check": "biome check",
    "format": "biome format --write",
    "lint": "biome lint",
    "ci": "biome ci"
  }
}

biome check 是 lint + format 的合体,加 --write 直接改文件。跑一下:

bash 复制代码
npm run check

刚 init 的项目通常没什么问题。往 App.tsx 里丢一行 const x = 1,再丢个 debugger,跑 check 就能看到 Biome 报错并自动修。

从 ESLint + Prettier 迁移

老项目已经有 ESLint + Prettier,不可能从头重写规则。Biome 提供了迁移命令:

bash 复制代码
# 把 .eslintrc 里的规则映射到 biome.json
npx @biomejs/biome migrate eslint --write

# 顺带把"受 ESLint 规则启发但 Biome 自创"的规则也加进来
npx @biomejs/biome migrate eslint --write --include-inspired

# 把 .prettierrc 里的格式化偏好迁移过来
npx @biomejs/biome migrate prettier --write

这两条命令会读你现有的配置,往 biome.json 里塞规则。--include-inspired 会把 Biome 中那些"灵感来自 ESLint 某条规则但名字/实现不同"的规则也映射进来,迁移更彻底。但迁移不是无损的,几个坑等着你。

坑1:不是每条 ESLint 规则都有对应

现象 :迁移完跑 biome check,原来 ESLint 报的一批错误突然不报了,比如某些 eslint-plugin-import 的规则、jsx-a11y 的部分规则。

原因:Biome 规则数量比 ESLint 生态少,迁移命令只能映射有对应的规则,没对应的会输出 warning 提示你手动处理。

解决:先跑一遍迁移,看终端输出的"unmapped rules"列表。两类处理:

  • 有替代方案的:去 Biome 文档查同名规则,手动加进 linter.rules
  • 暂无对应的:要么删掉这条要求,要么用 GritQL 自己写(见后面第 4 节)。

接受现实:Biome 不会覆盖 ESLint 全部规则,但它的 recommended 预设覆盖了 90% 高频问题,剩下的缺口正是 GritQL 插件要补的。

坑2:rules 的 severity 语义对不上

现象 :原来 ESLint 里某条规则是 warn,迁移到 Biome 后变 error,CI 直接挂。

原因 :ESLint 的 warn 在 Biome 里没有完全等价的中间态,迁移命令默认把能映射的规则设成 error

解决 :手动把不希望阻塞 CI 的规则降级。在 linter.rules 里单独配(注意顶层是 "preset": "recommended",单条规则在下面分组里覆盖):

json 复制代码
{
  "linter": {
    "rules": {
      "preset": "recommended",
      "style": {
        "useImportType": "off"
      },
      "suspicious": {
        "noConsoleLog": "warn"
      }
    }
  }
}

每个规则可以设 off/warn/error,按项目对 CI 的严格程度调。noConsoleLog 是 Biome 内置就有的(suspicious 组),如果你只是想禁 console.log,其实不用写 GritQL 插件,直接配这条即可------但 GritQL 的价值在"内置没有、自定义"的场景,后面讲。

坑3:ignore 配置方式变了

现象 :迁移后 dist/coverage/ 这些目录还是被扫,报一堆不该报的错。

原因 :ESLint 用 .eslintignoreignorePatterns,Prettier 用 .prettierignore。Biome 只认 files.includes.gitignore(开了 useIgnoreFile)。

解决 :删掉 .eslintignore.prettierignore,把这些路径加到 biome.jsonfiles.includes

json 复制代码
{
  "files": {
    "includes": ["**", "!**/dist", "!**/coverage", "!**/artifacts"]
  }
}

! 开头表示排除。配合 vcs.useIgnoreFile,基本不用手维护。

自定义规则:用 GritQL 写插件(v2 杀手锏)

v2 之前 Biome 不能自定义规则,是它被诟病最多的一点。v2 引入了 GritQL------一种代码模式匹配 DSL,本来用来做大范围代码重构,Biome 借它来写 lint 规则。

先说清楚一个事:禁 console.log 这种常见需求 Biome 内置就有 noConsoleLog 规则(见上一节坑2),不用写插件 。GritQL 插件真正发挥作用的地方是"内置没有、团队约定俗成"的规则,比如禁止调用某个内部废弃 API、强制所有 fetch 包裹一层统一的错误处理。下面拿 console.log 当例子纯粹是因为它最短好懂,真实场景你该写更团队特色的东西。

在项目根目录建 grit-plugins/no-console-log.grit

grit 复制代码
language js

`console.log($msg)` as $call where {
    register_diagnostic(
        span = $call,
        message = "生产代码禁止 console.log,改用项目的 log 工具",
        severity = "warn",
        fix_kind = "unsafe"
    )
}

这段我是拿 Biome 2.5.3 实测过的,能跑。逐行拆:

  • language js:声明作用于 JS/TS。
  • console.log($msg):反引号里是 AST 模式,$msg 捕获参数。
  • as $call:把匹配到的整个调用节点绑定到变量 $call,后面用它定位报错位置。
  • where { ... }:匹配后的处理块,在里头注册诊断。
  • register_diagnostic(...):核心,注册一条报错。span 指明高亮哪段代码,message 是报错文本,severitywarn/errorfix_kind 控制自动修复策略(safe--write 直接修,unsafe--write --unsafe)。

biome.json 里启用插件(字符串路径就够,Biome 会自动加载):

json 复制代码
{
  "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
  "plugins": ["./grit-plugins/no-console-log.grit"],
  "linter": {
    "enabled": true,
    "rules": {
      "preset": "recommended"
    }
  }
}

biome lint,源码里所有 console.log 都会被标出来。我实测的输出长这样:

c 复制代码
test.ts:1:1 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ! 生产代码禁止 console.log,改用项目的 log 工具

  > 1 │ console.log("调试用");
      │ ^^^^^^^^^^^^^^^^^^^^^

! 表示 warn 级别,消息和我写的一字不差。

插件还能顺手做自动修复------在 where 块里加一行 $call => \console.info($msg)`,把 console.log改写成console.info,配上 fix_kind = "safe"--write就能批量改。GritQL 的能力远不止 lint------它本质是代码改写 DSL,能做批量重构(比如把全项目的moment换成dayjs、给所有旧 API 调用加 deprecated 标记)。配 where` 还能加过滤条件,比如只在测试文件外匹配。作为 lint 插件用,已经够补上 Biome 规则数量的短板;想玩深的,文末 GritQL 文档有完整语法。

接 git hook 和 CI

工具配好了,得卡在提交前和 CI 上,否则没人跑等于白搭。

git hook:用 lint-staged 风格

Biome 不强制配 hook 工具,但配一个能省事。用 simple-git-hooks

bash 复制代码
npm install --save-dev simple-git-hooks lint-staged

package.json 里:

json 复制代码
{
  "scripts": {
    "prepare": "simple-git-hooks"
  },
  "simple-git-hooks": {
    "pre-commit": "npx lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx,json,css}": "biome check --write"
  }
}

提交时 biome check --write 只扫改动的文件,几百毫秒搞定,不会让你等。

CI:用 biome ci

CI 里别用 biome check,用专门的 biome ci。区别是 ci 模式默认不写文件、遇到任何问题直接非零退出,适合卡流水线:

yaml 复制代码
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx @biomejs/biome ci

biome ci 还会校验 biome.json 的 schema 是否正确,配置错了也能在 CI 抓出来。

踩坑记录

迁移跑通了,但日常用还是会撞到几个细节。

坑4:格式化结果和 Prettier 不完全一致

现象:把 Prettier 卸了换 Biome format,git diff 一大片,看着像全文件重写。

原因:Biome 的格式化策略和 Prettier 大部分一致,但有一小部分刻意不同。典型差异:JSX 多属性换行阈值、长链式调用的断行位置、对象字面量尾逗号策略、模板字符串拼接换行。Biome 团队的立场是这些差异是设计选择,不会为了对齐 Prettier 而改。

解决:接受这个一次性 diff,提交一个 "chore: switch to Biome format" 的大 commit,之后就干净了。不要试图逐条对齐 Prettier 行为,Biome 有自己的判断逻辑,硬调会掉进细节黑洞。

坑5:biome.json 的 schema 版本要对

现象 :从老教程 copy 来一段 biome.json 配置,跑起来报 "unknown field" 或字段不认。

原因 :Biome v1 到 v2 配置结构有 breaking change(比如 recommended: true 变成 preset: "recommended"),$schema 的 URL 也要对应版本。v2 系列内部小版本间 schema 也有微调,用了旧版 schema 会出现"字段不认"或补全失效。

解决$schema 永远和安装的 Biome 版本对齐。不确定版本跑 npx @biomejs/biome --version,把 schema URL 里的版本号改成一致的------比如本文写作时的稳定版是 2.5.3,schema 就是 https://biomejs.dev/schemas/2.5.3/schema.json。VS Code 装了 Biome 扩展会按 schema 做补全和校验,配置写错当场提示。

坑6:monorepo 的配置查找

现象 :monorepo 里每个子包各自 biome check,根目录跑却报找不到规则,或子包用了根包不该有的配置。

原因 :Biome 默认从当前目录向上找最近的 biome.json。monorepo 如果每个子包都放配置,根包的规则不生效;如果只在根包放,子包单独跑时又能找到(向上找),但 includes 路径会基于子包目录,行为不一致。

解决 :只在根目录放一个 biome.json,子包不单独配。在根 package.json 统一脚本:

json 复制代码
{
  "scripts": {
    "check": "biome check .",
    "ci": "biome ci ."
  }
}

显式传 . 让 Biome 在根目录作用,配合 files.includes 的相对路径,整 monorepo 一套规则打天下。

收个尾

Biome 的核心卖点是"一个工具 + 零插件依赖 + Rust 速度",迁移成本主要花在规则映射和接受少量格式差异上。GritQL 插件补上了自定义规则这块短板,v2 之后基本没什么不能干的了。

你的项目 lint + format 跑一次要多久?有没有被 ESLint 和 Prettier 版本冲突坑到怀疑人生的时刻?留言区聊聊。

相关资源

相关推荐
533_1 小时前
[element-plus] el-tabs和el-menu联动
前端·javascript·vue.js
小呀小叮当~1 小时前
Telegram 有哪些实用功能?一文了解
前端·人工智能·后端·其它
索西引擎2 小时前
【React】React Fiber 架构:设计动机、核心机制与调度策略分析
前端·react.js·架构
雨季mo浅忆2 小时前
上传文件夹
前端·vue3
谙忆10242 小时前
响应式图片实战:srcset、sizes、picture 与 art direction 到底怎么用
前端
索西引擎4 小时前
【React】JSX:JavaScript 语法扩展的本质、编译机制与表达能力分析
前端·javascript·react.js
国科安芯12 小时前
航天电子模拟前端三大支柱:精密运放、高速运放与电压监控的协同设计方法——ASL8522S/ASL622S/ASL706S技术解析
前端·单片机·嵌入式硬件·fpga开发·架构·安全性测试
神明不懂浪漫13 小时前
【第四章】CSS(二)——文本外观属性与复合选择器
前端·css·经验分享·笔记
kyriewen13 小时前
别再用AI debug了——这5种bug越修越烂
前端·javascript·ai编程