ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

代码格式化实践:Prettier原理、配置与团队协作落地指南

代码格式化实践:Prettier原理、配置与团队协作落地指南 1. 代码风格之争是一场没有赢家的内耗统一格式化值得认真做我到现在都记得那个周五下午我跟同事在 code review 里为了一个对象到底要不要花括号换行争了四十分钟。双方都有理都能翻到对应的规范文档给自己背书但项目里始终没定下来结果就是今天你按你的写明天他按他的改。一个 pull request 里真正变化的业务逻辑可能就 5 行格式调整却占了 40 行评审的人看半天最想在意的优先级顺序反而被淹没了。后来我们把 Prettier 引入项目格式化这个事才从人的讨论里正式交出去。操作成本几乎为零但收益比预想中要大得多。“Prettier 代码格式化统一代码外观”这句话听起来像一句口号实际做的时候它解决的是非常现实的问题。只要团队超过两个人代码风格永远是隐藏的内耗源头。缩进是 tab 还是空格单引号还是双引号分号加不加函数参数多长换行每个点都能引发一轮讨论但每个点都没有本质的对错。1.1 格式分歧是怎么变成内耗的先从成本说起。代码评审最核心的目标是看逻辑、看边界、看设计但格式不统一的时候注意力会被大量消耗在无关紧要的差异上。写代码的人改了 3 行逻辑保存文件时把旁边的几个对象也顺手整理了diff 里就会出现一团“乱改”评审者必须逐一确认哪些是真实改动。最终结果往往是两类一类人变得麻木直接 approve把 bug 漏过去另一类人变得较真每次花半小时看格式团队开始互相觉得对方针对自己。加上多人并行分支格式问题还会放大合并冲突。你改这个文件第 80 行我也改同一段代码因为我重排了整个对象数组git 没法把两边的修改识别成“互不影响”一个简单的小功能合并变得痛苦。这类冲突从来都不是“解决不了”而是总在最不该出现的时刻冒出来打断你本来顺畅的合并节奏。再有就是新人入场成本。团队如果有一套自己的“潜规则”风格哪怕文档写得再细新人上手时还是会犯各种格式小错。老同事反复提醒新同事反复修正熟悉代码的时间全浪费在这种事情上。而有了统一格式化工具新人只要写完后按一次格式化样式就自动贴近团队规范。1.2 一致比正确更重要我曾见过很多团队在选格式化规则时争论“到底哪个风格更好”。如果你认真研究会发现每个说法都能找到知名项目做佐证Google 写 80 列Twitter 之前用 120 列有人坚持 2 空格缩进有人为 4 空格辩护。这些风格在它们自己的项目里都运转良好。所以团队引入 Prettier 时真正要达成一致的不是“哪套风格最优雅”而是“我们愿意接受哪套风格来降低协作成本”。Prettier 是一个高度 opinionated 的工具它故意把配置项做得很少目的就是减少争论。你不需要理解整套排版哲学只要把它作为项目的前置约定它会用同一种方式处理所有代码。第一次格式化完整个仓库的“外观”会有一个明显变化。代码长得越来越像机器生成的但这恰恰是优点——没有人会因为格式化问题在 code review 里浪费时间也没有人会因为个人偏好“美化”一段代码。如果你还在纠结要不要引入我的建议是直接引入。它不会帮你消灭所有技术债但至少能把风格维度的问题彻底清出讨论范围让代码评审回到它该关注的地方。2. Prettier 内部发生了什么AST、Doc 与幂等输出一部分人第一次接触 Prettier 时会觉得它不过是个“整理缩进和空格”的小工具。但当你跑过复杂代码、尤其是嵌套很深的 jsx 和链式调用后就会发现如果只靠正则做文本替换很难实现这么稳定的效果。2.1 从源码到 ASTPrettier 不是做“文本美容”Prettier 的第一步是把源码解析成抽象语法树AST。简单理解AST 是把代码从“字符串”变成“数据”每个标识符、括号、语句都变成树状结构里的节点。树的结构里只保存语法层面的含义源码里的空格、换行这些“格式信息”在解析阶段就被丢掉了。为什么要丢掉格式信息因为只有丢掉原始格式输出才能完全由规则决定。这也是它和传统“代码美化器”的区别。传统工具通常在文本层上做缩进修正今天改一下这里、明天补一下那里互相之间经常打架。Prettier 的做法是“推倒重印”解析成 AST然后用统一的打印算法重新生成一份代码。针对不同语言Prettier 内置了不同解析器。JavaScript 默认用 babel 解析器TypeScript 会用 typescript 解析器CSS 用 css 解析器Vue 文件则有专门对 template 与 script 的组合推断。这意味着你在工程里混用多种语法并不会有太大问题。遇到主流框架的动态模板时它也尽量按代码块的语义来解析而不是简单把文件当纯 HTML 处理。2.2 打印算法里的“分组”和“适合宽度”AST 生成之后真正决定输出结果的是一套打印算法。我尽量简化描述Prettier 先把 AST 转成一种中间表示“Doc”再读取我们配置的 printWidth。Doc 里有很多“分组”结构每个分组像是一个可折叠盒子盒子里的内容如果能在当前宽度内放得下就展示成一行如果放不下分组就变成多行展示。这就解释了一个常见的现象为什么同一段代码在 printWidth 80 和 100 下会有完全不同的换行方式。它不只关心缩进更像在尝试寻找一个“看起来最平衡”的打印结果。Prettier 并不在乎参数是对象还是数组只要整个分组超宽就会触发换行。一旦触发换行相关的子结构往往会同步展开这样视觉上更稳定。用一段简单代码做例子。printWidth 80 时Prettier 可能把对象数组打印成const tools [ { name: Prettier, type: formatter }, { name: ESLint, type: linter }, ];保持紧凑、一行能放下就绝不展开。可你如果把 printWidth 调到 40它就会自动变成下面这种完全展开的形态const tools [ { name: Prettier, type: formatter, }, { name: ESLint, type: linter, }, ];这类行为不是靠“数空格”硬算出来的而是打印算法在解析器给的语义结构上做布局决策的结果。理解这一点你就不会在配置 printWidth 时产生误判它不是简单的“每行最多多少个字符”而是每行超出配置后Prettier 会重新分配哪些内容可以并排、哪些内容需要分行。2.3 幂等性如何保证多次格式化结果一致还有一个很少被提起但非常重要的特性是输出结果的幂等性。所谓幂等就是格式化一次之后再格式化第二次结果保持不变。这个特性极其关键。如果工具第一次格式化完是一种结果第二次又变成另一种样子团队提交后永远处理不完“格式漂移”。Prettier 的打印算法在设计上严格保证这一点给定同一个 AST打印规则是确定性的没有随机因素也不会根据当前文件是不是“已经格式化过”而产生不同分支。我自己的经验是第一次把 Prettier 加到一个有历史包袱的项目时先跑完一次全量格式化生成一个大但干净的 baseline commit。之后所有代码改动都基于这个 commit格式化本身就不会再成为 diff 噪声。后续每次保存、每个 CI 检查代码输出都一模一样。项目里多了一位“永不吵架”的同事问题自然就少了。3. 从零开始引入 Prettier安装、配置项和首轮提交聊完原理进入实操。很多人一开始图省事直接在命令行全局安装 Prettier然后到处格式化。对你自己的小脚本也许没问题但团队项目里我强烈建议把它装到项目本地并在 package.json 里固定版本。3.1 本地安装还是全局安装全局安装最大的问题是版本漂移。今天你本机安装了 3.x明天同事 clone 项目后全局可能还是 2.x两边格式化同一个文件的输出会不一致。代码格式化作到一半发现“怎么他和我不一样”反而制造新的混乱。所以别用全局项目级安装才是正确姿势npm install --save-dev prettier如果是用 pnpm 或 yarn命令变为 pnpm add -D prettier、yarn add -D prettier原理相同。团队里统一使用 package-lock 等锁文件保证每个人跑的命令都是同一个版本的 Prettier。3.2 配置项不用背但要理解几个关键项Prettier 的配置很简单通常是项目根目录下的 .prettierrc 文件。你可以用 JSON、YAML 或 JS 后缀只要团队约定一致即可。默认配置对很多项目已经够用但大多数团队还是会根据自己的代码习惯做少量调整。我常用的一个最小配置长这样{ printWidth: 100, tabWidth: 2, useTabs: false, semi: true, singleQuote: true, trailingComma: all }这里面的几个参数都值得理解而不是照抄。printWidth 是期望的每行宽度不等于硬性限制但它影响分组换行tabWidth 是缩进级别useTabs 表示用空格还是 tab 缩进semi 控制是否加分号singleQuote 控制字符串是否优先用单引号trailingComma 控制多行结构末尾的逗号。配置项的行为和团队习惯之间的权衡我在下表里做了归纳配置项默认值常见调整影响范围printWidth80100 或 120代码在多少列时换行tabWidth22 或 4缩进宽度useTabsfalsefalse是否使用 tab 缩进semitruetrue 或 false语句末尾分号singleQuotefalsetrue字符串引号风格trailingCommaes5all多行时末尾逗号关于 trailingComma我特别想多说一句。很多人担心尾逗号会带来兼容问题但实际上现代编译器都支持多行数组和对象的尾逗号。把配置设为 all 之后后续新增一行时 diff 只显示新增而不是把上一行从没有逗号改成有逗号清晰很多。3.3 第一批格式化与提交顺序配置写好后先拿一个目录做试验不要一开始就格式化整个仓库。比如先格式化 src/componentsnpx prettier --write src/components/**/*.{js,jsx,ts,tsx,vue,css,scss}命令里的 --write 表示直接覆盖文件。如果只想检查有没有符合规范可以换成npx prettier --check src/components/**/*.{js,jsx,ts,tsx,vue}第一次全量格式化时大概率会生成大量 diff。建议先提交一个独立的“chore: format code with prettier” commit再继续做业务改动。很多团队没处理好这一步把格式化 diff 和功能开发混在一起评审时又变成一团乱麻。先跑一次、提交一次、让所有人拉取一次代码之后保持同步日常开发就干净了。别忘了一个文件.prettierignore。它的写法和 .gitignore 类似但很多人总是漏掉。至少要把 node_modules、dist、build、coverage 这些目录加进去否则全量格式化会扫描出一大堆不想动的文件node_modules dist build coverage package-lock.json pnpm-lock.yamllock 文件为什么也要忽略因为它是自动生成的格式一致性由安装工具保证没必要让 Prettier 再处理一遍而且每次都去格式化 lock 文件只会增大 diff。以后使用时每下载一个新依赖重新生成的 lock 文件会保持它自己的格式也不会被检查。4. 把 Prettier 融进日常VS Code、Sublime 与 uni-app 项目配置好 CLI 只是第一步绝大多数开发者希望“保存文件时自动格式化”。这一步要和具体编辑器配合处理方式有一点点区别。4.1 VS Code 与 Sublime 的日常姿势VS Code 是最容易接入的。在扩展市场安装 Prettier - Code formatter 插件后打开一个代码文件执行 ShiftAltF它会询问使用哪个格式化程序选择 Prettier 即可。想让默认格式化对所有文件生效可以在设置里加一句{ editor.defaultFormatter: esbenp.prettier-vscode, editor.formatOnSave: true }这两项配置的含义分别是“默认格式化器为 Prettier”和“保存时自动格式化”。对团队来说最好把这些配置放到 .vscode/settings.json 里提交到仓库保证新人打开项目就拥有一致行为。Sublime 的情况稍有不同。Sublime Text 的默认操作是 CtrlShiftP 打开命令面板输入 Prettier 之后执行格式化但前提是先通过 Package Control 安装对应包。如果你习惯用快捷键可以在 Key Bindings 里绑定一个方便的组合例如把 F12 映射到 prettier_format。很多人在网上搜索“sublime格式化代码快捷键”实际上就是这一步的操作。JetBrains 系 IDE 也有官方扩展快捷键是 CtrlAltL安装 Prettier 插件后把它指定为默认格式化器即可。不同编辑器快捷键略有差异但核心逻辑一样在保存前让 Prettier 跑一遍或者显式手动触发。4.2 保存时格式化与快捷键如何选我在实际项目里通常建议开启 formatOnSave再加一个明确的手动快捷键。理由很简单保存时格式化能覆盖绝大多数“忘记格式化就提交”的情况手动快捷键则用于处理那些不想被自动改动的局部文件或者保存前想先看一眼历史代码。但要注意一个反模式不要在 Git commit 阶段才对所有文件强制运行 prettier --write。如果开发者本地没有开启 formatOnSave提交时才发现文件被改了两个动作挤在同一个 commit 里体验非常差。更合理的结构是本地编辑器负责写时格式化pre-commit 钩子只做校验校验失败就提示开发者重新格式化。4.3 uni-app 项目中要注意的条件编译uni-app 是一个经常出现在代码格式化讨论里的场景。它的单文件组件和 Vue 基本一致但多了条件编译语法例如 JavaScript 里的// #ifdef H5、// #endif模板里的!-- #ifdef MP-WEIXIN --。Prettier 本身能识别 Vue 单文件组件也能格式化 script 和 template 部分但对这类条件编译注释需要认真验证。常见的现象是Prettier 不会删掉注释但可能因为换行规则把注释移动到另一行的上方或下方。JS 的条件编译块如果本来写在某个变量前格式化后注释仍会保持在语句附近通常不影响编译。但在模板里遇到条件编译注释和自定义标签嵌套时格式化后模板的缩进结构会变这不会直接改变组件功能但肉眼排查对不上会让人很烦躁。所以我的实践顺序是先在整个 uni-app 项目里安装 Prettier 并配置好 Vue 解析支持格式化完一个页面后分别用 H5 和微信开发者工具编译一遍确认条件编译块没有被破坏。没问题再全量铺开。另外注意把小程序原生项目目录通常是 unpackage 或 dist加进 .prettierignore避免格式化产物目录导致一系列无关 diff。5. 谁管格式谁管规则Prettier 与 ESLint 的职责分工刚接触前端工程化的人很容易把 Prettier 和 ESLint 搞混。这两个工具属于不同层面但都在代码质量前防线里所以需要讲清楚边界。5.1 ESLint 和 Prettier 的界限ESLint 的核心能力是静态分析它的规则会检查代码是否可能出错、是否使用了未定义变量、是否有不安全逻辑同时也包含一部分代码风格规则比如“禁止多余的括号”“字符串必须单引号”这类规则。问题在于ESLint 的风格规则和 Prettier 的格式化规则会重叠比如引号风格、缩进、逗号、空格。重叠之后你在编辑器里会看到相互矛盾的提示甚至 lint --fix 改完一种格式Prettier 又改回来形成死循环。解决冲突的标准方法是引入 eslint-config-prettier。这是一个 ESLint 共享配置作用是把 ESLint 中和 Prettier 冲突的格式化规则全部关闭。这样一来ESLint 只负责逻辑类规则Prettier 只负责排版视觉两者不再打架。安装命令npm install --save-dev eslint-config-prettier然后在 ESLint 配置文件里把 prettier 加到 extends 末尾module.exports { extends: [ some-config, prettier ], };5.2 prettier --check 进入 CI把 Prettier 和 ESLint 并行我建议在 CI 里用两个独立命令分别跑{ scripts: { lint: eslint . --ext .js,.vue, format:check: prettier --check . } }format:check 只读不写发现任何不符合 Prettier 输出的文件就返回非零退出码从而让 CI 失败lint 单独负责 ESLint 检查。合并请求流水线里两个命令按顺序执行开发者在本地跑完一遍就知道哪里不合格。5.3 pre-commit 里用 lint-staged如果每次提交前要对全部代码执行 Prettier项目大了以后会非常慢。更灵活的方式是用 husky 加 lint-staged只对本次暂存区里的文件执行格式化与校验。package.json 里大致配置如下{ lint-staged: { *.{js,jsx,ts,tsx,vue,css,scss,md,json}: [ prettier --write, eslint --fix ] } }这样提交时只会处理你要提交的文件已经通过校验的老文件不会反复被扫描速度会快很多。我也见过团队把 prettier --write 放在钩子里并直接修改暂存文件效果也很好只是要注意把 lint-staged 的后置处理写好避免改动后没有重新 git add。6. 排坑手册模板字符串、注释位置与全量格式化节奏Prettier 使用人群足够大它的坑大多不是“能不能用”的坑而是“为什么结果和我想的不一样”的坑。6.1 模板字符串与 embeddedLanguageFormatting模板字符串是相对容易踩坑的一个点。遇到一个长模板字符串比如一串包含 HTML 的字符串Prettier 会根据 embeddedLanguageFormatting 配置决定是否对它做内部格式化。默认值是 auto也就是它能识别出模板字符串中是 HTML、CSS 或 JS 时会对嵌入内容也做统一格式。这听起来很方便但当模板里是带占位符的 SQL 语句时格式化反而可能破坏语义或让排版更乱。我的做法是在需要保留的模板字符串前加/* prettier-ignore */注释或者在项目配置里把 embeddedLanguageFormatting 设为 off只让 Prettier 处理模板字符串外层结构不深入模板内部。// prettier-ignore const sql select id, name from users where status 1;如果某个正则表达式写得比较复杂也可以用同样的注释跳过。这个注释机制本质上是一个最小干扰开关解决的是“Prettier 很聪明但聪明有时过度”的问题。6.2 注释不是“随从”位置可能被重新安排另一个容易让人困惑的点是注释的移动。Prettier 不会删除注释但会尽可能把注释和它附属的代码保持在相近位置。它不是传统的“文本保留”而是解析器把注释作为 AST 的附加节点处理因此注释在重组换行时可能移动到相邻行。比如你原本写const data { // 这是数据 id: 1 };printWidth 足够短时Prettier 可能输出成// 这是数据 const data { id: 1 };语义没有变但注释从行尾被提到了上一行。这类变化平时没有任何影响可如果是条件编译注释、ESLint 行禁用注释就需要验证是否仍然作用于目标代码。我在 uni-app 和某些带自定义宏的代码里都遇到过类似情况格式化完必须重新编译一次。6.3 大项目首次全量格式化的节奏历史越久的项目第一次格式化产生的 diff 越大。有几点经验可以参考。第一先格式化源码目录再逐步扩展到配置文件、文档、测试快照快照文件通常不要格式化因为 jest 快照是一个比较基准格式化后会出现大规模快照变更。第二全量格式化建议放在一个独立分支或合并窗口里不要和即将上线的版本并行。第三如果仓库里有自动生成的 vendor 文件或第三方 SDK 源码要提前加入 .prettierignore否则会在后续版本升级时频繁出现无关 diff。我第一次整理一个老项目时开场就跑了npx prettier --write .结果把 generated files 也改了CI 里多出几百个文件 diff最后只能回滚重来。经验是先npx prettier --check .看看清单再排除不想动的目录确认无误后再加--write。7. Keil5 与嵌入式 C非典型场景的格式化选择在文章开头提到过搜索热度里有个有趣的关键词是“keil5 格式化代码”。很多嵌入式工程师也在面对代码外观不统一的问题而且 Keil 的编辑器体验和 VS Code 完全不在一个级别这个问题更麻烦。7.1 Keil5 这类嵌入式 IDE 的处境Keil MDK 自带编辑器不仅没有太强的代码补全和重构能力也没有一个靠谱的、内置的代码格式化方案。很多团队用 Keil 写 C 代码多年缩进混乱、括号折行不一致、switch case 对不齐全靠老员工手动整理。网上搜出的答案大多是装 AStyle 插件或者在外部对源文件跑格式化后再同步回 Keil这类方案能用但接入成本和维护成本都偏高。7.2 如果还是想用 Prettier支持 C/C 但要知道边界Prettier 从较新版本开始已经能够解析一部分 C/C 代码也可以对 .c、.h 文件执行格式化。命令行用法和其他语言一样npx prettier --write app/src/**/*.{c,h,cpp,hpp}但它对 C/C 的支持深度明显不如 JavaScript。复杂的头文件、宏定义、编译器特有的扩展语法Prettier 可能无法稳定解析遇到不认识的语法会直接报错。所以对 Keil 工程我不建议一上来就把全部源码交给 Prettier。可以先在文件量小的模块里测试确认它对你们项目里的宏替换、结构体定义、位域这些语法都没有破坏后再逐步铺开。7.3 更好的嵌入式统一方案clang-format如果目标是让 Keil 项目的代码外观统一我更推荐 clang-format。它是 clang 工具链自带的格式化器对 C/C 的语法支持更完整配置方式也比 Prettier 更贴近嵌入式场景。通过 .clang-format 文件可以控制指针符号靠左还是靠右、花括号是否另起新行、case 是否缩进等这些细节恰恰是嵌入式团队最常争论的问题。把 clang-format 接入 CI 或者在本地脚本里统一执行也能达到和 Prettier 类似的“提交前格式化”工作流。Keil 里没有像 VS Code 那样的保存自动格式化插件但很多项目会额外用 VS Code 或 CLion 配合插件替代 Keil 的编辑体验编辑保存时自动跑 clang-format再回到 Keil 编译。说到底格式化的核心思想都一样把“风格”从人身上拿下来交给确定性工具。前端后端用 Prettier嵌入式用 clang-formatuni-app 用带条件编译验证的 Prettier八仙过海目标一致。能让你和同事把时间花在真正值得花的地方这个工具就值得引入。
返回列表