怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际虚拟商品商城,uni-app 一套代码发小程序和 App。核心链路是商品浏览 → 加购 → 下单 → 支付 → 订单查看。多语言多币种,用户网络条件差异大,支付渠道按地区不同。
为什么选这三个模块 电商前端的三个真正难点:列表与详情的性能(数据量大、图片多、要秒开)、下单支付的状态正确性(这是最容易出资损和投诉的地方,也是面试官最爱追问的)、跨端与包体积的工程约束(小程序的硬限制会逼出很多真实决策)。

模块一:商品列表与详情性能

  1. 商品列表与详情性能(长列表回收 + 图片治理 + 数据预取与缓存)★★★
    简历这样写 商品列表与详情页性能优化(uni-app + 长列表回收 + 图片按需尺寸 + 接口拆分与预取):列表用可视区回收渲染替代全量渲染,图片按设备宽度请求裁剪尺寸并懒加载;详情页接口按首屏与非首屏拆分,首屏字段先返回先渲染,同时在列表页预取详情缓存并做骨架屏。滑动 300 条商品后页面内存由持续增长转为稳定在 300MB 以内,详情页首屏可见时间由约 1.3s 降至 480ms 上下(预取命中时更快)。
    展开完整拆解
    为什么要这么设计

    列表页最初是「无限下拉,数据一直往数组里 push,全部渲染」。前一两百条没问题,滑到三四百条之后明显卡顿,小程序端还出现过白屏。三个原因叠加:

    一是节点数无限增长。每个商品卡片十几个节点,300 条就是几千个,加上图片的原生组件层,渲染层压力很大。二是 setData 的数据量过大——小程序每次追加数据都要把整个列表数组跨线程传给渲染层,数组越大传输越慢,这是小程序特有的性能陷阱。三是图片没有治理,服务端返回的是原图 URL,一张商品图两三百 KB,一屏六张就是一两兆。

    详情页的问题是接口太重:一个接口返回商品基本信息、多规格、详情图文、评价、推荐、库存、活动,服务端要聚合七八个数据源,P95 到了一秒多,前端只能一直转圈。而用户第一眼只需要看到主图、标题、价格

    所以三个方向:列表做回收渲染并优化 setData图片按需尺寸详情接口按首屏拆分并预取

    整体链路
    列表页 │ ├─ 分页拉数据(游标分页,不用 offset) │ ├─ 长列表回收渲染 │ └─ 只渲染可视区 + 上下缓冲 │ 滑出的项替换为等高占位(保持滚动条不跳) │ ├─ setData 优化 │ └─ 追加数据时只传增量,不重传整个数组 │ 小程序:用路径更新 this.setData({'list[20]': item}) │ 或按页分段渲染多个子列表组件,各自独立更新 │ ├─ 图片 │ ├─ URL 带尺寸参数,按设备宽度 x DPR 请求裁剪图 │ ├─ 现代格式(WebP,按端能力判断) │ ├─ 懒加载 + 占位(固定宽高比,避免抖动) │ └─ 列表项复用时先清空旧图,避免闪现上一个商品的图 │ └─ 预取:卡片进入视口 → 空闲时预取详情首屏数据存本地缓存 详情页 │ ├─ 优先读预取缓存 ──▶ 立即渲染首屏(主图/标题/价格/规格) │ └─ 缓存未命中 → 请求「首屏接口」(轻,只含必要字段) │ ├─ 并行请求非首屏部分(详情图文/评价/推荐)→ 到了再渲染 │ └─ 库存与活动价单独实时请求(不能用缓存,直接影响下单)
    分步拆解
    1. 长列表回收:滑出可视区的项替换为等高占位。不是直接删除(那样滚动条会跳),而是渲染成一个同样高度的空盒子。这样节点数和内存都被控制住,滚动位置也稳定。前提是卡片高度可预知,所以商品卡片要设计成固定高度或者固定宽高比。
    2. setData 的增量更新是小程序端的关键。直接 list = [...list, ...newItems] 会导致整个数组跨线程传输。正确做法是用路径更新只传新增部分,或者按页拆成多个子组件(每页一个组件,新增数据只更新新组件,老组件完全不动)。后者在 uni-app 里更好实现,也更彻底。
    3. 图片 URL 带尺寸参数。对象存储或图片服务通常支持 URL 参数裁剪,前端按 屏幕宽度 / 每行个数 × DPR 算出实际需要的像素宽度请求。不要请求原图——一张 1200px 的图显示在 180px 的卡片上,浪费的是用户流量和解码时间。
    4. 列表项复用时必须清空旧图。回收复用的组件如果直接换 URL,新图加载完成前会显示上一个商品的图,用户快速滑动时看到的是一堆错位的图。做法是先把图片源置空再设置新值,或者用占位图过渡。
    5. 详情接口按首屏拆分。「首屏接口」只返回主图、标题、价格、规格这些必须先看到的字段,服务端不需要聚合那么多数据源,能做到很快。详情图文、评价、推荐这些走第二批请求,异步补上。这个拆分要和后端一起定,是个跨端协作的决策而不是纯前端优化。
    6. 列表页预取详情。卡片进入视口后,在空闲时机预取该商品的详情首屏数据存本地。用户点进去时直接读缓存,几乎瞬间出内容。要限制预取的并发和总量——不能进一个视口就预取,那会打出大量请求;做法是只预取当前视口内的、且用户停留超过短暂时间的。
    7. 库存和活动价绝不能用缓存。这两个字段直接影响下单,缓存导致的后果是「用户看到有货点进去下单失败」或者「看到的价格和实付不一致」。所以详情页进入时必须实时请求一次库存和当前价,即使其他内容来自缓存。这个区分是电商前端的基本纪律。
    8. 骨架屏而不是转圈。骨架屏让用户感知到「内容马上就来」而且布局稳定,加载完成后不会跳。转圈会让人感觉不知道要等多久。骨架的形状要和真实内容接近,否则加载完成的瞬间会有明显的跳变。
    关键决策与取舍

    为什么不用现成的长列表组件。试过社区的方案,问题是商品卡片的高度不完全一致(标题一行和两行、有无促销标签),不定高的虚拟列表复杂度高且容易出滚动跳动。我们的选择是从设计上消除不定高——标题固定两行(不足补空、超出省略),标签区固定高度。这样就能用最简单的等高回收方案。用设计约束换实现简单,是前端很常见也很值得讲的取舍。

    预取的成本与收益。预取会产生额外请求和流量,如果用户压根不点进去就是浪费。所以做了限制:只预取视口内且停留过的只在网络状况良好时(弱网下预取会挤占正常请求)、只预取首屏字段(数据量小)。这样浪费的流量很有限,而命中时的体验提升很明显。要能说出「怎么控制副作用」,只说「我做了预取」是不够的。

    踩过的坑:小程序端图片过多导致渲染层崩溃。列表滑很久之后小程序整个白屏,日志里没有 JS 报错。定位过程:先怀疑内存,看到内存持续增长;再发现是图片组件的原生层实例数超限——即使 JS 层的节点被回收了,如果图片组件没有真正从树上移除,原生层的实例还在。修法是回收时确保组件真正卸载(不是 v-show 隐藏),并且限制同时存在的图片数量这个坑体现了小程序双线程架构的排查难点:JS 层看不到问题,要看原生层。

    没做的部分:没做列表数据的本地持久化。用户从详情页返回列表时,如果页面被回收了要重新拉数据、重新滑到原位置。理想做法是缓存列表数据和滚动位置,但涉及数据新鲜度的判断(缓存的价格可能过期),当时没有做完整方案。

    数字是怎么测的

    内存:Android 上 adb shell dumpsys meminfo 取 PSS,固定操作路径(匀速滑动 300 条商品,每 50 条记一次)。改造前持续增长直到被系统回收,改造后稳定在 300MB 以内。「稳定不增长」这个性质比绝对值更重要,因为绝对值和机型、系统、其他应用都有关。要报出测试机型

    详情页首屏可见时间:在页面 onLoad 和首屏内容渲染完成处打时间戳。改造前约 1.3s(等重接口),改造后 480ms 上下(首屏轻接口),预取命中时更快(几乎无网络等待)。要分别报「预取命中」和「未命中」两种情况——只报命中时的数字是选择性呈现,会被问穿。

    图片流量:可以补充这个指标。用抓包工具统计滑动固定条数商品的总下载量,改造前后对比。这个数字对国际业务尤其有说服力,因为很多市场用户对流量敏感。

    面试追问
    Q:小程序的 setData 为什么慢?你说的「只传增量」具体怎么做? A:小程序是双线程架构,逻辑层和渲染层分离,setData 的数据要经过序列化、跨线程传输、反序列化三步。传输的是你传给它的整个字段,所以 setData({list: 整个数组}) 会把几百条数据全部序列化一遍,数据量大时耗时明显。增量的做法是用数据路径setData({'list[300]': newItem}),只传这一项。追加多条时可以循环构造多个路径的对象一次传。在 uni-app 里更实用的方案是按页拆子组件——每 20 条一个子组件,新增数据只是新增一个子组件实例,已有组件的数据完全不动,框架不会重传它们。
    Q:预取会不会导致服务端压力变大? A:会,这是必须和后端确认的。控制手段几个:只预取首屏轻接口(服务端成本低,而且这个接口本身应该是可缓存的);限制预取并发(比如同时最多 2 个);加防抖(卡片进入视口后停留一段时间才预取,快速滑过的不预取);弱网或省流量模式下关闭预取。另外首屏接口在服务端应该做缓存,预取命中缓存的话服务端成本极低。如果后端明确说撑不住,那就把预取范围缩小到「用户点击的瞬间」——虽然收益小很多,但至少不增加无效请求。
    Q:详情页拆成两个接口,会不会导致两次请求反而更慢? A:首屏可见时间一定更快,因为首屏接口更轻。但「全部内容加载完」的总时间可能略慢(多了一次请求的开销)。这个取舍是明确的:用户对首屏可见极度敏感,对「下面的评价什么时候加载完」几乎无感,因为他还在看主图和价格。所以优化首屏是对的。要注意的是两个请求要并行发起而不是串行——首屏接口和非首屏接口同时发,不要等首屏回来再发第二个。
    Q:多规格商品(比如不同面额、不同区服)的选择怎么处理价格和库存? A:规格组合的价格库存不能前端算。做法是服务端返回所有可用组合的价格与库存映射(组合数不多时全量返回,多的时候按选择逐步请求)。前端根据用户已选的规格置灰不可用的选项(这一步很关键,否则用户选完才发现无货,体验很差)。下单时必须以服务端返回的组合价为准,而且要带上规格组合 ID 而不是价格——绝对不能前端传价格给后端,那是最典型的价格篡改漏洞。这个安全点必须主动说出来。

模块二:下单与支付

  1. 下单与支付(防重复提交 + 状态机 + 弱网结果确认)★★★
    简历这样写 下单与支付前端链路(uni-app + 多渠道支付 + 幂等令牌 + 状态机 + 退避轮询):下单用服务端下发的幂等令牌加按钮锁定防重复提交;支付结果不信客户端回调,统一以服务端订单状态为准并做退避轮询与超时兜底;针对弱网下「请求久未返回但实际已成功」的场景,设计先查状态再决定是否可重试的交互。压测下重复下单由每轮可复现数笔降至未再复现,支付结果的最终确认覆盖到断网、切后台、中途退出三类中断场景。
    展开完整拆解
    为什么要这么设计

    这个模块是从真实事故里长出来的。最初的实现很朴素:点下单按钮调接口,成功了跳支付,支付 SDK 回调成功就跳成功页。三个问题在国际业务的网络环境下集中爆发。

    一是重复下单。用户在网络慢的时候点了按钮没反应,又点一次、再点一次,产生了三笔订单。或者请求超时后前端自动重试,服务端实际上都处理了。

    二是最难缠的一个:请求几十秒才返回,前端已经判定失败。用户在弱网下提交订单,前端设了 10 秒超时,超时后显示「下单失败,请重试」并把按钮恢复。但那个请求实际上在第 30 秒成功了。用户看到失败又点一次,于是两笔订单。这个问题的本质是:客户端的超时不等于服务端的失败,超时只意味着「我不知道结果」。

    三是支付结果不可信。支付 SDK 的回调可能丢(用户在支付页面切后台、断网、手动返回),也可能回调说成功但服务端还没收到渠道通知。以客户端回调为准会出现「客户端显示成功但订单还是待支付」,或者反过来。

    所以整个设计围绕一个原则:客户端永远不是状态的裁定者,服务端订单状态是唯一真相

    整体链路
    进入结算页 │ ├─ 请求「预下单」→ 服务端返回:可下单校验结果 + 幂等令牌 token │ token 与本次结算上下文绑定,有效期短 │ 点击下单 │ ├─ 按钮立即锁定(禁用 + loading),全局标记「有进行中的下单」 │ ├─ 提交下单(带 token;同一 token 服务端只创建一笔订单) │ ├─ 结果分三种(关键:不是两种) │ ├─ 明确成功 → 拿到 orderId,进入支付 │ ├─ 明确失败(库存不足/校验不通过)→ 提示,允许重试(换新 token) │ └─ 未知(超时/网络错误)→ 不允许直接重试! │ └─ 转入「查询兜底」:按 token 查订单是否已创建 │ ├─ 已创建 → 当作成功,继续支付 │ └─ 确实没有 → 才允许用同一 token 重试 │ 支付 │ ├─ 请求支付参数(按地区选渠道)→ 调起渠道 SDK │ ├─ 本地记录「有一笔待确认支付」(持久化,防应用被杀) │ ├─ SDK 回调(成功/失败/取消)→ 仅作为「该去查询了」的信号 │ └─ 统一走「确认支付结果」 ├─ 退避轮询订单状态(1s,2s,3s,5s... 上限约 30s) ├─ 轮询期间页面不可返回(防用户以为失败重复支付) ├─ 超时未确认 → 不判失败,跳「处理中」页 + 引导去订单列表查看 └─ 应用重启时检测本地待确认记录 → 自动补一次查询 状态机(前端展示用,与服务端状态一一对应) 待支付 → 支付处理中 → 已支付 → 已发货 → 已完成 ↘ 已取消 ↘ 支付失败(可重新支付)
    分步拆解
    1. 幂等令牌由服务端下发。进入结算页时请求一次拿到 token,下单时带上。服务端保证同一个 token 只创建一笔订单——重复提交时返回第一次创建的那笔,而不是报错。令牌不能由客户端生成(客户端生成的话,客户端重装或者清缓存后可能重复,而且服务端无法预先校验)。
    2. 按钮锁定要在发请求之前。点击的第一件事是禁用按钮,不是发请求。而且要有全局的进行中标记——因为用户可能返回上一页再进来点,只锁单个按钮的状态不够。
    3. 结果必须分三态,这是整个设计的核心。成功、失败、未知。绝大多数 bug 来自把「未知」当成「失败」。超时、网络错误、请求被中断,这些情况下服务端可能已经成功了,客户端只是没收到响应。未知状态下绝不能提示「失败请重试」,必须先查询。
    4. 查询兜底用同一个 token。「按 token 查是否已创建订单」这个接口是解决未知态的关键。查到了就当成功继续走,确实没有才允许重试。这个接口要和后端一起设计,是前后端协作的产物。
    5. 支付前把「待确认」持久化到本地。写入 storage:订单号、发起时间、支付渠道。这样即使应用被系统杀掉、用户强制退出,下次启动时还能发现「有一笔支付没确认」并主动去查。只放在内存里的状态在移动端是不可靠的。
    6. SDK 回调只当信号,不当结论。不管回调说成功还是失败,都走同一个「查询服务端订单状态」的流程。回调成功只是让我们更早开始查询。特别注意:回调说「取消」也不能直接判失败——有些渠道的取消回调和支付成功是竞态的,用户可能已经付了但客户端收到了取消。
    7. 轮询要退避且有上限。间隔从 1 秒逐步拉长(1、2、3、5、8……),总时长上限约 30 秒。不能固定 1 秒轮询到底,那在支付渠道回调慢的时候会打出几十个请求。
    8. 轮询超时不判失败。跳到「处理中」页面,明确告诉用户「支付正在确认,请稍后在订单列表查看」,并且不给「重新支付」的入口(防重复付款)。这比显示「失败」好得多——显示失败会让用户去重新支付,那才是真的资损风险。
    9. 轮询期间禁止返回和重复支付。拦截物理返回键和手势返回,或者至少弹确认。这一步是防重复支付的最后一道防线。
    关键决策与取舍

    「未知态不允许直接重试」的体验代价。用户在弱网下下单超时,我们不让他直接重试而是先查询,查询本身在弱网下也可能慢,所以用户要多等几秒。但这个代价远小于产生重复订单的代价——重复订单要走退款、要客服介入、用户会不信任平台。这个权衡的方向必须明确:在支付链路上,宁可慢一点,不可错一次。

    为什么不用客户端本地的订单号做幂等。看起来客户端生成一个 UUID 也能做幂等。但两个问题:一是客户端生成的 ID 服务端无法预先校验(不知道这个 ID 是否对应一个合法的结算上下文),给了攻击面;二是结算上下文(商品、价格、优惠)需要在服务端锁定,token 正好可以承载这个绑定——服务端在下发 token 时就把这次结算的商品和价格记下来,下单时校验一致,防止客户端篡改。所以 token 不只是幂等键,还是结算上下文的句柄。

    踩过的坑:轮询期间用户切后台,回来后轮询已经停了。小程序和 App 在后台会挂起定时器,用户切回来时轮询已经中断,页面卡在「确认中」。修法是监听应用前后台切换,回到前台时重新触发一次查询,并且用「记录发起时间 + 回前台后算已过时间」的方式判断还该不该继续轮询。移动端的任何定时逻辑都要考虑前后台切换,这是和 Web 最大的差异之一。

    踩过的坑二:支付金额展示与实付不一致。结算页显示的价格是进入页面时算的,用户停留了十分钟,期间活动结束了价格变了,提交时服务端按新价算,用户实付更多。修法是结算上下文有明确的有效期,超时后强制刷新并提示用户价格已变化,需要用户重新确认。不能静默按新价扣款,那是投诉和监管风险。

    没做的部分:没做支付渠道的智能排序(按用户历史和地区推荐最可能成功的渠道)。这对转化率有影响,但需要数据支撑,当时是固定顺序展示。

    数字是怎么测的

    重复下单:用脚本或者抓包工具模拟弱网加重复点击(限速到很低,然后快速连点下单按钮 5 次),检查服务端产生了几笔订单。改造前每轮能复现数笔重复,改造后未再复现。这类可靠性问题用「可复现的测试」量化,比用线上比率更实在——线上重复下单的比率分母不好界定(多少次下单里有多少重复),而且量小的时候统计不显著。

    三类中断场景怎么验证:逐个手工演练。断网——调起支付后立刻开飞行模式,恢复后看是否正确确认;切后台——支付过程中切到桌面停留几分钟再回来;中途退出——支付页面直接杀掉应用,重启后看是否自动检测到待确认支付。这三个场景必须能说出具体的演练步骤和预期结果,说「我做了兜底」但描述不出验证方式,会被认为没真做。

    不要报「支付成功率提升 X%」。支付成功率主要由渠道、用户余额、风控决定,前端能影响的部分很小。把这个指标算成自己的成果一问就穿。可以报的是「支付结果确认的准确性」——改造前有多少笔订单出现过「客户端和服务端状态不一致」的客服工单,改造后这类工单消失。

    面试追问
    Q:你说的「请求超时但实际成功」的问题,为什么不能简单地把超时时间设长一点? A:设长了解决不了根本问题,只是把窗口挪后。无论超时设多长,总存在「服务端在超时之后才成功」的可能,因为网络延迟没有上限。而且超时设太长(比如 60 秒)会带来新问题——用户盯着转圈一分钟,体验极差,而且他很可能中途杀掉应用重进,那时候状态更混乱。正确的思路不是消除未知态,而是承认它存在并设计出处理它的流程:超时后进入「查询确认」而不是「判定失败」。这个认知是这个模块最核心的部分。
    Q:轮询 30 秒还没确认怎么办?用户会不会一直不知道结果? A:跳到「处理中」页面,明确告知「支付正在确认,结果会在订单列表更新」,并且不提供重新支付入口。同时做几件事:推送兜底——服务端确认支付后给客户端推一条消息;订单列表页每次进入时刷新状态应用启动时检查本地待确认记录并主动查一次。这样即使当次没确认,用户后续任何一次操作都能拿到真实状态。关键是绝不能显示「支付失败」——那会引导用户重新支付,可能造成重复付款,那是真正的资损。
    Q:用户点了支付,SDK 拉起了渠道页面,但用户直接返回了,这时候是什么状态? A:未知态,必须查询。用户返回可能是:压根没付(那订单还是待支付)、已经付了但没看到结果页(订单已支付)、渠道那边正在处理(处理中)。客户端无法区分,所以统一走查询。这里有个实现细节——SDK 的取消回调不能直接当失败处理,因为部分渠道在用户已完成支付但客户端返回时也会触发取消回调。我们的处理是:收到任何回调(包括取消)都触发一次查询,查询结果才是结论。
    Q:如果服务端也不确定(渠道还没回调),订单是什么状态?前端怎么展示? A:服务端应该有一个「支付处理中」的中间状态,而不是只有待支付和已支付。前端展示这个状态时要给出明确的文案(「支付确认中,通常几分钟内完成」)并且禁用重新支付按钮。同时服务端要有主动查询渠道的兜底任务——不能只等渠道回调,要定时反查渠道的订单状态,超过一定时间还没结果的走人工或者自动关单。前端能做的是不误导用户,真正的兜底在服务端。把这一点说出来,说明你理解前端在支付链路里的边界。

模块三:跨端差异与包体积治理

  1. 跨端差异与包体积治理(条件编译 + 分包 + 能力抽象层)★★★
    简历这样写 跨端工程治理(uni-app + 条件编译 + 分包与预下载 + 平台能力抽象层):把平台差异收口到统一的能力抽象层(支付、定位、分享、存储各一套接口,内部按端实现),业务代码不再散落条件编译;按业务域拆分包并配置预下载,静态资源迁出主包。小程序主包体积由约 2.3MB 降至 1.4MB 上下(低于平台限制且留有余量),新增业务模块不再占用主包配额。
    展开完整拆解
    为什么要这么设计

    跨端项目做到中期一定会遇到两个问题。

    一是条件编译泛滥。刚开始只有几处平台差异,写个条件编译很方便。半年后代码里有几百处条件编译,散落在各个业务文件里,同一个能力(比如支付)在五个地方各写了一遍平台分支。新增一个平台要改几百处,而且一定会漏。更糟的是没人敢删——不知道某个分支是为了解决哪个端的什么问题。

    二是主包体积撞上限。小程序对主包有明确的大小限制,撞上之后压根发不了版。这不是性能问题而是硬阻塞,而且往往是在要发版的时候才发现,非常被动。

    所以两个方向:把平台差异从业务代码里收口到抽象层把包体积做成可持续管理的(分包 + 门禁)而不是每次撞上限了才临时救火

    整体链路
    能力抽象层(业务代码只调这一层,不写条件编译) │ platform/ pay.js → pay(params) 内部:#ifdef MP-WEIXIN 微信支付 #ifdef APP-PLUS App 端渠道 SDK #ifdef H5 网页跳转支付 location.js → getLocation() / openMap() share.js → share(payload) 各端 API 差异极大 storage.js → get/set(统一序列化 + 容量兜底) media.js → chooseImage / compress / upload auth.js → login()(各端登录态获取方式不同) │ └─ 每个方法:统一的入参与出参约定 + 不支持该能力的端 → 返回明确的「不支持」而不是静默失败 分包结构 主包(必须精简) ├─ 首页 / 分类 / 购物车 / 我的(tabBar 页面必须在主包) ├─ 公共组件与核心工具 └─ 极少量必要的静态资源 分包 ├─ subpkg-order/ 下单、支付、订单列表、详情 ├─ subpkg-user/ 设置、地址、客服、帮助 ├─ subpkg-activity/ 活动页、专题页(变化快,独立发版影响小) └─ subpkg-static/ 图片、字体等静态资源 预下载 └─ 进入首页时预下载 subpkg-order(下单是主路径,提前拉好) 体积门禁 └─ CI 检查主包与各分包体积,超阈值构建失败
    分步拆解
    1. 先盘点所有条件编译,按能力归类。把散落的条件编译全部找出来,归类成「支付、定位、分享、存储、媒体、登录」这几个能力域。这一步会发现大量重复和矛盾——同一个平台判断在不同文件里写法不一致,甚至有互相矛盾的处理。
    2. 为每个能力定义统一接口。接口的入参出参要取各端的交集加必要的可选项,不能直接照搬某一个端的 API 形状(那样另一个端实现起来会很别扭)。比如分享,各端的参数差异极大,抽象层应该定义业务语义的参数(标题、描述、图片、跳转路径),由各端实现自己去映射。
    3. 不支持的能力要显式报「不支持」。比如某个端没有某个原生能力,抽象层要返回明确的不支持标识,让业务代码能做降级(隐藏入口或者提示)。最糟的做法是静默失败——用户点了没反应,也没有报错,排查极其困难。
    4. tabBar 页面必须在主包。这是平台限制,没法绕。所以主包精简的空间主要在「非 tabBar 页面」和「静态资源」。首页如果很重,要考虑把首页的次要模块做成异步组件。
    5. 分包按业务域拆,不按技术层拆。「下单相关的所有页面和组件」放一个分包,而不是「所有页面一个包、所有组件一个包」。因为分包的加载是按需的,同一个业务流程的东西放一起才能一次加载完,跨包引用会带来额外下载。
    6. 静态资源迁出主包。图片、字体这些能放 CDN 的全部放 CDN,必须内置的放独立分包。这通常是主包瘦身收益最大的一项——一个几百 KB 的背景图或者字体文件就能占掉主包很大比例。
    7. 配置分包预下载。用户在首页时预下载「下单」分包,因为那是主路径。这样点进商品下单时不用等分包下载。不要预下载所有分包,那等于没分包,还浪费流量。
    8. 加体积门禁。CI 里检查主包和各分包体积,超阈值失败。这是唯一能防止「几个月后又撞上限」的手段。阈值要留余量(比如设成限制的 80%),给紧急需求留空间。
    9. 公共代码的归属要小心。多个分包都用到的组件如果放主包,会占主包配额;如果各分包各放一份,会重复。判断依据是「用它的分包数量」和「组件本身的大小」——小而广泛使用的放主包,大而只有两个包用的可以重复。
    关键决策与取舍

    抽象层的成本。多了一层间接,调试时要多跳一层看实现;而且抽象接口一旦定了,改动会影响所有调用方。所以抽象层只对「多端有差异且被多处使用」的能力做——只在一个地方用一次的平台差异,直接写条件编译更清楚,硬抽象反而是过度设计。这个边界要说清楚,否则会显得为了架构而架构。

    抽象层不可能覆盖 100% 差异。有些差异是根本性的——比如某个端压根没有某个能力,或者交互流程完全不同(App 端可以在应用内完成支付,H5 端必须跳转到外部页面再跳回来)。这类差异不能藏在抽象层里假装一致,必须在业务层显式处理,甚至设计两套不同的交互流程。「一套代码多端运行」是理想,实际是「大部分代码复用,关键差异正面处理」。

    踩过的坑:分包后跨包引用导致体积没降。把页面移到分包了,但那些页面引用的组件还在主包,或者反过来主包的页面引用了分包的东西,构建工具会把被引用的部分打进主包。结果是「看起来分包了,主包却没小多少」。修法是用构建分析看主包的实际构成,逐个排查跨包引用,把只被分包使用的组件移进分包。这个坑很典型:分包不是简单移动文件,要理清依赖关系。

    踩过的坑二:条件编译在样式里也要处理。只关注了 JS 的条件编译,忽略了不同端的样式差异——安全区(刘海屏、底部手势条)、状态栏高度、滚动条行为在各端表现不同。做法是把这些差异做成 CSS 变量或者统一的布局组件,而不是在每个页面写条件编译的样式。

    没做的部分:没做按端裁剪构建产物。理论上编译到微信小程序时,App 端专用的代码应该被完全剔除。条件编译能做到一部分,但抽象层里的各端实现如果写在同一个文件里,可能会有残留。更彻底的做法是按端拆成不同文件(pay.mp.js / pay.app.js),由构建时选择。

    数字是怎么测的

    主包体积:用小程序开发者工具的「代码依赖分析」或者构建产物大小直接看。要说清是压缩后(上传时的实际体积)还是源码体积——小程序平台限制的是压缩后的体积,说源码体积没有意义。改造前约 2.3MB,改造后 1.4MB 上下。

    「低于平台限制且留有余量」比单纯的数字更重要。因为主包体积的意义是「能不能发版」,不是「快多少」。所以描述里应该体现相对平台限制的余量,这才是这个优化的真实价值。

    条件编译的数量:可以补充这个指标——改造前散落在业务代码里的条件编译有多少处,改造后收口到抽象层的有多少个文件、业务代码里还剩多少处。这是个能直接数出来的结构性指标,比「代码更好维护了」这种空话有说服力。

    不要报「启动速度提升 X%」。主包变小对启动确实有帮助,但启动耗时受平台、机型、网络影响极大,而且分包预下载又会带来新的开销。硬报一个启动提速的百分比很容易被追问到答不上来。

    面试追问
    Q:uni-app 一套代码多端,实际能复用多少?哪些一定要分开写? A:业务逻辑、状态管理、接口层、大部分组件能复用,这部分占大头。一定要分开写的有几类原生能力调用(支付、分享、定位、推送,各端 API 和授权流程都不同);登录态获取(小程序是静默的 code 换 session,App 端是账号密码或第三方登录,H5 是 cookie 或 token);路由与页面栈行为(各端的页面栈深度限制和返回行为不同);样式的安全区与状态栏性能敏感的列表(小程序的 setData 机制和 App 端的 WebView 渲染,最优策略不同)。我的经验是逻辑层复用度很高,越靠近系统和渲染的部分复用度越低,这个规律说出来比给一个百分比更有价值。
    Q:主包又要撞上限了,还能怎么优化? A:按收益排序:先看静态资源(图片、字体,转 CDN 或者移到分包,通常收益最大);再看非 tabBar 页面(还有没有页面能移到分包);再看依赖(有没有引了大库只用了一小部分,能不能按需引入或者手写替代);再看首页的异步化(首页的次要模块做成异步组件,用到才加载);最后是代码层面(删死代码、检查有没有重复打包)。如果这些都做完还是超,那说明业务复杂度已经超出小程序的承载能力,要考虑把部分功能改成 H5 内嵌或者引导到 App,这是产品层面的决策了。能说出「最后一步是产品决策而不是技术还有办法」,比硬说还能优化更诚实。
    Q:分包预下载会不会浪费用户流量? A:会,所以要选择性预下载。原则是只预下载「大概率会用到的主路径」——商城的下单流程用户很可能走到,值得预下载;设置页、帮助中心用户很少进,不值得。另外可以按网络类型控制,只在 WiFi 下预下载,蜂窝网络下不预下载(平台的预下载配置通常支持指定网络类型)。要衡量的是「预下载浪费的流量」和「用户等待分包下载的体验损失」,主路径上后者的代价更大,非主路径上前者的代价更大。
    Q:能力抽象层设计好了,但某个端的实现有 bug 或者行为不一致,怎么发现? A:这是抽象层的固有风险——它让业务代码看起来一致,但底层行为可能不一致,问题更难发现。几个手段:抽象层要有各端的验证清单,每个能力在每个端上手工验证一遍并记录(这是最实在的办法,没有捷径);接口的出参约定要严格,包括错误码的语义,各端实现必须映射到统一的错误码;加日志埋点,抽象层内部记录调用的端、参数、结果,线上出问题能定位到是哪个端的实现有问题。另外要接受「抽象层需要持续维护」——平台 API 会变、会有新的端,抽象层不是一次性工作。

没有匹配的内容,换个关键词试试。

项目拆解 · 跨境商城(uni-app)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据