chooseImage 数量上限、compressImage 支持度等平台差异收敛到一层能力抽象里。改造后弱网场景下多图上传的成功率明显提升,低端机的压缩阶段闪退不再复现。
先说清这块和商品列表那边的「图片治理」不是一回事:那边是展示侧(懒加载、格式、尺寸、CDN 裁剪),这边是上传侧——用户从相册选图、压缩、传到我们的存储。约束完全不同。
第一版是最直觉的写法:chooseImage 拿到路径数组,Promise.all 全部压缩,再 Promise.all 全部上传。在我的手机上一次传五张图,两秒搞定。然后就收到了四类反馈。
一是弱网下全军覆没。用户在地铁里传图,十张全并发,带宽被瓜分,每一条都慢到超时,最后一张都没成功。而如果串行传,至少能成几张。并发在网络好的时候是优化,在网络差的时候是灾难——这一点我当时完全没想到。
二是低端机在压缩阶段闪退。并行压缩十张图,每张都要解码成位图,内存峰值直接超了低端机的上限。表现是应用闪退,用户以为是「传图会崩」。而我在旗舰机上测完全正常。
三是失败了要整批重来。Promise.all 有一个失败整个 reject,我的处理是提示「上传失败请重试」。用户已经成功传了八张,重试时又从第一张开始——不仅慢,还可能产生重复的图。
四是压缩把细节图压花了。我用了固定的压缩质量,对普通照片没问题,但用户传的商品瑕疵凭证(比如衣服的一个线头)压完就看不清了,客服无法判定,售后纠纷。
所以四个改动:逐张压缩(把内存峰值从「N 张」降到「1 张」)、限制上传并发数(在网络好和网络差之间取平衡)、每张图独立状态、失败只重试单张、压缩按最大边长和目标体积而非固定质量。
这个模块最想说的一句话是:客户端的难点不是实现功能,是「我的设备和用户的设备不一样」。并发、内存、网络这三件事在开发机上都不成问题,而它们恰好是这个功能的全部工作量。所以我后来养成的习惯是:任何涉及批量和媒体处理的功能,都要在低端机加弱网下测一遍。
Promise.all 的语义(一个失败全部 reject)在这里是错的,要用「各自结算」的方式收集结果。用户传了十张成了八张,重试的应该只是那两张。chooseImage 的数量上限、compressImage 的支持度、返回路径的形态各端不同。业务代码里不应该出现条件编译,应该调统一接口,差异关在抽象层里。逐张压缩的代价是总耗时明显变长,我认为必须接受。并行压缩快得多,但它把内存峰值推到低端机的上限之上——快和崩之间没有取舍空间。缓解办法是把压缩和上传流水线化:第一张压完就开始传,同时压第二张,这样用户感知的等待时间接近「压一张的时间 + 传全部的时间」而不是两段相加。这个流水线是纯收益,没有额外风险。
并发数选一个固定的小值,而不是按网络类型动态调。动态调听起来更聪明,但网络类型 API 的准确性有限(显示 4G 实际很慢的情况很常见),而且判断错了反而更糟。取一个在弱网下也能工作的小并发数,是更稳的选择——它在好网下损失一点速度,但在所有网络条件下都可用。这个取舍偏向了下限保障。
压缩失败时选择「降级为不压缩但校验体积」,而不是直接拒绝。某些端不支持压缩 API。直接拒绝会让那个平台的用户完全用不了这个功能。降级的代价是可能上传一张较大的图,所以配一个体积硬上限——超过上限的在不支持压缩的端上才拒绝。这是「尽量让功能可用,但守住底线」的思路。
踩过的坑一:在旗舰机上测完全正常,用户的低端机闪退。这个坑让我改变了测试习惯——凡是涉及批量处理和媒体解码的功能,必须在低端机上测。而且要测「用户会做的极端操作」:一次选满上限、选大尺寸原图、连续操作。我后来专门借了一台三年前的安卓机放在工位上,这件事的收益远超它的成本。
踩过的坑二:Promise.all 的语义用错了。它的「一个失败全部失败」在批量任务里几乎总是错的选择。批量任务需要的是「各自结算、汇总结果」,而不是「全成功或全失败」。这个认知后来在做任何批量操作时都在用——先问「部分成功是不是可接受的状态」,如果是,就不能用全或无的语义。
踩过的坑三:把存储的密钥写在客户端配置里。为了图快,直接用了存储服务的长期密钥。代码评审时被指出来:客户端代码在用户手上,反编译或者抓包就能拿到,等于把存储桶的写权限公开了。修法是服务端签发限时令牌并限定路径前缀。这件事让我理解了「客户端没有秘密」这个原则——任何打进客户端的东西都要假设它是公开的。
踩过的坑四:用户切后台十分钟回来,上传全失败且提示看不懂。临时文件路径失效,上传接口报了一个底层错误码,我原样弹给了用户。修法是检测路径可读性并给出人能理解的提示。这类问题的通用教训是:底层错误不能原样透给用户,要翻译成「他能采取行动」的话。
没做的部分:没做断点续传。单张商品图压缩后通常不大,重传一次的成本可以接受,而断点续传需要分片、记录偏移、服务端配合合并,复杂度和收益不成比例。如果业务变成上传视频,那断点续传就是必需的了——判据是「单个文件重传一次的代价能不能接受」。
弱网成功率:用开发工具的网络限速模拟弱网(或者真的去信号差的地方),一次选满上限的图,重复若干轮,统计「全部成功的轮次比例」和「平均成功张数」。要报后者——因为改造前的典型失败形态是「一张都没成」,改造后是「都成了或者少数几张要重试」,只看「全部成功率」会丢掉这个差别。测试要说清限速档位和图片张数,否则数字不可比。
低端机闪退「不再复现」:这是断言型结论,报法是「在指定机型上,一次选满上限的原图,反复操作若干次未再复现闪退」,并说明机型档位和内存大小。不要说「解决了内存问题」——你只能证明在测过的机型上不再复现。
内存峰值:用平台的性能面板看压缩阶段的内存曲线,对比并行与逐张两种实现。报峰值的量级差异(N 倍降到 1 倍)而不是精确 MB 数,因为它取决于图片分辨率。
压缩效果要按内容类型分别报。普通照片和细节图(文字、瑕疵)的压缩表现完全不同。只报平均压缩率会掩盖「细节图被压花」这个问题,而那恰好是我踩过的坑。做法是准备两组样本分别报体积和主观可辨识度。
耗时要拆开报:选择、压缩、上传三段各自多久。因为优化手段完全不同(压缩靠策略、上传靠并发)。只给总耗时无法说明你知道时间花在哪。
不要报什么:不要报「上传成功率 99.9%」——弱网下不可能,而且这个数字取决于用户网络而不是你的代码。该报的是「同样网络条件下的对比」。也不要报「压缩率 70%」——取决于原图质量,且掩盖了细节图的问题。
第一版的地址页很朴素:进页面就请求定位、拿到坐标做逆地理编码、把返回的地址字符串填进输入框、用户点保存。看着挺智能,四个问题都出在「我把权限和定位结果当成了可靠的东西」。
一是一进页面就弹权限,用户直接拒绝。他还不知道你要干什么就被弹窗打断,第一反应是点「拒绝」。而系统权限被拒绝一次之后,再想请求就弹不出来了(要引导他去系统设置里改),相当于永久失去了这个能力。这是最典型的时机错误。
二是权限被拒绝之后功能全废。我的代码只写了「拿到定位 → 填地址」这一条路径。用户拒绝之后,页面就一直转圈或者一片空白,他压根没法填地址,只能退出去。这个 bug 影响的是「所有拒绝过定位的用户」,比例不低。
三是逆地理编码的地址直接用,配送出问题。返回的是「某市某区某路 123 号」——这是坐标所在的位置,但用户住在 3 号楼 502 室,而这部分坐标里没有。我把它当完整地址存了,配送员到了楼下找不到人。坐标能告诉你「在哪个楼栏」,但告诉不了「几号房」,这个差距就是配送成功与失败的差距。
四是地址存成一个字符串,后面全是麻烦。运费要按省份算、有些商品有区域限售、仓库要按区域分配——这些都需要结构化的省市区,从一个字符串里去解析是不可靠的(用户可能自己改过、可能少写了市)。
所以四个改动:权限请求时机后置(用户点「定位」才请求,此时他知道为什么要权限)、权限被拒绝有完整的替代路径(手动选城市 + 手动填地址)、逆地理结果只做预填、门牌号强制补全、地址按省市区编码加详细地址结构化存储。
这个模块最想说的一句话是:权限不是能力,是用户的选择。所以任何依赖权限的功能,都必须有一条不依赖权限的路径——不是降级体验,是要能完成同一件事。用户拒绝定位之后照样要能填地址下单,这不是可选项。
权限请求后置的代价是「用户可能压根不点那个按钮」,也就是定位功能的使用率会下降。进页面就弹的话,一部分用户会顺手同意。但我认为这个取舍是对的:顺手同意的用户里有相当比例是被打断后随便点的,而被拒绝的代价是永久性的(系统不再弹窗)。后置之后同意的人是真的想用这个功能,而且没同意的人也能通过手动路径完成任务,业务上没有损失。判据是:这个权限是「锦上添花」还是「功能前提」——填地址属于前者(手动能填),所以可以后置;如果是扫码支付这种没有摄像头权限就完全做不了的,那就该在用户明确要扫码时请求,并把「为什么需要」说清楚。
门牌号强制补全 vs 直接用逆地理结果,选强制补全,接受多一步操作。直接用更「智能」、少一步操作,但它产出的地址有相当比例是不可配送的。而配送失败的代价(重新联系、二次配送、用户投诉)远大于让用户多打几个字。缓解办法是把光标自动停在需要补充的位置并给出示例(「如 3 号楼 502 室」),让这一步尽量轻。
地址存结构化字段而不是单一字符串,代价是表结构和表单都更复杂。单字符串最简单,但运费、限售、仓储分配都需要行政区划。从字符串解析行政区划是不可靠的——用户可能自己改过、可能少写了市、可能用了简称。判据是「下游有没有系统需要按结构化字段做计算」,有就必须结构化,这个成本躲不过去。
踩过的坑一:进页面就请求权限,被拒绝率高得离谱。而且更糟的是我当时以为「拒绝了下次再问」,实际上系统不会再弹——这个用户永久失去了定位能力,除非他自己去系统设置里改。这件事让我理解了权限请求是「一次性机会」,所以时机和上下文比什么都重要:要在用户明确想要这个能力的那一刻请求。
踩过的坑二:权限被拒绝之后页面卡在加载态。我只写了成功路径,拒绝时那个 Promise 既不 resolve 也没被 catch,页面就一直转圈。用户以为是网络问题,反复刷新。教训是:任何依赖系统能力的调用,都要显式处理「用户拒绝」这个分支,而它不是异常,是正常的业务分支。我后来把权限做成了一个显式的状态机(未请求 / 已授权 / 已拒绝 / 不可用),四种状态各有 UI,就不会漏了。
踩过的坑三:把逆地理的地址当完整地址存,配送失败。客服反馈「配送员找不到收件人」,查出来是一批地址都只到楼栏级别。这个坑的教训是要理解每个数据源能提供什么精度——坐标的精度是米级,能定位到楼,但「几号房」这个信息在物理世界里就不在坐标系统里,指望它给出来是搞错了数据的能力边界。
踩过的坑四:删除默认地址后没有指定新默认,下单页没地址可选。用户明明有三个地址,下单页显示「请添加收货地址」。这类问题的通用模式是「删除一个有特殊角色的记录时,那个角色要有人接手」——默认地址、主账号、主图都是同一类问题。我后来遇到类似场景都会先问一句「删了之后这个角色归谁」。
没做的部分:没做地址智能解析(用户粘贴一整段文字,自动拆出省市区和手机号)。这个功能体验很好而且技术上可行,但解析的准确率不可能到 100%,而解析错了用户不一定会检查——错误的地址比让他自己填更糟。如果要做,我的方案是解析后必须让用户逐字段确认,而不是直接填入并保存。
权限同意率:埋点统计「请求权限的次数」与「同意的次数」,对比时机后置前后。要说清分母是什么——后置之后请求次数本身大幅下降(只有点按钮的人才触发),所以同意率上升是自然的,不能只报这个比例。更有意义的指标是「最终成功保存地址的用户比例」,它衡量的是整个流程的通过率,不受权限请求次数变化的影响。这一点必须主动说明,否则用同意率来证明改进是有误导性的。
权限四种状态的覆盖用手工验证清单:未请求、已授权、已拒绝、系统定位关闭,四种状态各走一遍,断言每种都能通过手动路径完成保存。这是断言型的验证,报「四种状态全部有可用路径」而不是百分比。「已拒绝」这个状态要在真机上测——模拟器的权限行为和真机不一致。
地址完整性:报「详细地址中包含楼栋或门牌信息的比例」,用规则粗略判定(是否含数字加单位)。要承认这个判定是粗略的。改造前后对比这个比例,它是「门牌号强制补全」这个改动最直接的证据。
配送异常反馈:报客服工单里「地址不详无法配送」这一类的绝对数量变化。用绝对数不用比率,因为订单量本身在变。这个数字是业务侧的,要说清是从客服系统的工单分类里取的,不是自己估的。
定位超时的阈值怎么定:统计真实环境下定位耗时的分布(室内、室外分别测),取一个覆盖大部分成功情况的值作为超时。要说清这个值是测出来的而不是拍的,以及室内定位明显更慢这个事实。
不要报什么:不要报「定位准确率」——那是系统和地图服务的能力,不是你的代码决定的。不要报「地址填写成功率 99%」——如果分母是「进入地址页的人」,这个数字包含了大量中途放弃的正常行为。该报的是「四种权限状态都有可用路径」和「地址完整性比例的变化」。
评价发布看起来就是一个表单加几张图,第一版两天做完。但它踩的坑几乎都在「用户不是一次性顺利填完」这件事上。
一是写了半天丢了。用户写了两百字的评价、传了五张图,中途被电话打断切到后台,回来页面已经被系统回收重建,内容全空。他的反应是「这什么破 App」,然后不写了。而评价对电商是很重要的内容资产,流失一条就是流失一条。
二是重复发布。提交时网络慢,用户以为没点上,又点了一次。结果发出两条一样的评价。我加了按钮禁用,但用户切后台再回来,页面状态重建,按钮又可点了——禁用只是个视觉状态,它挡不住真实的重复请求。
三是草稿没清导致的怪问题。加了草稿功能之后出现新问题:用户成功发布了评价,下次进这个页面又看到了上次的草稿内容,他以为没发成功,又发了一次。草稿功能反而制造了重复发布。
四是敏感词拦截的位置错了。我只在客户端做了敏感词过滤,觉得够了。后来发现有人直接调接口发布违规内容——客户端校验对绕过前端的人完全无效。
所以四个改动:草稿自动保存并在成功后立即清理、防重复用「提交中状态 + 幂等键」而不是按钮禁用、客户端校验只做前置提示、服务端校验才是防护、提交失败时草稿必须保住。
这个模块最想说的一句话是:表单类功能的第一原则是「用户输入过的内容一个字都不能丢」。其他都可以商量,这一条不行——因为输入是用户付出的成本,丢了就是白费他的时间,而这件事对体验的伤害远大于任何加载慢。把这条当成硬约束之后,草稿、失败恢复、切后台保护这些需求自然就出来了。
草稿存本地而不是存服务端,是按「这个场景需不需要跨设备」判断的。存服务端的好处是换手机也能恢复,但评价这个场景用户几乎不会换设备继续写,而存服务端要多一套接口、要处理草稿的并发与清理、还涉及未发布内容的合规存储问题。本地存储的代价是清缓存或换设备会丢,但这个场景下概率很低。如果是长表单(比如商家入驻资料,用户可能填几天),那就该存服务端。
恢复草稿选择「询问」而不是「静默填入」,接受多一次交互。静默填入更顺滑,但用户会困惑内容是哪来的,而且他可能本来想重新写。询问让状态始终对用户透明。这里有个细节:询问的文案要说清是什么时候的草稿(「你有一份 2 小时前未完成的评价」),时间信息能帮他判断要不要恢复。
幂等键由客户端生成而不是先向服务端申请。先申请更「标准」(服务端可以做更多控制),但它多一次网络往返,而且在弱网下这次往返本身就可能失败,反而增加了失败面。客户端生成的代价是键的唯一性依赖客户端逻辑正确——所以键的构成要包含足够的业务维度(订单号 + 商品 ID + 编辑会话),而不是用一个纯随机数(纯随机的话每次编辑都是新键,起不到防重作用)。
踩过的坑一:清草稿的代码放在了 finally 里。正常路径下完全正常,但提交失败时草稿也被清了——用户写了两百字,网络失败,内容全没。他重写一遍,又失败,又没了。这个 bug 是「异常路径上的正确性」问题,功能测试完全测不出来,因为测试时网络都是好的。我后来的习惯是:凡是涉及清理用户数据的代码,一定要明确它在哪个分支,绝不放在 finally 或者公共出口里。
踩过的坑二:加了草稿之后反而出现重复发布。成功提交没清草稿,用户下次进来看到旧内容以为没发成,又发一次。这个因果很反直觉——一个用来提升体验的功能,因为缺了一步清理,制造了新的业务问题。教训是新增任何有状态的功能,都要把它的完整生命周期走一遍:创建、更新、成功后、失败后、过期后,每个环节都要有明确行为。
踩过的坑三:只在客户端做敏感词过滤。有人直接调接口发布违规内容。我当时的想法是「用户又不会自己调接口」,但这个假设对恶意用户不成立。修法之外,更重要的是理解客户端校验和服务端校验的职责根本不同:前者为体验(尽早告知),后者为防护(唯一不可绕过)。
踩过的坑四:按钮禁用挡不住重复提交。用户点提交、网络慢、切到微信回消息、回来页面被重建、按钮恢复可点、他又点了一次。禁用是视觉层的,页面重建之后就没了。这件事让我理解了「防重复必须在服务端有最终判据」——客户端的所有措施都只是减少概率。
没做的部分:没做草稿的多设备同步和「继续在电脑上写」。评价场景用不上。也没做富文本(评价只支持纯文字加图片),因为评价内容会展示给所有买家,富文本会带来和后台商品详情一样的清洗和 XSS 问题,而评价压根不需要格式——这是主动选择不引入复杂度,不是没能力做。
「写了半天丢失」这类问题的验证用场景清单,逐个手工走:切后台再回来、切后台被系统回收后回来(可以用开发者选项限制后台进程来模拟)、提交失败后、页面被返回手势关闭后。四个场景各断言内容能恢复。报法是「四个中断场景全部可恢复」而不是百分比。其中「被系统回收」这个场景必须真机模拟,模拟器不会真的回收页面。
重复提交的验证要构造慢请求:把提交接口延迟数秒,在等待期间反复点提交、以及切后台再回来点提交,断言服务端只产生一条评价。不构造延迟测不出来,因为本地网络太快,第二次点击时第一次已经返回了。这一点必须说明。
草稿清理的验证要正反两面:提交成功后断言草稿已清(再进页面不提示恢复);提交失败后断言草稿还在。后一条是我踩过的坑,所以必须有用例守着。
「重复评价反馈不再出现」:报客服工单里这一类的绝对数量变化,说明是从工单分类取的。用绝对数不用比率。
字数统计的验证:造包含首尾空白、连续空行、表情符号、多语言字符的输入,断言计数符合预期。表情和多语言字符要特别测——它们在不同平台的字符长度计算可能不一致(代理对问题),而跨境业务里用户会用各种语言写评价。
不要报什么:不要报「草稿保存成功率 100%」——存储写入可能失败(容量满),而且这个数字没有测量意义。该报的是「四个中断场景可恢复」和「构造慢请求下只产生一条评价」这两个可验证的行为。
没有匹配的内容,换个关键词试试。
项目拆解 · 商城客户端基础能力(uni-app · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据