[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f3ttfgnrthls5o":3},{"_id":4,"slug":5,"title":6,"subtitle":7,"kind":8,"cards":9,"tags":38,"categories":39,"source":40,"lang":43,"author":44,"audioState":47,"stats":48,"publishedAt":51,"renderer":52},"6abb4608ca21c797c7e9c0da","post-dbda0847","小程序点餐页吸顶滚动之分类按需加载，上划切换","点餐页常见的做法是先请求分类，再把所有分类的商品一起取回来，右侧排成一条长列表。这样做左右联动很直观：滚到哪一段，就选中左边哪一类。但商超分类多、商品数量差异又大时，用户可能只看了前两类，后面十几类的商品却已经全部请求并渲染了。 这次想试的是另一种节奏：分类目录先展示，右侧一次只看一个分类；进入某一类时才加载它的商品。少的分类只有一两件，多的分类可以有十几件。滑到当前分类底部，会看到“上拉继续浏览…","news",[10,13,18,23,28,33],{"headline":6,"body":11,"imageUrl":12,"sourceImageUrl":12},"点餐页常见的做法是先请求分类，再把所有分类的商品一起取回来，右侧排成一条长列表。这样做左右联动很直观：滚到哪一段，就选中左边哪一类。但商超分类多、商品数量差异又大时，用户可能只看了前两类，后面十几类的商品却已经全部请求并渲染了。 这次想试的是另一种节奏：分类目录先展示，右侧一次只看一个分类；进入某一类时才加载它的商品。少的分类只有一两件，多的分类可以有十几件。滑到当前分类底部，会看到“上拉继续浏览下一类”；再次上拉，或者直接点击提示，才进入下一类。 实现时，页面保留一个主要的纵向滚动容器，左侧分类栏吸顶并可独立滚动。右侧只渲染当前分类，重点是把商品请求放到真正进入分类的时刻。","https:\u002F\u002Fp3-xtjj-sign.byteimg.com\u002Ftos-cn-i-73owjymdk6\u002Fed515ad80dcc42e19924274c61ee800e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Sh5ZCN6Lev5Lq6:q75.awebp?rk3s=f64ab15b&x-expires=1791170369&x-signature=8wdd01gvvcSw7zOjG5lKQeJ8XIw%3D",{"headline":14,"body":15,"imageUrl":16,"images":17},"页面一开始就知道分类的顺序、名称和下一类是谁，但不因此生成所有商品。Demo 用本地 categories 模拟分类目录，用…","页面一开始就知道分类的顺序、名称和下一类是谁，但不因此生成所有商品。Demo 用本地 categories 模拟分类目录，用 fetchCategoryDishes(category) 模拟按分类 ID 请求商品。makeDishes 在这次模拟请求执行时才生成该分类的商品，进入页面只加载第一类。 这里的商品数量只是 mock 配置，用来展示两件、九件、一件、十二件以及空分类等不同高度。真实接口接入时，应让分类接口返回目录，再按当前分类 ID 请求商品；不要把 Demo 的 count 当作实际商品列表。 点击左侧分类、点击底部提示、上拉进入下一类，最终都走分类切换逻辑。它先更新左侧高亮和右侧分类信息，再把外层滚动位置移回菜单顶部，最后加载目标分类。已访问过的分类直接从 productCache 恢复，不再发同一请求。 因此这不是“把下一类追加到当前列表底下”的无限列表。切换后右侧只有目标分类，向前查看可以点左侧分类，已请求的数据仍在本页缓存中。Demo 顶部的“请求次数 \u002F 缓存类数”就是为了直观看出这点。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-dbda0847\u002F1.webp",{"local":16},{"headline":19,"body":20,"imageUrl":21,"images":22},"模拟请求也有两个容易忽略的保护：pendingRequests 按分类 ID 复用尚未结束的 Promise，避免快速重复操作发出同一请求；请求回来后只有目标分…","模拟请求也有两个容易忽略的保护：pendingRequests 按分类 ID 复用尚未结束的 Promise，避免快速重复操作发出同一请求；请求回来后只有目标分类仍是当前分类，才把结果显示到右侧。快速从 A 切到 B 时，A 的迟到响应可以入缓存，但不能覆盖 B 的页面。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-dbda0847\u002F2.webp",{"local":21},{"headline":24,"body":25,"imageUrl":26,"images":27},"如果一滚到底就立刻请求下一类，短分类会尤其容易被跳过。这个 Demo 把“到达底部”和“确认继续”拆成两次动作：滚动事件和 scrolltolower…","如果一滚到底就立刻请求下一类，短分类会尤其容易被跳过。这个 Demo 把“到达底部”和“确认继续”拆成两次动作：滚动事件和 scrolltolower 更新底部状态；手指按下时记录本次手势开始前是否已经到底；抬起时还在底部、且向上滑过一定距离，才切换下一类。 触摸事件只绑在右侧商品区域，滚动左侧分类栏不会误触发“下一类”。不想再滑一次时，next-cue 本身也可以点击。最后一个分类只显示“已浏览全部分类”，不会继续请求不存在的分类。 一两件商品的分类还有一个布局问题：右侧内容太短，左侧 position: sticky 所在的父容器如果也只有一小段高度，左栏还没来得及贴住固定头部，整个菜单就滚走了。固定的 demo-hero 随后会盖住分类。 所以需要量出 demo-hero 的实际高度，再分别计算滚动内容的顶部留白和左栏的吸顶位置。菜单区域至少占满一屏，让短商品列表也有足够的吸顶行程。 页面只有一个主要的纵向 scroll-view，左侧分类栏才有自己的内部滚动。切换分类后，scrollToMenu 用菜单相对于滚动内容的位置减去固定头部高度，让右侧从菜单顶部开始展示，不把分类标题藏到 demo-hero 后面。 按需加载不能只处理“有商品”的成功状态。Demo 分别展示加载中、零商品和加载失败；顶部“模拟失败”会让下一次未缓存分类的模拟请求失败，右侧可点击“重新加载”。缓存中即使是空数组，也代表该分类已经成功请求过，不应反复请求。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-dbda0847\u002F3.webp",{"local":26},{"headline":29,"body":30,"imageUrl":31,"images":32},"目前的 fetchCategoryDishes 只是延时 mock，没有接真实接口，也没有做单分类分页。如果真实分类内部还有多页商品，应该先把该分类的页加载到结…","目前的 fetchCategoryDishes 只是延时 mock，没有接真实接口，也没有做单分类分页。如果真实分类内部还有多页商品，应该先把该分类的页加载到结束，再允许底部提示进入下一分类；否则“上拉下一类”会抢走“加载当前类下一页”的手势。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-dbda0847\u002F4.webp",{"local":31},{"headline":34,"body":35,"imageUrl":36,"images":37},"代码在 menu-scroll-on-demand.vue，微信小程序页面路由为 \u002Fpages-demo\u002Fmenu-scroll-on-demand。依次看两件…","代码在 menu-scroll-on-demand.vue，微信小程序页面路由为 \u002Fpages-demo\u002Fmenu-scroll-on-demand。依次看两件商品的短分类、九件商品的长分类、零商品分类，再返回已经访问过的分类，可以观察滚动边界、请求计数与缓存行为。 这套方式适合“用户一次专注一类，按需进入下一类”的商超菜单。分类目录保持完整，商品请求则跟随用户的选择推进，页面不必为尚未浏览的分类提前承担加载和渲染成本。","\u002Fapi\u002Fmedia\u002Fposts\u002Fpost-dbda0847\u002F5.webp",{"local":36},[],[],{"name":41,"url":42},"Juejin","https:\u002F\u002Fjuejin.cn\u002Fpost\u002F7690205250242052130","zh",{"handle":45,"displayName":46},"spots","Spots",null,{"views":49,"likes":50,"saves":50,"shares":50,"completions":50,"opens":50,"skips":50,"depthSum":50},3,0,"2026-09-29T05:00:56.972Z","local"]