窗外的霓虹灯早已熄灭,外卖盒堆在桌角,咖啡杯里只剩下冰冷的残渣。我盯着屏幕上那一片红色的报错,手指悬在键盘上,不知道该从哪里开始。凌晨两点的办公室里,只剩下我和键盘的敲击声,空调的嗡嗡声像是在嘲笑我的无奈。这已经是这个月的第三个周末加班了,我甚至记不清上一次完整的周末是什么时候。朋友圈里,别人在晒聚会、晒旅行、晒美食,而我的朋友圈,永远是那句"又在加班"。我打开手机看了一眼时间,想着要不要给自己点个夜宵,但转念一想,还是算了,Bug 还没修完,哪有心情吃东西。
那些让人崩溃的瞬间
周五下午五点,我正准备收拾东西下班,产品经理的消息弹了出来:"这个充值页面的优惠券显示逻辑改一下,月卡用户和普通用户要区分开,周一上线。"我看着这条消息,心里咯噔一下,周五下午五点提需求,周一上线,这意味着什么?意味着我的周末又没了。我深吸一口气,回复了一个"好的",然后打开了 charge.vue 文件。2908 行代码扑面而来,密密麻麻的模板、逻辑、样式混在一起,像是一团乱麻。我开始在代码里搜索"优惠券"相关的逻辑,发现这个文件已经被改过无数次了,每次需求变更都会留下一些注释掉的代码,还有一些"临时方案",现在这些"临时方案"已经变成了"永久方案"。
vue
<!-- 月卡用户 -->
<view class="subscribe-lifepass" v-if="canUseLifePassBook == 'Y' && amountCalculate">
<view class="subscribe-lifepass-title">
{{ $t("order.lifePassTitle") }}
</view>
<view class="subscribe-lifepass-content" :class="[amountCalculate.personCount > 1 ? 'disabled' : '']">
<view v-if="amountCalculate.personCount == 1">-¥{{ amountCalculate.price }}
</view>
<view v-else-if="amountCalculate.personCount > 1">{{ $t("order.lifePassOnlyOne") }}
</view>
</view>
</view>
看起来很简单对吧?就是一个条件判断,显示或隐藏优惠券信息。但当我开始改的时候才发现,优惠券逻辑和月卡逻辑耦合在一起,价格计算分散在三个不同的方法里,还要考虑中英文切换的问题。更要命的是,改完这里,订单确认页 orderConfirm.vue 也要同步改,因为两个页面共用同一套价格计算逻辑。我花了整整一个晚上,才理清楚这些逻辑之间的关系,然后小心翼翼地修改代码,生怕改了这里,那里又出问题。每改一行代码,我都要在浏览器里刷新好几次,确保没有破坏原有的功能。一个"小细节",牵一发而动全身,这就是前端开发的日常。
周六晚上,我正在家里吃晚饭,手机突然震动了起来。我拿起手机一看,是测试在群里 @所有人:"线上紧急 Bug!用户预约课程时,选择多人后优惠券不显示了!"我的心一下子提到了嗓子眼,线上 Bug 意味着用户正在受影响,必须立刻修复。我放下碗筷,打开电脑,连上 VPN,开始排查问题。我立刻打开代码,在 orderConfirm.vue 里找到了优惠券显示的逻辑:
vue
<!-- 券 -->
<view class="subscribe-coupon" v-if="
!(
canUseLifePassBook == 'Y' &&
amountCalculate &&
amountCalculate.personCount == 1
)
">
这个条件判断看起来没问题啊?月卡用户单人预约时不显示优惠券,其他情况显示。我在本地环境测试了好几遍,都没有复现问题。我开始怀疑是不是测试环境的数据有问题,于是我让测试把线上的请求数据发给我。当我仔细检查 amountCalculate 的数据结构时,终于发现了问题:后端返回的 personCount 字段,在某些情况下是字符串 "1",而不是数字 1。JavaScript 的 == 比较,"1" == 1 是 true,所以单人预约时逻辑正常。但是当 personCount 是字符串 "2" 时,"2" > 1 也是 true,导致多人预约时优惠券被隐藏了。原来是类型转换的坑!这种问题在开发时很难发现,因为本地环境的数据都是规范的,只有线上环境才会出现各种奇怪的数据格式。我立刻修改了代码,把 == 改成了 ===,并且加上了类型转换:
javascript
// 修复前
amountCalculate.personCount == 1
// 修复后
Number(amountCalculate.personCount) === 1
凌晨三点,我盯着这行代码,想起了那句话:"JavaScript 是世界上最好的语言"(笑)。修复完这个 Bug,我又测试了好几遍,确保没有其他问题,然后提交代码,发布到线上。看着部署成功的提示,我长舒了一口气,但心里却没有一丝成就感,只有疲惫。这种紧急 Bug 修复,就像是在走钢丝,一不小心就会掉下去。而且最让人无奈的是,这种问题本来是可以避免的,如果后端接口规范一点,如果前端做好类型检查,如果测试覆盖更全面一点......但是在快节奏的开发中,这些"如果"都变成了奢望。
最让我崩溃的是这个需求:"用户可能有多个品牌的钱包余额,要支持选择不同钱包支付。"这个需求听起来很简单,不就是一个列表选择吗?但实际上,这涉及到复杂的状态管理和支付逻辑。用户可以选择钱包支付、微信支付,或者两者混合支付。每种支付方式都有不同的限制条件,比如钱包余额不足时要提示补差价,月卡用户不能使用某些优惠券,多人预约时不能使用钱包支付等等。这些规则交织在一起,形成了一个复杂的状态机,稍有不慎就会出现逻辑错误。
vue
<template v-if="isCollapseWallet && walletList.length != 0">
<view class="valueCard" v-for="(item, index) in walletList" :key="index">
<view class="valueCard-icon">
<image v-if="item.brand == 57" src="https://oss.xxx" />
<image v-if="item.brand == 58" src="https://oss.xxx" />
<image v-if="item.brand == 59" src="https://oss.xxx" />
</view>
<!-- ... -->
</view>
</template>
看起来很简单的列表渲染,但实际上要处理的逻辑非常复杂。首先是状态管理的问题,每个钱包的选中状态要互斥,用户只能选择一个钱包,而且钱包支付和微信支付也要互斥。其次是余额不足的判断,当用户选中的钱包余额不足时,要提示用户补差价,并且自动勾选微信支付。最后是支付类型的计算,钱包支付、微信支付、混合支付,三种情况对应的 payType 不一样,后端需要根据 payType 来处理支付逻辑。我花了整整一个下午,才理清楚这些状态之间的关系,然后写出了下面这段代码:
javascript
computed: {
selectedOtherWalletAmount() {
const selected = this.walletList.find(item => item.isPaySele);
return selected ? selected.walletAmount || 0 : 0;
},
payType() {
const isOthPay = this.walletList.some((item) => item.isPaySele);
if (isOthPay && this.useWxPay) {
return 6; // 混合支付
}
// ... 还有一堆判断
}
}
这段代码看起来很简洁,但实际上是经过无数次重构和优化的结果。最开始的版本,我用了一堆 if-else 来判断状态,代码又长又难维护。后来我改用 computed 属性来计算派生状态,代码变得清晰了很多。但即使这样,我还是担心会有遗漏的边界情况,所以我写了一个测试用例列表,把所有可能的组合都测试了一遍。测试的过程中,我又发现了好几个问题,比如用户快速点击时会触发多次支付请求,钱包余额更新不及时导致显示错误等等。每修复一个问题,就会发现新的问题,就像是在打地鼠游戏一样,永远打不完。
周日下午,我正准备休息一下,产品经理又发来消息:"英文版的充值页面,文案显示有问题。"我心里一阵无奈,这个项目要支持中英文双语,每次改功能都要检查两遍。我打开代码一看,果然又是中英文布局不一致的问题:
vue
<view class="discount" v-if="language == 'en-US'">
<text>+{{ item.rewardRatio }}%</text>
<text>FREE!</text>
</view>
<view class="discount" v-else>
<text>赠送</text>
<text>+{{ item.rewardRatio }}%</text>
</view>
中英文的布局逻辑不一样,导致样式也要分开写:
vue
<template v-if="language == 'en-US'">
<view class="value-infos-under value-infos-under-en">
<view>Regular Period Charge:</view>
<view>¥{{ item.amount }}</view>
</view>
</template>
<view class="value-infos-under" v-else>
续费价 ¥{{ item.amount }}
</view>
中英文的布局逻辑不一样,英文版要把百分比放在前面,中文版要把"赠送"放在前面。而且因为英文单词比较长,样式也要单独调整,不然会出现文字溢出或者换行的问题。整个项目里,这样的中英文判断有上百处,每次改一个地方,都要检查两遍,确保中英文版本都正常显示。有时候我在想,为什么不用国际化框架来统一管理多语言?但是项目已经开发了这么久,重构的成本太高了,只能继续用这种"土办法"。每次看到这些 v-if="language == 'en-US'" 的判断,我都觉得很无奈,这就是技术债的代价。
那些无力的瞬间
做前端开发这么多年,我遇到过无数次让人无力的瞬间。最常见的就是需求变动,产品经理说"这个功能先不做了,改成那个",或者"上周说的那个需求,这周又要改回来",甚至还有"能不能加个小功能?就一个按钮的事"。每次听到这些话,我都想问:你知道我为了这个功能,改了多少文件吗?你知道我为了实现这个逻辑,熬了多少个夜吗?但是我知道,问了也没用,因为在他们眼里,前端就是"改改页面",很简单的事情。他们不知道,每一个按钮背后,都有复杂的状态管理、数据交互、异常处理。他们不知道,每一次需求变更,都可能导致整个系统的重构。
还有沟通上的困难。我说"这个需求技术上有风险,需要重构",老板说"用户等不了,先上线再说"。我说"但是这样会留下技术债",老板说"技术债以后再还"。可是"以后",永远不会来。技术债就像滚雪球一样,越滚越大,最后变成了一个无法维护的巨型项目。每次打开代码,都能看到各种"临时方案"、"待优化"、"TODO"的注释,这些注释就像是墓碑一样,记录着那些被放弃的理想。我知道,总有一天,这些技术债会爆发,到那时候,可能就不是加班能解决的问题了,而是要推倒重来。
上线的压力也让人喘不过气来。测试说"这个 Bug 必须今天修完,明天要上线",我说"但是我还没测完其他功能",测试说"那你加班测"。于是,又是一个加班夜。有时候我在想,为什么总是这么赶?为什么不能多给一点时间,让我们把代码写得更好一点?但是我知道,在互联网行业,时间就是金钱,快速迭代才是王道。用户不会关心你的代码写得多优雅,他们只关心功能能不能用,Bug 会不会影响体验。所以我们只能在有限的时间里,尽可能地保证质量,然后祈祷不要出问题。
技术难点:Vue 组件的状态管理
这次加班,最大的技术难点是多个组件之间的状态同步。整个预约流程是这样的:用户在课程列表页选择课程,跳转到订单确认页,选择优惠券,选择支付方式,最后提交订单。这个流程看起来很简单,但实际上涉及到大量的状态管理。课程信息(courseInfo)、价格计算(amountCalculate)、优惠券列表(couponList)、支付方式(useWalletPay, useWxPay, walletList)、用户信息(vipInfo),这些状态之间相互依赖,任何一个状态的变化都可能影响其他状态。比如用户选择了优惠券,价格就要重新计算;用户切换了支付方式,可用的优惠券列表也要更新;用户修改了预约人数,支付方式的限制条件也要改变。这种复杂的状态依赖关系,如果处理不好,就会导致数据不一致、界面显示错误等问题。
我的解决方案是使用 Vue 的响应式系统来管理状态。首先,我把所有的派生状态都用 computed 属性来计算,这样可以确保状态的一致性。比如判断是否可以使用优惠券,我不是在每个地方都写一遍判断逻辑,而是定义一个 computed 属性:
javascript
computed: {
// 判断是否可以使用优惠券
canUseCoupon() {
if (this.couponList.length) {
return this.couponList.some((e) => e.canUse === "Y");
}
return false;
},
// 计算选中的其他钱包余额
selectedOtherWalletAmount() {
const selected = this.walletList.find(item => item.isPaySele);
return selected ? selected.walletAmount || 0 : 0;
},
// 动态计算按钮样式
getFooterBtnClass() {
const classList = ['footer-content-btn'];
const status = this.courseInfo.bookStatus;
const isDisabled =
!this.isAgree ||
![2, 3, 4].includes(status) ||
(status === 2 && this.preAppointmentCount < this.canChooseNums[this.currentIndex]);
if (isDisabled) {
classList.push('disabled');
}
return classList;
}
}
这样,无论在哪里需要判断是否可以使用优惠券,我都可以直接用 this.canUseCoupon,而不用担心逻辑不一致的问题。而且 computed 属性是有缓存的,只有依赖的数据变化时才会重新计算,性能也更好。
其次,我把所有的状态变更都封装成方法,而不是直接修改数据。比如切换支付方式,我定义了一个 changePayType 方法:
javascript
methods: {
// 切换支付方式
changePayType(type, item, index) {
if (type === 'wallet') {
this.useWalletPay = !this.useWalletPay;
if (this.useWalletPay) {
this.useWxPay = false;
this.walletList.forEach(w => w.isPaySele = false);
}
} else if (type === 'wx') {
this.useWxPay = !this.useWxPay;
if (this.useWxPay) {
this.useWalletPay = false;
this.walletList.forEach(w => w.isPaySele = false);
}
} else if (type === 'otherPay') {
this.walletList.forEach((w, i) => {
w.isPaySele = i === index ? !w.isPaySele : false;
});
if (item.isPaySele) {
this.useWalletPay = false;
this.useWxPay = false;
}
}
this.getAmountCalculate(); // 重新计算价格
}
}
这个方法看起来很长,但实际上逻辑很清晰:根据不同的支付类型,更新对应的状态,并且确保状态之间的互斥关系。最后调用 getAmountCalculate() 重新计算价格。这样做的好处是,所有的状态变更都在一个地方处理,不会出现遗漏或者不一致的情况。而且如果以后需要修改逻辑,只需要改这一个方法就可以了,不用到处找代码。
最后,我使用 watch 来监听关键状态的变化,自动触发相关的更新:
javascript
watch: {
currentIndex() {
// 人数变化时,重新计算价格
this.getAmountCalculate();
},
couponCodeList() {
// 优惠券变化时,重新计算价格
this.getAmountCalculate();
}
}
这样,当用户修改预约人数或者选择优惠券时,价格会自动重新计算,不需要手动调用方法。Vue 的响应式系统会自动追踪依赖关系,确保数据的一致性。
当然,在实现这些功能的过程中,我也踩过不少坑。最常见的就是数据类型不一致的问题,后端返回的数字字段,有时是字符串,有时是数字,导致比较和计算时出现意外的结果。还有异步数据依赖的问题,价格计算依赖多个接口返回的数据,如果加载顺序不对,就会出现数据不完整或者计算错误的情况。我不得不在每个接口调用后都加上 loading 状态的判断,确保所有数据都加载完成后再进行计算。另外,多个支付方式之间的互斥逻辑也很容易出错,稍不注意就会出现两个支付方式同时选中,或者都没有选中的情况。为了避免这些问题,我在每次状态变更后都会打印日志,检查状态是否正确,这样可以及时发现问题。
凌晨一点的反思
调试到凌晨一点多,我还反思个鸡毛啊,抓啊睡觉吧!!!
对着屏幕发呆。我想起了那些被我错过的周末,那些没能参加的聚会,那些没能陪伴家人的时光。我想起了朋友问我"你怎么总是在加班"时,我无奈的笑容。我想起了女朋友说"你是不是更爱你的代码"时,我无言的沉默。我知道,这不是我想要的生活,但我又能怎么办呢?这就是互联网行业的现状,这就是前端开发的日常。我只能告诉自己,再坚持一下,再坚持一下,总会好起来的。但我心里清楚,这样的"再坚持一下",可能会持续很久很久。
写在最后
如果你也是前端开发,如果你也在加班,如果你也遇到过这些问题,那么请记住:你不是一个人。我们都在这条路上,一边写着 Bug,一边修着 Bug,一边骂着产品经理,一边继续加班。我们都经历过需求变更的无奈,都经历过线上 Bug 的恐慌,都经历过技术债的折磨。我们都在深夜里对着屏幕发呆,都在凌晨里怀疑人生,都在周末里加班到天亮。但是,我们也在成长。每一个通宵调试的 Bug,都让我们对代码有了更深的理解,让我们学会了如何排查问题,如何优化性能,如何处理边界情况。每一次需求变更,都让我们学会了更灵活的架构设计,让我们懂得了如何解耦代码,如何提高可维护性,如何应对变化。每一个上线的项目,都是我们技术能力的证明,都是我们努力的结果,都是我们价值的体现。
所以,加油吧,前端人。虽然又是一个周末加班夜,虽然我只想哭,但明天太阳升起的时候,我还是会继续写代码。因为,这就是我们的工作,也是我们的热爱。我们热爱创造的过程,热爱解决问题的快感,热爱看到产品上线的成就感。即使有再多的加班,再多的 Bug,再多的技术债,我们依然会坚持下去。因为我们知道,每一行代码,都在改变着这个世界;每一个功能,都在服务着千千万万的用户;每一次优化,都在让产品变得更好。这就是我们的价值,这就是我们的意义,这就是我们继续前行的动力。
写于凌晨 1:23,办公室的灯还亮着,外卖盒已经堆成了小山,但代码,终于跑通了。晚安,世界。晚安,Bug。晚安,我的周末。明天,又是新的一天,又是新的挑战,又是新的代码。