少一点口头规范,多一层工程护栏
少一点口头规范,多一层工程护栏
这篇文章整理自一次内部前端代码质量工具建设实践。为了方便公开分享,文中对公司名、包名、内部规则名、私有组件、仓库地址和业务数据都做了脱敏。示例只用于说明思路,不代表任何线上实现。
适合谁看
这篇文章更适合下面几类同学看:
- 前端工程师:想了解团队代码质量工具大致是怎么一回事。
- 工程效能 / DevOps 同学:在考虑怎么把质量检查落进自己的流水线。
- 对代码治理感兴趣的互联网同行:希望看点真实落地的经验,而不是理论推演。
文章不会深入编译原理或 AST 的底层细节,而是从工程视角讲清楚:我们为什么要做、它长什么样、技术上怎么搭起来,以及哪些地方我们也还没想得特别清楚。

难的不是写规范,而是让规范每天生效
在团队还小的时候,代码质量靠 Code Review、几份文档和工程师的自觉,多半能撑住。
但当项目变多、仓库变大、业务线之间开始互相影响之后,事情会慢慢变难:
- 规范散落在文档、群消息、评审经验里,新同学很难系统地掌握。
- Code Review 本来应该关注设计和边界,却经常被用来挑重复性问题。
- 老项目历史问题较多,一次性全量拦截,业务侧承接成本较高。
- 不同项目的技术栈和目录结构有差异,规则既要统一,又不能过度刚性。
- 治理如果只靠人工推动,很容易做成“一阵风”。
我们想做的,其实不是再发一份更完整的规范文档,而是一层更轻量的工程护栏:把那些高频、明确、能被自动判断的问题前置掉,让人可以把注意力留给真正需要判断的部分。
最后落到一个 CLI 工具
最后落下来的形态很朴素:一个命令行工具。
- 本地能跑,用来自查。
- 能接到 CI/CD,用来拦住新问题。
- 能输出控制台、JSON、表格等不同格式,给不同角色用。
从目标上看,它主要做三件事:
- 把团队约定沉淀成可执行的规则。
- 把代码问题变成可定位、可统计、可追踪的数据。
- 通过“增量扫描”让治理的心理成本可控:新代码先不增加债务,老代码逐步还。
我们有意没有把它做成一个很重的平台。初期阶段,一个可配置、可扩展、可接入流水线的 CLI,是最容易落地、最容易被项目接受的形态。
先从增量开始,别一上来吓到大家
下面这组数据做过区间化和脱敏处理,只用于说明趋势,不代表任何单一项目的真实指标。
- 扫描范围:在 PR 阶段从“全仓扫描”收敛到“变更文件 + 变更行过滤”,历史问题对当前提交的干扰明显降低。
- 扫描耗时:中等规模前端项目里,全量扫描通常是几十秒到分钟级;增量扫描多数情况下可以收敛到秒级到十几秒,主要受变更文件数和 CI 机器性能影响。
- 问题暴露:全量报告里可能一次出现数百条历史问题;增量报告通常收敛到个位数到几十条,更适合作为 PR 反馈。
- 接入节奏:首次接入时先只上报、不阻断,观察 1 到 2 个迭代后,再把误报较低、共识较高的 error 规则逐步纳入准入判断。
从结果看,增量扫描带来的最大变化不是“工具更快”这么简单,而是让质量反馈从“历史问题清单”变成了“这次提交需要关注什么”。这会显著降低团队第一次接入的心理压力。
它大概能看哪些问题
规则大致分成几类(这里使用脱敏后的描述):
- 安全与稳定性:不安全的 JSON 解析、空的 catch、明文敏感信息、直接操作敏感浏览器能力等。
- 国际化与多地域适配:硬编码文案、硬编码地区 / 语种判断等。
- 业务一致性:金额展示、金额输入、敏感输入、多因子验证等需要使用统一基础能力。
- 性能与体验:重复串行请求、弹窗中的重复初始化、图片组件迁移等。
- 可维护性:函数过长、循环依赖、嵌套三元、显式 any、未使用代码 / 依赖等。
- 工程约束:目录结构、样式引入方式、内联样式等。
我们有一个比较明确的取舍:工具更适合检测“稳定、明确、有共识”的问题。需要很多业务上下文判断的规则,我们宁愿先输出 warning、收集数据,也不轻易做强拦截。

整体就是一条朴素的流水线
工具内部是一条分层的流水线,整体设计保持克制:
CLI 命令层
↓
配置加载 & 任务编排
↓
文件收集 & 解析
↓
规则执行引擎
↓
问题过滤 & 评分 & 报告生成每一层的职责都尽量单一:
- CLI 命令层:解析扫描路径、输出格式、配置文件、增量 diff 范围等参数。
- 配置加载:合并默认预设、项目配置、环境变量、命令行参数。
- 文件处理:收集待扫描文件,应用忽略规则,跳过构建产物、依赖目录等。
- 解析服务:对不同语言生成 AST 或结构化数据。
- 规则引擎:注册规则,判断是否启用,统一收集上报的问题。
- 评分模块:把规则触发情况换算成一个粗粒度分数。
- 报告模块:输出控制台、JSON、表格等格式。
这种分层的好处是:每一条规则的作者只需要关心“怎么识别问题”,其他事情交给引擎来做。

为什么不用简单字符串匹配
最早确实尝试过只用字符串或正则。简单的场景足够用,但规则一多就会碰壁:
- 同一个调用出现在不同上下文里,含义不一样。
- import 别名、对象属性、链式调用都会让正则不稳定。
- JSX、TypeScript、装饰器等语法会进一步干扰简单匹配。
- 问题需要精确的行列信息,方便报告和编辑器跳转。
所以我们最终在 JS / TS / JSX / TSX 等文件上,统一使用一个宽容模式的 AST 解析器,带上常见语法插件(类属性、装饰器、JSX、TypeScript 等),并开启错误恢复。这样即使某些文件的语法风格不完全统一,也不至于直接中断扫描。
一条规则长这个样子(脱敏版):
interface Rule {
id: string;
name: string;
severity: 'error' | 'warn' | 'info';
enabled: boolean;
check(context: RuleContext): Promise<void> | void;
}规则通过 context.report() 上报问题,引擎会统一补齐文件路径、位置、严重程度、规则 ID 等信息。规则本身尽量“只做一件事”,这也是后面能持续加规则而不互相影响的前提。
一个例子:为什么要管空 catch
以“空 catch 检测”为例。这个规则本身不复杂,但很适合自动化,因为它同时满足几个条件:团队共识高、误报相对可控、问题定位明确、修复建议也比较清晰。
一个简化后的问题代码可能是这样:
try {
await submitForm(values);
} catch (error) {
// 忽略异常
}这类代码的问题不在于一定会立刻导致故障,而是它会让失败被静默吞掉。用户侧可能没有反馈,监控侧也可能没有线索,后续排查成本会被放大。
更合理的处理方式通常是至少做一件明确的事:
try {
await submitForm(values);
} catch (error) {
logger.warn('submit form failed', error);
showToast('提交失败,请稍后重试');
}规则实现时并不需要理解完整业务,只需要在 AST 中找到 CatchClause,再判断 catch body 是否为空、是否只有注释、或者是否只有缺乏后续处理的占位逻辑。为了避免过度打扰,早期可以把这类规则先设为 warning,等命中情况稳定后再决定是否升级。
增量扫描:先守住新代码
如果只能全量扫描,质量工具会遇到一个很现实的问题:老项目里历史问题太多,一扫几百上千条,大家反而不知道该从哪开始改。
所以我们把增量扫描作为核心能力之一,而不是一个“可选项”。
它的思路其实不复杂:
- 通过
git diff找到这次变更的文件、新文件行号范围(hunk)。 - 只对变更文件执行规则扫描。
- 扫完之后,再按问题所在行号过滤一轮,只保留落在变更范围内的问题。
- 同时产出两份结果:
- 变更文件完整问题(让你看到这个文件整体还欠多少债)。
- 本次变更行上的问题(真正用于准入判断的)。
git diff
↓
解析变更文件与 hunk 行号
↓
扫描变更文件
↓
规则发现问题
↓
按变更行过滤
↓
输出增量报告
这个设计的价值不在算法上,而在心理预期上:历史问题可以慢慢治理,但新代码尽量不再继续欠债。这个共识并不难达成,真正落到工具上,关键就是要有这一层过滤。
Diff 解析里的一些细节
增量扫描主要依赖 git diff 的 hunk 信息,例如:
@@ -10,0 +11,3 @@我们关心的是“新文件中的起始行”和“变更行数”,把它构建成一个按文件聚合的映射:
type DiffMap = Map<string, Array<{
startLine: number;
lineCount: number;
}>>;后续判断一个问题是否“属于本次变更”时,只需要看它的起始行是不是落在任一 hunk 范围内。
实际落到真实仓库,有几个不复杂但很真实的细节需要处理:
- 删除文件:没有新代码,不需要扫描,但要从变更文件列表里正确剔除。
- 重命名文件:要保留新旧路径映射,否则报告里显示的路径和实际 diff 对不上。
- 大 diff:
git diff输出可能非常大,需要设置超时和缓冲区上限,不然容易挂掉 CI。 - CI 的浅克隆:很多流水线默认只拉很少的历史,导致
origin/main...HEAD这种范围根本不可用,需要显式设置拉取完整历史。 - 路径规范化:绝对路径、相对路径、反斜杠 / 正斜杠要统一成同一种形式,否则匹配总会差一点。
这些点单独看都很普通,但如果不处理好,工具会一会儿跑得通、一会儿跑不通,反而影响团队对工具的信任。
配置:默认能用,也允许慢慢调
工具提供几种预设:默认模式、严格模式、精简模式。项目也可以通过一个配置文件覆盖规则启用状态、严重程度、忽略路径、输出格式等。
配置合并遵循这样的优先级:
命令行参数 > 项目配置文件 > 环境变量 > 默认预设这个顺序比较符合直觉:
- 临时跑一下的时候,命令行参数最直接。
- 项目长期约定放在配置文件里。
- 敏感信息(比如 AI 相关的 Token)走环境变量。
- 预设保证什么都不配也能跑。
脱敏后的配置示例:
module.exports = {
preset: 'default',
rules: {
'no-empty-catch': {
enabled: true,
severity: 'error',
},
'max-function-lines': {
enabled: true,
severity: 'warn',
options: { max: 120 },
},
},
ignorePatterns: [
'node_modules/**',
'dist/**',
'**/*.test.{ts,tsx}',
],
outputFormat: 'console',
};有两个细节比较关键:
- 配置项的默认值由规则自己声明,引擎只在“配置文件里有明确设置”时才覆盖它。这让规则本身成了“单一事实来源”。
- 忽略规则用的是类似 glob 的语法,在部分匹配场景做了轻量适配,方便后续支持更精细的项目级过滤。
评分只是参考,别被数字绑架
评分系统的目的不是制造压力,而是给团队一个“粗粒度的健康度参考”。
我们没有按 issue 数量直接扣分,而是按“触发了多少类规则”扣分。原因是:同一个问题模式,在一个文件里可能重复出现几十次,如果按数量扣分,分数会被某个局部问题过度放大,但实际修复成本未必同等增加。
脱敏后的思路如下:
最终得分 = 100 - 错误级规则数 × A - 警告级规则数 × B- 错误级规则代表安全、稳定、强一致性问题,权重较高。
- 警告级规则更多是可维护性 / 改进建议,权重较低。
- 信息级规则只提示,不扣分。
分数最后会映射到一个“优秀 / 良好 / 中等 / 及格 / 不及格”的档位,主要用来沟通,不适合做硬指标。
真正有价值的其实是:
- 问题数量的趋势(在变好还是变差)。
- 规则分布(是集中在几条规则,还是普遍分散)。
- 单次 PR 的增量结果(新代码还在不在制造同类问题)。
报告要让不同人都看得懂
一个工具如果只能输出到终端,很难同时服务本地开发和平台化消费。所以我们有三种报告:
- 控制台报告:本地开发用,格式紧凑、带颜色、带编辑器跳转链接。
- JSON 报告:流水线用,适合做二次处理或在平台上展示。
- 表格报告:阶段性盘点用,适合在治理复盘或跨团队沟通时对齐节奏。
报告里会包含文件路径、规则 ID、严重程度、位置、简要建议等信息。对于本地场景,还会生成指向编辑器的跳转链接(例如 VSCode 的 vscode://file/...),尽量减少“看到问题但找不到位置”的摩擦。

CI 接入:更适合在 PR 阶段提醒
在 CI 上,我们更推荐在 PR 阶段跑增量扫描,而不是每次都全量扫。脱敏后的一个典型配置:
name: Code Quality Check
on:
pull_request:
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm install -g your-code-quality-cli
- run: your-code-quality-cli scan ./src --diff origin/main...HEAD --output json --out-file quality-report.json这里有几个实践中比较容易忽略的点:
fetch-depth: 0非常关键。如果 CI 只拉了一个很浅的历史,origin/main...HEAD这种 diff 范围就会失败,工具看起来一切正常,其实并没有扫到真正变更的行。- 第一次接入时,不要开强拦截,先跑一段时间、看看误报分布再逐步升级为阻断。
- 最好同时产出“完整问题”和“增量问题”两份报告,一份用于盘点,一份用于准入判断。
做规则时的一些经验
做完这一轮之后,有几个自己反复验证过的感受:
第一,规则应该从高共识的问题开始。空 catch、危险 API、明文敏感信息、明显偏离基础设施的写法,这类问题大家不太会吵,上线阻力小,也更容易建立对工具的信任。
第二,规则需要分级,不要一开始就全 error。强安全、强稳定问题设 error;风格、复杂度、迁移建议更适合先设 warn。过早阻断容易让团队把工具理解成额外负担,而不是工程辅助。
第三,误报成本要认真对待。工具一旦频繁误报,大家很快就会失去耐心、甚至开始整体忽略。边界不清的规则,我们更倾向先只上报、不阻断,收集数据之后再决定是否升级。
第四,治理节奏比规则数量更重要。一次上线几十条规则看起来很完整,但未必有效。更可持续的是小步上线、观察反馈、修正规则、再推广。
还没做好的地方
说实话,这个工具离“好用”还差不少:
- 规则适用范围可以更精细,比如按文件类型、目录、框架动态启用。
- 解析能力可以继续扩展,Vue / 样式文件 / 跨文件依赖的覆盖度还能更强。
- 报告更应该关注“趋势”,而不只是“本次快照”。
- CI 阻断策略可以更灵活,例如只阻断 error,warning 走评论提醒。
- AI 分析目前更适合做辅助解释,不应该替代确定性规则;我们在边界上还在摸索。
这些问题不影响它现在发挥价值,但会决定它能不能从“能用”真正走向“好用”。
最后回头看
回过头看,这个工具最大的价值其实不是“写了多少条规则”,也不是“扫得多快”,而是:
把团队中原本分散在文档、评审经验、群聊里的工程共识,变成了一种可以运行、可以反馈、可以持续迭代的机制。
代码治理这件事,很难靠一次专项行动完成。更现实的路径是先守住新代码,再逐步改善旧代码;先自动化明确问题,再把人的注意力留给复杂判断。
如果一定要用一句话概括的话,大概是:我们不是想做一个“挑错工具”,而是想在日常开发里多加一层温和但稳定的工程护栏。它不替代人,但可以让人少被重复问题打扰。
2026 © Lizhenyui.