
那天早上,刚坐下还没喝口咖啡,旁边工位的小李就转过椅子凑过来,一脸为难。
「哥,帮我看个东西。这是我写的商品列表,现在产品要加个『店铺专享』的标签。本来挺简单一个活儿……但我不敢动手。」
我问他为啥不敢。
他说:「这个组件被四个页面用着。我改一个地方,另外三个页面说不定就一起崩了。上次加个小功能,测试给我提了五个 bug,全是别的页面冒出来的。」
我把代码拉出来一看,心里咯噔一下——
1200 行。37 个 props。点开往下翻,满屏都是 if-else,跟乱麻一样。
我问他:「这组件咋变成这样的?」
小李挺委屈:「我一开始想得可好了,四个页面都是列表,长得也像,我寻思复用一下多省事啊,就抽了个『通用列表组件』出来。结果写着写着发现,这四个页面看着像,业务逻辑根本不是一回事。没办法,只能一个 if 一个 if 往上堆……堆着堆着就成这样了。」
那一刻我特别想拍拍他肩膀。因为这个坑,我自己当年也一模一样地踩过。
而小李缺的,恰恰不是技术。他 React 用得挺溜,hooks、Context 该会的都会。
他缺的,是一个很少有人专门教、但决定你三年后是被同事感谢还是被同事吐槽的能力——工程判断力。
今天咱们就掰开揉碎聊聊它。
先别被这个词吓到,它一点都不玄。
工程判断力 = 你在敲下这行代码的时候,能停半秒问自己一句:「这么写,三个月后会不会回过头来咬我一口?」
就这么简单。
它不是让你去学新框架。也不是让你去背设计模式。更不是让你把代码写得花里胡哨、显得很高级。
它是一种「在好几个看起来都还行的方案里,挑出那个最不容易让你将来后悔的」的能力。
新手和老手的差距,很多时候不在「会不会写」,而在这儿。新手想的是「这功能怎么实现」,老手在实现的同时,脑子里还有另一根弦:「这玩意儿以后好不好改?」
光说概念太空。我给你拆四个真实场景,全是日常开发里天天能碰上的。看完你大概率会「卧槽这不就是我」。
假设你手上有两个列表页面,乍一看长得跟双胞胎似的:
页面A(商品列表) 页面B(我的收藏)
┌─────────────────┐ ┌─────────────────┐
│ 商品名 | ¥价格 │ │ 商品名 | ¥价格 │
│ 库存:10 | [收藏] │ │ 收藏于:3天前|[删] │
└─────────────────┘ └─────────────────┘
新手的第一反应几乎都是:「这俩都是列表,都有商品名、有价格,我抽一个通用组件出来,两边一起用,多优雅!」
于是就有了这玩意儿:
// ❌ 一开始挺美好
<ProductList
items={products}
showPrice={true}
showInventory={true}
showFavoriteBtn={true}
showRemoveBtn={false}
showLastViewTime={false}
// ……三个月后变成了这样:
// showShopExclusive={...}
// showCompareBtn={...}
// highlightStyle={...}
// 数了数,props 整整 37 个
/>
刚开始你还挺得意,觉得自己写得真复用。
可现实是这样的:过了俩月,产品说要再上一个「推荐页」,样式得不太一样。你寻思「简单,加个 highlightStyle 不就行了」。结果你这边一改,收藏页的样式跟着崩了——因为它俩共用同一个组件啊。
然后你就开始打补丁:「那我加个判断,推荐页才用新样式。」一个 if。过几天又来个需求,又一个 if……
不知不觉,你就变成了文章开头那个小李。
老手看到这两个页面,不会先想「它俩像不像」,而是先问一句:
它俩长得像,但「关心的东西」是一回事吗?
掰开看:
你看,皮肤像,骨头完全不一样。既然业务上根本是两码事,为啥要硬塞进一个组件里互相添堵?
所以老手会很「不优雅」地直接写两个:
// ✅ 各写各的,反而清清爽爽
// HomepageList.jsx —— 首页商品列表
function HomepageList({ products }) {
return (
<div>
{products.map((p) => (
<div key={p.id}>
<h3>{p.name}</h3>
<span>¥{p.price}</span>
<span>库存:{p.stock}</span>
<button>收藏</button>
</div>
))}
</div>
);
}
// FavoritesList.jsx —— 我的收藏
function FavoritesList({ products }) {
return (
<div>
{products.map((p) => (
<div key={p.id}>
<h3>{p.name}</h3>
<span>收藏于:{p.addedDate}</span>
<span>我的笔记:{p.myNote}</span>
<button>删除</button>
</div>
))}
</div>
);
}
三个月后又要加推荐页?太好了,再写一个 RecommendationList 就完事,跟前面俩谁也不挨着。各自演进,互不打扰。
我知道你心里可能有个声音:「这不就是重复代码吗?老师不是说 DRY(Don't Repeat Yourself)吗?」
这恰恰是新手最容易误解的一点:DRY 说的是「不要重复同一段业务逻辑」,不是「不要写长得像的代码」。 长得像但业务不同,那叫巧合,不叫重复。强行合并巧合,才是真正的灾难。
坏判断 | 好判断 |
|---|---|
「看着像」就合并 | 「业务不同」就分开 |
props 越加越多,最后 37 个 | 每个组件都清清爽爽 |
改一处,崩三处 | 各改各的,互不影响 |
一年后:没人敢动 | 一年后:随便扩展 |
所以下次手痒想抽组件之前,先问自己一句:这俩的「像」,是会一直像下去,还是只是现在碰巧像?
后端新返回了一个字段 lastPurchaseTime(用户最后一次下单的时间)。
产品说,要根据这个字段做一堆「区分新老用户」的逻辑。你二话不说,哪儿要用就在哪儿写:
// 首页
if (user.lastPurchaseTime) {
showPromotion(); // 买过的用户才给看优惠
}
// 商品详情页
if (Date.now() - user.lastPurchaseTime > 90 * 天) {
showBadge('热门'); // 90 天没买的,给看"热门"标签
}
// 搜索结果页
if (user.lastPurchaseTime) {
showPersonalizedRecs(); // 买过的才推个性化
}
// 邮件通知
if (Date.now() - user.lastPurchaseTime > 180 * 天) {
sendRecallEmail(); // 180 天没买的,发召回邮件
}
// 活动页
const discount = user.lastPurchaseTime ? 0.9 : 0.95;
写的时候特别顺,一个需求一个 if,五分钟搞定,下班!
然后……半年后的某个下午,产品端着咖啡过来了:
「咱们以后不用『最后购买时间』判断新老用户了,改成『最近一个月有没有活跃登录』。」
你嘴上说「行没问题」,心里已经开始冒冷汗——因为这个判断,你散在了五个文件里。
你得一个一个去翻、一个一个去改。改完首页、详情页、搜索页、邮件……眼看就改完了,结果活动页那个 discount 你给忘了。
上线,用户投诉:「我明明是老用户,怎么折扣比新人还少?」
排查、复盘、写事故报告。一个下午没了。
老手看到「区分新老用户」这种需求,脑子里会亮一盏灯:
这个『判断用户活不活跃』的定义,明显会在一堆地方用到。它不该是散装的,它该有个『老家』。
于是先建个文件,把这个「概念」供起来:
// userStatus.js —— 「用户状态」的唯一定义处
// 整个项目,只在这里说清楚"什么叫活跃用户"
exportfunction isActiveUser(user) {
if (!user.lastPurchaseTime) returnfalse;
returnDate.now() - user.lastPurchaseTime < 90 * 24 * 60 * 60 * 1000;
}
exportfunction isInactiveUser(user) {
return !isActiveUser(user) && user.lastPurchaseTime;
}
然后各个页面,都不自己判断了,统一来这儿「借」:
// 首页
if (isActiveUser(user)) showPromotion();
// 商品详情页
if (isInactiveUser(user)) showBadge('热门');
// 邮件通知
if (isInactiveUser(user)) sendRecallEmail();
现在产品再来改需求?你只需要打开 userStatus.js,改 isActiveUser 这一个函数。 改完,全项目自动生效,一处不漏。
整个过程从「改一下午还漏一个」变成「改一分钟,下班」。
坏判断:逻辑散落五处 → 改五个地方 → 一定漏一个 → 线上事故
好判断:逻辑收归一处 → 改一个地方 → 全自动同步 → 安心下班
判断的关键就一句话:当你发现一段逻辑要在第二个地方写的时候,就该警觉了——它是不是该有个『老家』?
这个场景特别常见,尤其是小公司、创业团队。三周要上线 MVP,时间紧任务重,于是代码就写成了这样:
// HomePage.jsx —— 啥都往里塞
function HomePage() {
useEffect(() => {
fetch('/api/products') // API 调用在这
.then((r) => r.json())
.then((data) => {
// 业务逻辑也糊在这
let result = data.filter((p) => {
if (currentUser.vip) {
return p.price < 1000; // VIP 看贵的
} else {
return p.price < 500; // 普通用户看便宜的
}
});
// 价格处理还糊在这
result = result.map((p) => ({
...p,
showPrice: currentUser.vip ? p.price * 0.9 : p.price,
}));
setProducts(result);
});
}, []);
return<div>...</div>;
}
能跑吗?能跑。上线了吗?上线了。当下看,一点毛病没有。
可问题是,三个月后需求一定会变,而这时候你会发现:
老手赶起时间来一点不含糊,但他有个习惯:写之前先想一秒,这堆东西里,哪些以后可能会变?把它们拎出来单独放。
// 第一步:API 调用,拎出来
asyncfunction getProducts() {
return fetch('/api/products').then((r) => r.json());
}
// 第二步:筛选逻辑,拎出来
function filterByUserType(products, user) {
const limit = user.vip ? 1000 : 500;
return products.filter((p) => p.price < limit);
}
// 第三步:价格逻辑,拎出来
function applyDiscount(product, user) {
return {
...product,
showPrice: user.vip ? product.price * 0.9 : product.price,
};
}
// 组件本身,瞬间清爽
function HomePage() {
useEffect(() => {
getProducts()
.then((data) => filterByUserType(data, currentUser))
.then((data) => data.map((p) => applyDiscount(p, currentUser)))
.then(setProducts);
}, []);
return<div>...</div>;
}
注意,这个版本写起来并不比上面那个慢,就是多拆了三个小函数而已。但三个月后的待遇天差地别:
API 路径变了? → 改 getProducts() 一个函数 ✅
VIP 折扣调了? → 改 applyDiscount() 一个函数 ✅
想加缓存? → 在 getProducts() 里加几行 ✅
坏判断:全糊一起 → 写得快,但一碰就碎
好判断:各归各位 → 写得一样快,但稳如老狗
快没有错,糊在一起才有错。下次赶时间之前,花一秒问问自己:这里头,哪些是『以后大概率要动』的?把它们先隔开。
这个坑,越是有点经验、有点追求的工程师越容易踩。因为它看起来特别「有远见」。
某个团队发现项目里表单到处都是,长得还都差不多,于是拍板:「咱们做一个『通用表单引擎』吧,以后所有表单一个配置就生成,多牛!」
吭哧吭哧搞了俩月,功能确实强大:JSON schema 自动生成表单、字段条件显示、动态校验、字段联动……应有尽有。
// 表单引擎 —— 3000+ 行,条件分支密密麻麻
const formConfig = {
fields: [
{
name: 'type',
type: 'select',
showIf: (data) => data.step === 1, // 条件 1
},
{
name: 'price',
type: 'number',
showIf: (data) => data.type === 'electronics', // 条件 2
validate: (val, data) => {
if (data.type === 'electronics') {
// 又是一堆条件……
}
},
},
// ……还有几十个字段,每个都带一串条件
],
};
第一个表单,用着挺爽。第二、第三个,也能凑合。
然后画风开始崩了:
当初为了「省事」造的轮子,最后成了全组的噩梦。
老手不会上来就造引擎。他会很务实地说:
我现在手上就三个表单,先看看它们『真正共同』的部分是啥。
一看,三个表单真正一样的,其实就那么点:文本输入框、下拉选择框、错误提示。
那就只抽这几个最稳的小积木:
// 最基础的小积木,几乎永远不用改
function TextField({ label, value, onChange }) {
return (
<div>
<label>{label}</label>
<input value={value} onChange={(e) => onChange(e.target.value)} />
</div>
);
}
function SelectField({ label, options, value, onChange }) {
return (
<div>
<label>{label}</label>
<select value={value} onChange={(e) => onChange(e.target.value)}>
{options.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
</div>
);
}
然后每个表单,用这些小积木自己拼,各拼各的:
// 电子产品表单 —— 自己的逻辑自己管
function ElectronicsForm({ onSubmit }) {
const [form, setForm] = useState({ type: 'electronics', price: '' });
return (
<form onSubmit={onSubmit}>
<SelectField
label="类型"
options={['electronics', 'clothing']}
value={form.type}
onChange={(val) => setForm({ ...form, type: val })}
/>
<TextField
label="价格"
value={form.price}
onChange={(val) => setForm({ ...form, price: val })}
/>
<button type="submit">提交</button>
</form>
);
}
function ClothingForm({ onSubmit }) {
// 服装表单,自己的逻辑,自己写
}
每个表单也就 50~100 行,谁来看都一目了然。
第四个表单要搜索功能?加在它自己组件里就行,不影响别人。第七个要跨字段校验?同样加在自己家里。
那什么时候才该把某个功能抽出来共用?等到你发现『有五个表单都在写一模一样的搜索逻辑』的那一刻——这时候抽,才是真需求,不是脑补。
坏判断:为想象中的需求造轮子 → 过度设计 → 越改越难
好判断:等真实需求出现了再抽 → 刚刚好 → 随时能扩
有句话送给你,写代码时贴在屏幕上都不为过:
「等它疼了再吃药,别没病先吃一柜子保健品。」
读到这你可能有个疑问:既然判断力这么重要,为啥学校不教、培训班不讲、网上也很少有系统教程?
原因很扎心,就一句话:判断力没有标准答案。
你看那些「确定的东西」,都有地方学:
可判断力呢?
答案永远是「看情况」——看这是什么项目、什么团队、处在什么阶段。
没有非黑即白,全是灰度。
这也是为啥很多人宁可一个接一个地刷新框架——因为框架有标准答案,学了就是学了,有成就感。而判断力,得在模糊里一点点摸,短期内还看不出效果。
但恰恰是这种「短期看不见、长期拉开差距」的东西,才是新手和高手真正的分水岭。
别灰心。判断力虽然没有教科书,但它不是天赋,是可以刻意练的。给你三个我自己一直在用的笨办法。
写完一个功能,别急着扑向下一个需求。在日历上给自己标个三个月后的提醒,到点了,回来看一眼:
「我三个月前抽的那个共享组件,现在到底有没有用?」
大部分人从来不回头看,写完就忘,所以踩一样的坑能踩好几年。就这么一个动作,你的判断力会比同龄人涨得快得多。
每次准备改一段代码,花十秒,过一遍这三问:
Q1:这是临时的改动,还是长期的?临时试验性的(比如新业务线试水),别太较真,能跑就行;核心功能、长期要维护的,那就值得多花点心思设计。
Q2:这个改动会牵连到几个地方?只影响这一处,随便改;影响五六处,那这逻辑就该收到一个统一的地方管。
Q3:万一这个决定是错的,改回来有多难?改回来很简单(比如临时加的样式),那就大胆试;改回来要命的(比如数据库表结构、对外的 API 格式),那就慎之又慎,多验证两遍再动手。
三个问题答完,你心里大概就有数了:这个决定到底是「随手就改」级别,还是「得开个会」级别。
这是性价比最高的一招。
跟团队里的资深工程师聊,别问「这个该怎么写」——那种答案文档里都有。要问:
「你见过哪些当年觉得很聪明、后来后悔得拍大腿的设计?」
一个老司机随口就能给你讲:
「我见过一个通用表单引擎,俩人写了俩月,结果四年了就三个表单在用,投入根本没回本。后来有个新项目,我直接拍板每个表单自己写,反而快了三倍。」
这一个真实的『翻车故事』,比你刷十篇理论文章都有用。 因为它把判断的「分寸感」直接喂给了你。
懒得记那么多?没关系,下次写代码做决定时,脑子里过这三道关就够了:
1. 这个"像",是稳定的吗?
├─ 只是暂时碰巧像? → 别合并,各写各的
└─ 是长期的固定模式? → 可以考虑共用
2. 这段逻辑,会到处重复出现吗?
├─ 第一次写 → 先就地写,别急
├─ 第二次写 → 开始警觉,考虑收口
└─ 第三次写 → 别犹豫了,抽出来统一管
3. 万一决定错了,代价多大?
├─ 巨大(改不动) → 慎重,多验证
├─ 中等 → 留条"后悔药",方便回退
└─ 很小(随手改) → 大胆做,错了再说
把这张图截下来,存手机里。下次手悬在键盘上犹豫的时候,拿出来对一对。
遇到的情况 | 新手的反应 | 老手的反应 |
|---|---|---|
看到代码重复 | 立刻抽象共用 | 等到第三次出现才抽 |
一个字段到处要用 | 哪儿要用哪儿写 | 收到一个地方统一定义 |
赶时间上线 | 全糊在一起 | 照样快,但拎出会变的部分 |
需求还不确定 | 做个"足够灵活"的大系统 | 只做"现在真需要"的 |
改一段代码 | 改哪算哪 | 先问"会不会牵连别处" |
下周,当你又要伸手改代码的时候,先停 30 秒。
就停半分钟,问自己三句话:
1. 我现在在共用什么?它会一直一样下去吗?2. 我现在在写的这段逻辑,会不会在别处重复出现?3. 我现在这个决定,三个月后的我,会想穿越回来揍我吗?
你回头看会发现,很多「当时觉得特别合理」的代码决定,其实都是在给几个月后的自己埋雷。
而所谓好的判断力,说穿了也不神秘——
就是在那个落笔的瞬间,比别人多踩了半脚刹车而已。
这半脚刹车,新手没有,老手有。它不靠天赋,靠的是一次次「三个月后回头看」攒出来的肌肉记忆。
从今天这篇开始练,不晚。
你有没有过这样一个时刻:当时觉得自己写得特聪明,三个月后却后悔得想删库?
来评论区交作业,就说两点:
每一个「踩坑现场」,都是在帮后面的兄弟姐妹排雷。点赞最高的那个故事,我会放进下期文章里,配上『怎么避坑』的拆解。 👀
要是这篇让你想起了某段不堪回首的代码,转发给你那个还在疯狂抽组件的同事吧,救他于水火。