news 2026/10/1 12:19:52

微信小程序开发实战:登录、请求封装与分页加载全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序开发实战:登录、请求封装与分页加载全攻略

复盘了一下苍穹外卖这个项目的整体进度,今天是Day6,目标是把C端小程序端跑通核心下单链路。前五天基本把管理端那套CRUD、菜品分类、口味管理、套餐管理都做完了,也从数据库设计一路写到了后台接口联调。今天开始切到微信小程序端,这部分的坑跟管理端完全不同,尤其涉及微信生态的登录态、请求封装、分页加载这些,每一个都是实战中绕不开的坎。

如果说前五天的重点是“管理后台能不能满足运营需求”,那Day6的核心就是“用户能不能在小程序里顺畅地把外卖点完”。这个顺畅,不是页面好看就行,而是首次登录要不要授权、列表数据能不能滚动加载、图片上传服务器之后回显是不是正常、token过期了会不会直接白屏。这些问题看着不大,但任何一个没处理好,用户都会在第一步流失掉。今天这篇就把小程序端从零到一搭起来的完整过程和踩坑点记录下来。

1. 项目进度梳理与Day6目标拆解

1.1 苍穹外卖项目整体到Day5都做了什么

先交代一下背景,方便后面对照着看。苍穹外卖这个项目是典型的前后端分离架构,管理端用的是SpringBoot + MyBatis Plus,数据库MySQL,管理端页面用Vue3 + Element Plus做了一套基础的增删改查页面。前五天实际上做的事情是:建表设计、菜品管理、分类管理、口味管理、套餐管理、以及一套基础的登录框架。换句话说,管理端的基本盘已经立住了,店铺能上架商品、能管理分类、能设置套餐,后台数据已经能支撑日常运营。

但是外卖系统只做管理后端,是完不成闭环的。用户端没有入口,用户就看不到菜品、下不了单,整个业务逻辑就断在半路。所以Day6开始,重心转向用户端微信小程序,这一步是把整个项目从“一个后台系统”变成“一个能跑的外卖业务”的关键节点。今天的任务说白了就是两件事:第一,把小程序的项目骨架搭起来;第二,把用户端浏览菜品的完整链路走通,包括登录、首页数据展示、分类联动、菜品列表的加载与分页。

1.2 今天具体要做哪些模块

第一梯队是基础设施,包含小程序的目录结构初始化、微信登录流程接入、全局请求封装。因为小程序端的所有页面和数据请求都要依赖这三个东西,它们是后续一切功能的地基。很多新手上来就写页面,写到一半发现每个页面都在重复写wx.request,而且登录状态没法统一管理,回头再补封装的时候会非常痛苦。

第二梯队是首页,主要包含顶部导航栏适配、搜索框、轮播图、分类区域、菜品列表。首页是用户进入小程序的第一个落脚点,体验好坏直接影响留存。这块做得糙一点,用户滑两下就走了,连注册登录的意愿都没有。

第三梯队是核心列表逻辑,即分类切换菜品、滚动分页加载。外卖列表的特点是数据量不会像电商那样动不动上百条,几十条菜品也要做分页,不然一次性加载几十张菜品图片,用户手机流量直接告急,等待时间也会非常影响体验。

第四梯队是在具体页面上会用到的辅助能力,比如本地上传图片、用户离开小程序的状态监听。这些今天先做基础实现,后续的订单、购物车模块会继续复用今天搭的这个底座。

2. 小程序项目初始化与目录结构设计

2.1 为什么小程序端要单独建一套项目,而不是复用管理端结构

很多从前后端分离过渡到小程序开发的同学,第一反应是“我已经有管理端的目录了,把页面搬过去改改就行”。这个思路放到小程序端是非常危险的。管理端的Vue项目跑在浏览器里,有完整的DOM、路由、状态管理库;小程序跑在微信的宿主环境里,没有DOM概念,页面渲染和逻辑层是分开的,路由也没有beforeEach那种全局守卫,很多浏览器端理所当然的东西在小程序里根本不存在。

说得再直白一点,Vue项目里的每一个组件、每一条路由、每一个状态管理的思路,在小程序里都可能要走一遍“翻译”。与其硬移植,不如直接按小程序的规范重新搭一套,把后端接口能力对接好,页面按小程序的组件模型来设计。Day6我把目录结构设计成下面这样:

sky-take-out-mini/ ├── assets/ │ └── images/ # 静态资源 ├── components/ │ ├── dish-card/ # 菜品卡片组件 │ └── search-bar/ # 搜索框组件 ├── request/ │ ├── http.js # 请求核心封装 │ └── api.js # 接口统一管理 ├── pages/ │ ├── index/ # 首页 │ ├── category/ # 分类页 │ ├── cart/ # 购物车 │ ├── order/ # 订单 │ └── profile/ # 个人中心 ├── utils/ │ ├── auth.js # 登录态管理 │ └── format.js # 格式化工具 ├── app.js ├── app.json └── app.wxss

这个结构看起来简单,但每层都是按职责分的。components只放可复用UI,pages放页面级逻辑,request单独抽一层,utils存纯函数。最核心的一点是:请求单独封装,页面永远不直接调用wx.request,后续加拦截器、加统一错误处理全都在这一个地方搞定。

2.2 app.json的全局配置细节

小程序能跑起来,全局配置文件是最不能被忽视的。app.json里page窗口表现、tabBar导航栏、网络超时时间这三项是要重点设置的。我Day6最开始踩了一个小坑,tabBar里配置的图标路径写的是相对路径,结果一直白屏,排查了十分钟才发现是路径前面少了个斜杠。这种低级错误在开发工具里不报错,但真机预览时会出现整个页面无法加载的问题,遇到这种诡异白屏的时候,优先检查app.json里的路径。

{ "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/order/order", "pages/profile/profile" ], "window": { "navigationBarBackgroundColor": "#ffc300", "navigationBarTextStyle": "black", "navigationBarTitleText": "苍穹外卖", "backgroundColor": "#f5f5f5" }, "tabBar": { "color": "#999999", "selectedColor": "#ffc300", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/category/category", "text": "分类" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/order/order", "text": "订单" }, { "pagePath": "pages/profile/profile", "text": "我的" } ] }, "networkTimeout": { "request": 10000, "uploadFile": 15000 }, "style": "v2" }

这里networkTimeout是真的会救命的。微信开发者工具里默认超时时间可能是60秒,但真机上如果接口10秒内没响应,用户早就退出去了。设置成10秒比较合理,既能给慢网络留余地,也不会无限等待。再配合后端接口的自身超时控制,用户体验才不会走到“卡死”的境地。

3. 微信登录与请求封装全流程

3.1 wx.login() 登录流程,不能只在页面里调一次就完了

苍穹外卖小程序端的登录设计遵循微信生态的标准流程。首次进入小程序,前端拿到wx.login()返回的code,把这个code通过后端接口发给自己的服务器;后端拿code去微信的接口换取openid和session_key,然后生成自己的登录态token返回给前端。前端把token存到storage里,后续所有请求带上这个token,后端通过token识别用户身份。

这个流程里有一个很隐蔽的坑:wx.login()拿到的code有效期只有5分钟,而且只能用一次。很多同学的写法是,每次打开小程序都无脑调wx.login()拿新code,然后发给后端换token。这在后端session机制下会导致之前的下发token全部失效,用户在操作过程中会被莫名其妙地顶下线。正确的做法是:首次登录或者token过期时才调wx.login(),平时直接用存量token发请求。

// utils/auth.js const TOKEN_KEY = 'sky_token'; function getToken() { return wx.getStorageSync(TOKEN_KEY); } function setToken(token) { wx.setStorageSync(TOKEN_KEY, token); } function removeToken() { wx.removeStorageSync(TOKEN_KEY); } function login() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { if (res.code) { wx.request({ url: 'https://api.example.com/api/user/login', method: 'POST', data: { code: res.code }, success: (response) => { const token = response.data.token; setToken(token); resolve(token); }, fail: reject }); } else { reject(new Error('微信登录失败')); } }, fail: reject }); }); } function ensureLogin() { const token = getToken(); if (token) { return Promise.resolve(token); } return login(); } module.exports = { login, ensureLogin, getToken, setToken, removeToken };

很多人会问,为什么这里要包一层Promise而不是直接在页面里写wx.login?因为登录状态会被几乎每一个页面依赖。首页要用户信息、加购物车要用户token、下单更要校验登录态。如果每个页面都自己调一次wx.login,代码会分散到没法维护。封装成ensureLogin之后,任何页面要用户凭证都走同一个入口,统一处理、统一过期刷新,后续维护成本会低非常多。

3.2 全局请求封装,统一拦截器才是正解

wx.request在小程序里的地位等同于axios在Vue里的地位,但它比axios原始得多,连拦截器都没有。如果不在底层做统一封装,每个页面写一遍请求、每个请求单独处理错误,代码会瞬间失控。我的封装方案是基于Promise做一层薄封装,把baseURL、header、token注入、错误拦截、loading管理全部收口到一个文件里。

// request/http.js const { getToken, ensureLogin } = require('../utils/auth'); const BASE_URL = 'https://api.example.com/api'; function request(path, method, data, options = {}) { return new Promise((resolve, reject) => { const token = getToken(); const header = { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }; if (options.loading !== false) { wx.showLoading({ title: options.loadingText || '加载中...', mask: true }); } wx.request({ url: BASE_URL + path, method: method || 'GET', data: data || {}, header, success: (res) => { if (res.statusCode === 200) { const body = res.data; if (body.code === 1) { resolve(body.data); } else if (body.code === 401) { // token过期,重新登录后重试 removeToken(); ensureLogin().then(() => { request(path, method, data, options).then(resolve, reject); }); } else { wx.showToast({ title: body.msg || '请求失败', icon: 'none' }); reject(body); } } else if (res.statusCode === 401) { // 无权限,重新登录 removeToken(); ensureLogin().then(() => { request(path, method, data, options).then(resolve, reject); }); } else { wx.showToast({ title: '服务器异常', icon: 'none' }); reject(res); } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }); reject(err); }, complete: () => { if (options.loading !== false) { wx.hideLoading(); } } }); }); } module.exports = { get: (path, data, options) => request(path, 'GET', data, options), post: (path, data, options) => request(path, 'POST', data, options), put: (path, data, options) => request(path, 'PUT', data, options), del: (path, data, options) => request(path, 'DELETE', data, options) };

这套封装有几个关键点值得说一下。第一个是loading管理,页面正常情况下都会展示加载中,但有某些场景(比如上拉加载更多)不希望每次都弹loading,会通过options.loading:false关掉,这种按场景可配置的设计在真实项目里非常实用。第二个是自动重试逻辑,401的时候自动清token,重新登录后再把原请求重放一遍,用户无感完成登录态刷新。第三个是后端响应体的约定,苍穹外卖后端约定code为1表示成功、其他code表示失败,所以前端封装的这个判断逻辑跟后端是严格对应的。

3.3 为什么接口要统一收敛到api.js管理

除了请求封装,接口路径的集中管理同样重要。我见过太多项目把接口URL直接写在页面里,后面后端改了base路径,前端要把每个页面翻一遍。api.js的好处是:接口定义收敛到一处,改动只动一个文件;同时能清晰看到当前项目到底对接了多少后端能力,后续排查问题时有全局视角。

// request/api.js const { get, post, put, del } = require('./http'); module.exports = { // 登录 login: (data) => post('/user/login', data), // 首页 getBanners: () => get('/home/banner'), getCategories: () => get('/category/list'), getDishList: (params) => get('/dish/list', params), // 上传图片 uploadImage: (filePath) => { // 上传要走wx.uploadFile,不能走wx.request,单独处理 } };

写到这里必须额外提醒一下,上传文件的接口不能走上面这套request封装。wx.uploadFile和wx.request是两套API,前者专门处理multipart/form-data,可以携带文件流并同时附带额外formData参数。我习惯在api.js里把uploadImage单独导出一个函数,因为这个操作跟普通JSON请求的差异太大,硬塞进通用request里只会把封装搞复杂。

4. 页面顶部导航栏高度适配

4.1 为什么顶部导航栏的高度不能写死

小程序的顶部导航栏由两部分组成:状态栏和导航栏。状态栏就是手机顶部显示电量、时间的那一小条;导航栏是承载页面标题和返回按钮的那一条。安卓和iOS的刘海屏、挖孔屏、胶囊按钮的位置都不尽相同,如果写死一个高度,可能出现标题被挖孔遮挡,或者返回按钮和胶囊重叠。所以最好的方案是让小程序自己读取系统信息,动态计算导航栏高度。

当初我第一次做小程序时,直接在wxss里写了navigationBarHeight: 44px,结果在iPhone 13上正常,换到小米手机上,胶囊按钮直接把页面标题顶到看不见。后来才明白,导航栏真实高度要这样算:

// utils/system.js function getNavigationBarHeight() { const systemInfo = wx.getSystemInfoSync(); const menuButton = wx.getMenuButtonBoundingClientRect(); // 导航栏高度 = 胶囊按钮高度 + 上下各留8px间距 const navigationBarHeight = menuButton.height + (menuButton.top - systemInfo.statusBarHeight) * 2; return { statusBarHeight: systemInfo.statusBarHeight, navigationBarHeight, // 菜单按钮位置信息 menuButton }; }

这个计算逻辑的原理是:微信官方说要保持导航栏内元素和胶囊按钮垂直居中,所以拿胶囊按钮的top减去状态栏高度,得到的是胶囊距状态栏底部的间距,上下各留相同间距,再加上胶囊本身的高度,就是整个导航栏的精确高度。为了省事也可以distory后存到globalData里,页面直接读取。

4.2 自定义导航栏的完整方案

Day6我选择了自定义导航栏,而不是用系统默认的。原因是默认导航栏的样式灵活性很低,背景颜色、文字大小都受限,且无法在页面上叠加自定义搜索框或其他组件。自定义导航栏的思路是:在app.json的window配置里把navigationStyle设为custom,让整个顶部区域都交给页面自己布局。

{ "window": { "navigationStyle": "custom" } }

然后在页面里根据上面算出的高度铺一个定位容器,里面放状态栏占位、标题、以及可扩展的其他内容。比如首页想在导航栏里直接放搜索框,就把搜索框组件跟导航栏做在一起。需要注意Custom模式下,页面内容会顶到屏幕最上方,所有页面的第一个元素都要主动避开这个高度,否则内容会被状态栏盖住。我统一封装了一个占位组件,所有页面开头都放它,后续新增页面也不会忘了适配。

5. 首页数据加载与分类联动

5.1 首页接口返回结构的设计

进入首页第一个要处理的问题,是数据接口怎么定义。大部分外卖小程序的首页,其实由好几个业务块组成:轮播图、公告、分类、菜品列表。最直观的做法是后端为一个页面聚合一个接口,一次返回所有数据。这个小项目当时选择的是分接口拉数据,各区块通过请求并发操作,可以各自维护,后续运营改动一个模块不会影响其他模块。

这里多说一句,接口设计没有绝对的标准,页面复杂度低时聚合接口更省事,业务复杂了拆开更灵活。苍穹外卖的首页我采用的是:轮播图和分类列表分别走独立接口,菜品列表和分类联动单独走分页接口。首页的onLoad会并行发起三个请求,虽然请求数量多了,但是每个接口返回的数据量更小,失败也能隔离单点。

5.2 分类联动菜品列表,要处理排序和切换的竞态

分类联动这块今天花了最多时间调优。用户点击左侧分类,右侧菜品列表要整体换成该分类下的菜品。听起来简单,但这个交互里有几个容易出问题的细节。第一个细节是点击分类时页面状态的处理,如果用户手速快,快速切换了三个分类,可能出现先点的分类响应慢、后点的分类响应快,结果页面最终显示的是先点的分类数据,这是典型的网络竞态问题。

处理竞态的办法也不复杂,在onLoad里用一个loading状态标记当前请求,请求返回后先判断当前选中分类是否还是发起请求时的那个分类,如果不是就丢掉这次数据。页面代码加上一个id标记即可做到:

// pages/index/index.js Page({ data: { categories: [], currentCategoryId: 0, dishList: [], loading: false, page: 1, pageSize: 10, hasMore: true }, switchCategory(e) { const id = e.currentTarget.dataset.id; this.setData({ currentCategoryId: id, dishList: [], page: 1, hasMore: true }); this.loadDishList(true); }, loadDishList(isRefresh = false) { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); const requestId = this.data.currentCategoryId; api.getDishList({ categoryId: requestId, page: this.data.page, pageSize: this.data.pageSize }).then((res) => { // 关键检查:确保返回时当前分类没有变 if (requestId !== this.data.currentCategoryId) return; const list = isRefresh ? res.list : this.data.dishList.concat(res.list); this.setData({ dishList: list, hasMore: res.list.length === this.data.pageSize, page: this.data.page + 1 }); }).finally(() => { if (requestId === this.data.currentCategoryId) { this.setData({ loading: false }); } }); } });

这段代码里最重要的就是那个requestId判断。处理完竞态问题之后,页面切换分类的稳定性会好很多。还有很多项目会在这里搭配一个骨架屏方案,让用户在数据没返回时看到骨架而不是空白。不过骨架屏需要额外写一套组件,Day6我暂时用loading动画顶着,后续有时间再迭代。

5.3 列表加载更多时wxss和wxml配合

关于加载更多的UI交互,我的习惯是底部放一个状态提示。一共有三种状态:加载中(转圈或文案提示)、已加载全部(显示没有更多了)、加载失败(提供重试)。这三种状态的处理逻辑如果分散在每个页面,后期改起来工程量不小。但这天我采用的是最简单的方案,底部的“上拉加载更多”通过onReachBottom触发,触底时回调会自然执行分页加载。

有几个注意事项需要特别说明。onReachBottom默认触底距离是50px,可以通过在app.json的window里配置onReachBottomDistance来调整,但对这个项目来说默认值就够用。另外一个真正要小心的是,如果页面里同时存在scroll-view和原生页面滚动,onReachBottom只在页面级滚动时触发,scroll-view内部的滚动到底不会触发这个钩子,如果列表外层套了scroll-view,分页加载就会失效。

6. 本地上传图片功能实现

6.1 上传图片前先压缩和校验,后端也要配合

Day6另一个任务是实现“本地上传图片”。这个功能在评论、店铺头像、菜品反馈这些场景下都会用到。小程序端的wx.chooseMedia支持从相册选择或直接拍照,拿到临时文件路径之后,先做校验再调用上传。

需要注意的一点是,wx.chooseMedia选出来的图片体积可能很大,现在手机随便拍一张都三五MB。如果直接上传到后端,图片处理服务器的压力、流量消耗都是问题。所以在客户端做一个压缩是非常必要的。小程序支持用canvas压缩图片,也可以用wx.compressImage接口,这个接口是官方提供的专门做压缩能力的API,压缩质量参数可调。我采用的策略是:超过500KB的图片先用compressImage压到500KB以内再上传。

function compressAndUploadImage(filePath) { return new Promise((resolve, reject) => { wx.compressImage({ src: filePath, quality: 80, success: (res) => { // 压缩完成,校验大小 const fs = wx.getFileSystemManager(); fs.getFileInfo({ filePath: res.tempFilePath, success: (info) => { if (info.size > 1024 * 1024) { wx.showToast({ title: '图片不能大于1M', icon: 'none' }); reject(new Error('图片太大')); } else { uploadFile(res.tempFilePath); } } }); }, fail: reject }); }); }

还有一个细节是,上传图片时后端返回的是图片的URL路径,但小程序端在真机上访问这个URL需要网络权限配置。开发调试阶段,把项目详情里“不校验合法域名”的勾选打开,就能在开发者工具中正常访问本机后端。但真机预览时如果不做域名配置,图片会加载不出来。这个真想提到生产环境,必须把后端域名配到微信公众平台的后台“request合法域名”和“uploadFile合法域名”里,图片访问域名也要配,同时要求后端接口必须走HTTPS。

6.2 wx.uploadFile和wx.request在请求头处理上的差异

调用wx.uploadFile时的header处理,和wx.request几乎完全不同,这是很多同学踩坑的高发区。wx.request默认Content-Type是application/json,加token在header里直接写Authorization即可。但wx.uploadFile是multipart/form-data格式,如果在header里手动设置Content-Type会导致boundary缺失,后端解析请求体直接失败。

function uploadFile(filePath) { const token = getToken(); return new Promise((resolve, reject) => { wx.uploadFile({ url: BASE_URL + '/upload', filePath, name: 'file', // 这里只传Authorization,不手动设置Content-Type header: { 'Authorization': token ? `Bearer ${token}` : '' }, formData: { // 额外的业务参数,比如图片类型 type: 'dish' }, success: (res) => { const data = JSON.parse(res.data); if (data.code === 1) { resolve(data.data.url); } else { reject(new Error(data.msg)); } }, fail: reject }); }); }

必须强调:wx.uploadFile里header不用也不能手动写Content-Type。微信底层会自动加上带boundary的multipart声明,一旦你手动设置,上传接口大概率返回415。这个坑我印象深刻,当时是服务端同事排查了半天,最后发现是前端手动设置了Content-Type导致的,纯前端一个多余的设置居然坑了整个联调流程。

7. 用户离开小程序时机的监听

7.1 onHide和onShow在业务上的实际区别

外卖小程序跟其他工具类小程序有个很大的区别:用户可能中途切出去回个消息,或者接个电话,再回来继续下单。这种情况下,如果小程序的页面状态没有处理好,可能用户切回来发现购物车没了、列表状态混乱了。Day6预留的这个离开与返回监听能力,在后续做订单状态刷新、购物车数据恢复时会派上大用场。

页面Page里自带onHide和onShow两个生命周期。onHide是页面被切换到后台(比如用户按Home键退出了小程序)时触发,onShow是页面重新回到前台时触发。很多新手会把这俩跟app.js里的onHide/onShow搞混。app.js里的onHide是“小程序整体切后台”时触发,页面的onHide则是“当前页面被隐藏”时触发,包括但不限于小程序切后台、跳转其他页面、打开半屏的弹窗。判断用户是离开小程序还是只是页面跳转,不能在页面钩子里单独判断,要结合app.js的onHide看先后顺序。

7.2 用app.js统一管理前后台切换状态

更合理的方案是,在app.js里维护一个全局的isAppBackground状态,同时在页面onShow里结合这个状态判断是否需要刷新数据。比如用户从后台回小程序,切到购物车页,就应该重新请求一次购物车最新数据,而不是展示切出前的旧数据。

// app.js App({ globalData: { isAppBackground: false }, onHide() { this.globalData.isAppBackground = true; }, onShow() { this.globalData.isAppBackground = false; } });

然后在页面的onShow里做判断:

onShow() { const app = getApp(); if (app.globalData.isAppBackground) { // 用户刚刚从后台回到小程序,刷新当前页数据 this.refreshData(); } }

之所以要用这种方式,是因为页面onShow触发时无法知道用户是从小程序外切回来的,还是从另一个页面返回的。有了这个全局标记辅助判断,才能区分两种情况。如果不做区分,每切回一个页面都请求一次接口,页面栈里的数据全都要刷新,对用户体验反而是一种伤害。

8. 常见问题与排查技巧实录

8.1 真机预览接口请求失败,开发者工具却正常

这是几乎每个人都会遇到的经典问题。开发者工具里所有接口都通,一上真机,所有请求全部失败,报net::ERR_CERT_COMMON_NAME_INVALID或网络错误。最常见的原因是开发者工具默认不校验合法域名,真机则会强制校验。解决办法分两步:第一步,开发阶段可以在微信公众平台把“不校验合法域名”打开,但只能在开发调试版生效;第二步,真正对接生产环境时,务必把HTTPS域名配置到后台白名单里,并确认SSL证书是正规机构签发的,微信不认自签证书。

还有一小概率是后端接口返回的header带特殊字段,触发了微信的安全机制。之前遇到一次后端在header里返回了token刷新标记,前端在开发者工具里能读到,真机上被微信安全层拦截了,表现也是请求失败。这类问题排查时直接打开后端日志看有没有接受到请求,比看小程序端的报错信息有用得多。

8.2 请求封装好了,但仍然偶发401重试死循环

按前面封装的逻辑,请求遇到401会自动清token、重新登录、再重放原请求。但有一种极端情况就是:登录接口本身返回的就是401。一旦这种情况出现,整个请求链会陷入递归死循环,每次调登录接口都401,然后无限重试,用户手机电量悄悄被耗尽,接口还一直占用。

要规避这个问题,循环入口的登录接口必须排除在重试逻辑之外。我后来在request函数里加了一个参数,专门标记当前重试的次数,超过两次直接提示“登录状态异常,请重新打开小程序”,并清空本地所有状态。这个兜底逻辑非常重要,尤其项目上线以后,用户网络环境复杂、后端并发压力大,偶发401的频率比想象中高,没有兜底真的会出事。

8.3 分页加载数据重复,问题不在前端也不在后端

分页数据重复是列表加载里常见问题。后端接口正常返回了page参数对应的数据,前端拼接的时候却发现每页尾部会出现上一页的数据,甚至在并发请求的场景下页面会出现数据错乱。这个问题的根源往往出在“重复触发加载”上。onReachBottom在你把页面滑到接近底部的时候触发一次,如果用户手还在继续滑动,可能再次触发onReachBottom,导致同一页的数据被请求了两次。

我采用的方案是:加一个loading锁,请求没完成时不再触发第二次加载。这个锁在代码里其实就是if (this.data.loading) return。但只加锁还不够,有时候点击分类切换很快时,上一次请求还没结束就切了新分类,原先分类的请求回来会被丢弃,而新分类的请求如果晚到,又可能被当成旧请求丢弃,导致页面数据空白。这里要用前面提到的requestId竞态判断,同时保留一个页面级标记来控制当前数据归属,两个机制配合使用才能让分页加载稳定。

8.4 缓存导致图片上传成功后页面显示旧图

小程序里的图片缓存策略是,同一路径的图片短时间内不会重新请求,即使在服务端图片内容已经发生了替换。这会导致用户上传完新图片,页面回显的还是旧图,造成“上传失败”的错觉。破局方案是给图片路径加上时间戳参数:

const url = `${fileUrl}?t=${Date.now()}`;

这种加时间戳的方式在服务端不识别的时候会造成多余参数,但一般后端不会拦截query参数,图片服务也能正常返回。如果后端对路径校验严格,就换成用缓存清理接口或者让后端在图片更新时生成新的随机文件名。苍穹外卖这里后端生成文件名时本身就带了时间戳,所以这个坑反而没踩到,但仍值得记住。

8.5 顶部自定义导航栏遮挡页面内容

自定义导航栏后,页面第一个元素会直接顶到屏幕顶部,被状态栏覆盖。有人会问,为什么不用默认导航栏呢?因为默认导航栏做不到背景渐变、做不到导航栏内嵌搜索框。所以还是回到自定义的方案上。处理方式很简单,全页面在wxml里引入一个占位组件,组件高度取statusBarHeight加navigationBarHeight之和。这个占位组件放在每个页面最顶部,后续所有页面都统一遵守这个规范,就不会出现内容被遮挡的问题。

末尾再加一个使用建议,iphone和安卓的状态栏高度不同,用wx.getSystemInfoSync拿到statusBarHeight直接赋值是最稳的。不要用CSS的env(safe-area-inset-top)去硬适配,微信小程序的CSS变量兼容性没那么理想,真机经常拿到不准确的值。

今天Day6的内容,核心工作就是把小程序端的地基打牢。登录流程、请求封装、导航栏适配、分页加载、图片上传这些能力,在后续做购物车、订单列表、个人中心时都会反复用到。按目前的进度,明天可以着手开始做点餐流程的完整串联了,购物车加菜、下单、结算这套主流程跑通之后,整个苍穹外卖项目基本就到了可以拿出去演示的状态。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:19:24

用Python抓取淘宝京东评论,SnowNLP情感分析实战

简介:这是一套面向毕业设计及期末大作业场景的Python综合项目资料,聚焦淘宝、京东商品数据爬取与评论情感分析,适用于具备一定Python基础、希望完成完整课程项目或毕设系统的学习者。资源共103个文件,包含py源码、csv数据集、jpg/…

作者头像 李华
网站建设 2026/10/1 12:19:14

AI工程化从零锻造:五大支柱实战指南

1. 这不是“搭积木”,而是亲手锻造AI系统的完整工程链 “AI Engineering from Scratch”——看到这个标题,很多人第一反应是:“又要从零写Transformer?还是手推反向传播?”其实完全不是。我带过六支AI产品团队&#xf…

作者头像 李华
网站建设 2026/10/1 12:18:34

YOLO数据增强实战:txt标注同步变换的六种方法与避坑指南

简介:面向目标检测入门与进阶学习者,资源聚焦YOLO已标注数据集的智能增强,解决小样本训练易过拟合、标注样本不足等常见问题。压缩包共6个文件,包含3个Python脚本、1个说明文档、1个附赠内容压缩包及1个备份文件,整体仅…

作者头像 李华
网站建设 2026/10/1 12:18:33

工业缺陷检测实战:小样本训练与漏检控制完整方案

各位做工业视觉的同行,今天想聊一个绕不开的话题——缺陷检测里的小样本训练和漏检控制。这俩问题在产线上几乎是绑定出现的:缺陷样本永远不够用,但客户对漏检率的要求永远是零。我见过太多项目死在漏检上,不是模型不好&#xff0…

作者头像 李华
网站建设 2026/10/1 12:18:12

Windows下MinIO服务化:用NSSM实现后台运行与开机自启

1. 前言:为什么要把 MinIO 放在 Windows 后台运行做对象存储的同学应该都遇到过这个场景:本地开发环境用的是 MinIO,在命令行窗口敲了minio server D:\data启动服务,开发调试一切正常。可一旦关掉这个黑色的命令行窗口&#xff0c…

作者头像 李华
网站建设 2026/10/1 12:18:12

Python实现EPUB转PDF:无损排版转换完整方案与源码解析

EPUB 转 PDF,网上那些工具我是真不放心。自己用 Python 写过一个转换脚本,完整跑通了无损排版转换,今天把这套方法和源码分享出来。这次不是闹着玩的改造,是一个能处理实际书籍、保留原始排版格式的完整方案。 先说结论&#xff…

作者头像 李华