又是一个周末加班夜,前端的我只想哭…

窗外的霓虹灯早已熄灭,外卖盒堆在桌角,咖啡杯里只剩下冰冷的残渣。我盯着屏幕上那一片红色的报错,手指悬在键盘上,不知道该从哪里开始。凌晨两点的办公室里,只剩下我和键盘的敲击声,空调的嗡嗡声像是在嘲笑我的无奈。这已经是这个月的第三个周末加班了,我甚至记不清上一次完整的周末是什么时候。朋友圈里,别人在晒聚会、晒旅行、晒美食,而我的朋友圈,永远是那句"又在加班"。我打开手机看了一眼时间,想着要不要给自己点个夜宵,但转念一想,还是算了,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。晚安,我的周末。明天,又是新的一天,又是新的挑战,又是新的代码。

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax