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

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

相关推荐
谢尔登1 小时前
Vue3 响应式系统——computed 和 watch
前端·架构
愚公移码1 小时前
蓝凌EKP产品:主文档权限机制浅析
java·前端·数据库·蓝凌
欣然~3 小时前
法律案例 PDF 批量转 TXT 工具代码
linux·前端·python
一个小废渣3 小时前
Flutter Web端网络请求跨域错误解决方法
前端·flutter
符文师3 小时前
css3 新特性
前端·css3
ct9784 小时前
WebGL开发
前端·gis·webgl
C_心欲无痕4 小时前
前端页面渲染方式:CSR、SSR、SSG
前端
果粒蹬i4 小时前
生成式 AI 质量控制:幻觉抑制与 RLHF 对齐技术详解
前端·人工智能·easyui
WordPress学习笔记5 小时前
解决Bootstrap下拉菜单一级链接无法点击的问题
前端·bootstrap·html
Never_Satisfied5 小时前
C#插值字符串中大括号表示方法
前端·c#