装一套前端工程化链路,十几个 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: false、useIgnoreFile: 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 用 .eslintignore 或 ignorePatterns,Prettier 用 .prettierignore。Biome 只认 files.includes 和 .gitignore(开了 useIgnoreFile)。
解决 :删掉 .eslintignore、.prettierignore,把这些路径加到 biome.json 的 files.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是报错文本,severity是warn/error,fix_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 版本冲突坑到怀疑人生的时刻?留言区聊聊。
相关资源
- Biome 官网:biomejs.dev
- Biome GitHub 仓库:github.com/biomejs/bio...
- Biome v2 发布说明:biomejs.dev/blog/biome-...
- GritQL 插件语法参考:biomejs.dev/linter/plug...