需求解构:产品眼里的"常识",开发眼里的"雷区"
我们先来看看这次的业务背景:产品要求在首页(/home)针对老版本用户(版本号 < 1.0.6)推送一个升级弹窗。
产品给的约束听起来非常"合理":每天只准弹一次;不管是冷启动进来的,还是从别的页面退回来的,都要能触发。
听懂了吗?在老司机眼里,这短短两句话,直接爆出了三个技术雷区:
- "每天弹一次":不能用简单的布尔值,得防着用户跨时区,或者熊孩子调系统时间刷游戏体力。
- "版本号 < 1.0.6":传统的字符串对比在这里就是个笑话,你得教代码认识啥叫"语义化"。
- "退回来也要触发":兄弟,这可是开了 Keep-alive 的首页啊,生命周期它不按套路出牌!
核心技术实现的"防坑指南"(人脑主导,AI打工)
在让 AI 帮我写代码之前,我脑子里必须先有清晰的建模。如果连你自己都不知道要什么,AI 只会给你生成一坨漂亮的垃圾。
2.1 频率控制:滑动时间窗口的艺术
为了实现"每天只弹一次",很多新手喜欢在 localStorage 里存个 isPushed: true,然后每天半夜清空。这太脆弱了。我们弃用了这种标记法,直接上了基于时间戳的滑动窗口逻辑。
我给 AI 下了指令:"写一个 24 小时频率抑制逻辑,从 localStorage 读时间戳,注意防御异常数据。" AI 唰唰唰吐出了这段代码:
/** 24小时频率抑制逻辑 (AI与我的结晶) */
const UPDATE_POPUP_INTERVAL = 24 * 60 * 60 * 1000;
const canCheckAppUpdatePopup = (isTest) => {
if (isTest) return true; // 产品验收用的免死金牌
const lastTime = Number(localStorage.getItem(LAST_HANDLE_KEY));
// 重点来了:利用 Number.isFinite 防御数据被篡改为 NaN 的异常
return !Number.isFinite(lastTime) || Date.now() - lastTime >= UPDATE_POPUP_INTERVAL;
};你看这句 !Number.isFinite(lastTime)。如果是以前,我可能就随便写个 if(!lastTime) 敷衍了事了。但 AI 帮我补全了这种防御性编程的细节。并且,我们将状态判断(canCheck)、状态读取与变更完美解耦,强迫症看了直呼内行。
2.2 版本号比对:别让 1.0.10 变成 1.0.1 的弟弟
在处理 < 1.0.6 这个逻辑时,如果你敢用 parseFloat 去转版本号,那遇到 1.0.10 的时候,你的逻辑就会原地爆炸(因为 1.0.10 转出来是 1.0.1)。
我们必须遵循 Semantic Versioning(语义化版本)规范。这部分算法属于纯逻辑,AI 极其擅长,我一敲回车,完美的分段递归比对算法就出来了:
function compareVersion(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const maxLen = Math.max(parts1.length, parts2.length);
for (let i = 0; i < maxLen; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 < num2) return -1;
if (num1 > num2) return 1;
}
return 0;
}这不仅解决了三段式版本号的问题,就算以后产品丧心病狂地搞出 1.0.10.5,这段代码依然稳如老狗。
2.3 混合生命周期:Vue Keep-alive 里的僵尸组件
这是整个开发中最折磨人的地方。在xxx App 里,为了秒开体验,首页是被缓存在 Keep-alive 里的。这意味着什么?意味着组件的 onMounted 钩子只有在 App 冷启动,或者进程被杀掉重开时才会触发一次。当用户点进商城页买完东西,点返回回到首页时,面对的是一个"假死"的页面,onMounted 根本不理你。
这就需要我们精细化微操了:
- onMounted:负责拦截纯冷启动的场景。
- onActivated:专门用来蹲守用户从二级页面回退(Keep-alive 激活)的场景。
- 同时还要加上并发锁,防止这俩兄弟在某些极端机型上同时触发,导致同时发出两个网络请求。
AI 协同:从"补全代码"到"逻辑蒸馏"的爽文体验
作为在 2026 年写代码的打工人,我不吹不黑,直接上数据。在 Cursor 的加持下,这个需求的开发时间被压缩到了离谱的程度:
只花了10分钟!我这杯美式咖啡都还没凉。
除了快,AI 带来的最大红利其实是"代码下限的提升"。以前赶进度的时候,谁有空给你写完美的 try-catch?谁有空给你加 JSDoc 注释?结果这次,AI 在帮我写 fetch 请求时,自动在外层裹上了极其严密的异常捕获,甚至在 finally 块里主动重置了我的并发请求锁(isQuerying = false)。
说真的,如果在以前手动编码,这种锁的状态极其容易因为某个 return 分支的疏忽而变成"死锁",导致弹窗再也出不来。AI 直接用它的肌肉记忆,把这种低级 BUG 扼杀在了摇篮里。代码不仅跑得通,而且漂亮得像是一件艺术品。
架构复盘:为什么 AI 还不能取代你我的饭碗?
看到这里,你可能要问:"既然 AI 这么牛,那老板为什么还要发工资给你?" 好问题。在这次自测复盘中,我深刻体会到了一个真理:AI 能写出 100 分的"局部正确代码",但在"全局架构博弈"面前,它依然是个连职场潜规则都不懂的实习生。
在这个组件里,我无情地驳回了 AI 提出来的一个看似聪明的方案。
AI 刚开始生成的代码,是让 AppUpdateDialog 组件内部自启动。也就是说,这个组件一挂载,它自己就去查接口、自己决定弹不弹。听起来很独立对不对?完全解耦?
大错特错。 我立刻动手把代码改了,坚决引入了控制反转(IoC)。我用 Vue 的 defineExpose 把检查更新的函数 checkUpdate 暴露给了父组件(首页),让父组件来决定什么时候调用它。
我的理由是什么?因为 AI 根本不知道,咱们这破首页上,除了升级弹窗,还特么有"开屏广告弹窗"、"新手大礼包弹窗"、"实名认证强制弹窗"!如果每个组件都像 AI 写的那样"自启动",用户一开 App 就会看到四五个弹窗像叠罗汉一样糊在脸上,产品经理绝对会提着刀来找我。
我把弹窗的"检查权"和"触发权"上交给父组件,就是为了给未来的"全局弹窗优先级队列管理器"预留出架构空间。在这个系统里,父组件是交警,弹窗是汽车,谁先走谁后走,必须由交警指挥。这种基于业务大局观的架构权力,是开发者不可替代的护城河。
另外,AI 无法感知公司的"地下协议"。我们需要判断当前是不是在 App 内部。AI 给我的方案是去解析 navigator.userAgent。但我知道,咱们公司有个祖传的私有 JSBridge 协议——得手动接入 $xxxGlobalUtils.inapp() 才是最准的。这种属于团队业务环境的历史沉淀,你指望 AI 能猜出来?它只会给你标准答案,而你需要的是业务答案。
测试与回归:QA 看了直呼无聊
一个优秀的 10x 工程师,交付的代码必须是让 QA 挑不出刺的。为了验证 AI 产出的可靠性,我自己搞了个三维度的测试矩阵:
- 时间旅行测试:通过控制台把 localStorage 里的时间戳强行往前扣掉 25 个小时,刷新页面,弹窗应声而出。爽。
- 并发疯狂测试:利用代码强行让 onMounted 与 onActivated 在 1 毫秒内同时触发。打开 Network 面板,很好,拦截锁生效了,接口只发了一次。
- 跨端回归测试:把页面扔到普通的手机浏览器(非 App 壳子)里,环境判断逻辑生效,组件保持死一般的寂静,没有任何误触发。
一套打完,直接提 PR,下班回家。
总结:2026年的开发基线
复盘完这次 AppUpdateDialog 的开发,我其实特别想跟还在焦虑"前端已死、AI 抢饭碗"的同行们说几句掏心窝子的话。
我们现在验证了一个绝对核心的观点:AI 是效能的放大器,而人脑的经验,才是逻辑的边界锚点。
在这个结对编程的新常态里,分工已经非常明确了:
- AI 负责"广度"与"体力活":去覆盖所有的语法细节、去写又臭又长的正则表达式、去写无聊的容错捕获和注释。
- 人负责"深度"与"决策权":去处理生命周期的微操、去打通跨系统的恶心通讯、去规划复杂弹窗的优先级策略、去背架构演进的锅(和功劳)。
所谓的 10x 工程师,在以前是个神话,是指那种敲键盘快到冒烟、能同时记住 100 个 API 参数的天才。但现在,10x 工程师的定义变了——它是指那些能够熟练驾驭 AI 杠杆,把自己从"搬砖"中解放出来,将全部精力聚焦于业务建模、架构决策和复杂交互逻辑上的技术玩家。
不要惧怕工具,不要抗拒进化。当别人还在手敲 <div> 的时候,你已经在思考组件的高阶控制反转了,这才是真正的降维打击。
博主结语:如果你也对"前端多弹窗优先级调度算法"或者"如何优雅地驯服 AI 助手"有独到见解,欢迎在下方评论区跟我互喷探讨,咱们下期见!
发表评论
分享你的想法和反馈