简介:这是一份面向小程序开发者及AI应用入门者的微信小程序AI聊天页面源码,聚焦于将大语言模型能力接入小程序,帮助读者快速实现带人设的智能对话界面。代码覆盖前端实现的关键部分,包括wxml页面结构、wxss样式、js交互逻辑、json配置,以及分别用于用户和机器人的png头像图片,整体可组合成一个可直接运行的聊天页面模板。资源共6个文件,压缩包约386KB,轻量易用,适合直接导入微信开发者工具预览与改造。页面逻辑预留了对接LLM接口的位置,可结合后端服务或云函数发起真实对话请求;即使暂不接入后端,也能通过修改系统提示词来设定儿童教育专家、商城客服、法律顾问等角色,并自定义回答语气与要求。目前已有978人学习下载,可用于课程设计、期末项目、业务演示,或作为小程序AI功能快速起步的基础模板,尤其适合需要快速验证对话交互效果的产品原型场景。
1. 微信小程序聊天页面对接LLM:为什么照着协议写还是跑不通
微信小程序聊天页面加上AI大语言模型接口,这个组合看起来工程量不大:左边一个对话列表,右边一个输入框,中间发一个请求。但我在实际对接里发现,真正把项目做完的人,多数时间不是花在画页面上,而是卡在小程序网络层和LLM服务之间那几道看不见的墙:域名白名单、端口限制、缺失的流式通道、上下文一长就失控。问题不在于你有没有能力调通一个HTTP接口,而在于“微信允许你这么调吗”。
这篇文章按我平时做项目的顺序拆:先把聊天页面的数据模型和滚动定位做对,再讲怎么在小程序网络限制下安全地调到LLM,包括非流式最小链路和WebSocket中转的流式方案,最后是上线前必查的坑位清单。适合谁:准备在微信小程序里做AI对话的前端或全栈开发者,以及已经拿到LLM接口、正在联调阶段反复踩坑的团队。你照着章节走,能拿到一套能真机预览、能持续对话、能上线的小程序源码链路,而不是一个只能在模拟器里转圈的Demo。
2. 先把聊天页面做对:消息数据模型与滚动定位
聊天页面最容易出问题的不是样式,而是数据模型。很多初版Demo把messages做成一个简单的字符串数组,字段只有“发送者”和“内容”,结果后面加状态、加流式追加、加失败重试时,代码反复重构。我一般会在一开始就把消息结构定好,后面所有功能都往这个结构上挂。
2.1 消息数组不只是数组:给每条消息一个独立的渲染状态
先看单条消息的设计:
// pages/chat/data.js // 单条消息的完整结构 const messageSample = { id: 'msg_1720000000001', // 唯一ID,建议用时间戳 + 随机数生成 role: 'user', // 'user' | 'assistant' | 'system' content: '', // 当前消息文本,流式输出时会被反复追加 status: 'done', // 'sending' | 'loading' | 'done' | 'error' time: 1720000000001 // 时间戳,纯聊天场景可以不展示 }为什么要把status独立成一个字段:用户发出去的消息需要显示“发送中”,但文本还在输入框里,不能算发送成功;LLM的回答一开始是空字符串,需要显示一个打字光标并进入loading状态;请求失败后要把这条消息标记为error,方便在UI里给出重试按钮。这些状态不是文案变化,而是UI分支。如果只用content是否为空来判断状态,流式输出一到,逻辑就分不清“还没返回”和“已经返回空串”的区别。
有了消息结构,就得配套消息更新方法。最常见的问题是每次收到一条新消息就把整个messages数组setData一遍,这在后面接流式输出时会成为性能灾难。我习惯把更新动作收敛成一个方法:
// pages/chat/chat.js // 更新单条消息,只替换变化的那一项,不整体重建数组 updateMessage(id, patch) { const messages = this.data.messages.map(msg => { if (msg.id === id) { return { ...msg, ...patch } } return msg }) this.setData({ messages }) } // 追加一条消息,返回消息ID,后面流式更新要用 appendMessage(role, content, status = 'done') { const msg = { id: 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000), role, content, status, time: Date.now() } this.setData({ messages: [...this.data.messages, msg] }) return msg.id }逻辑说明:updateMessage遍历数组找出目标消息,用patch合并新字段,再整体setData。这里没有做更极致的性能优化,是因为小程序setData对数组diff有内置处理,多数场景下这个写法足够。appendMessage返回的id是后续流式输出的关键索引,必须保留。
在onLoad里还要初始化一条欢迎语和会话ID:
// pages/chat/chat.js onLoad() { const sessionId = 'session_' + Date.now() this.setData({ sessionId }) this.appendMessage('assistant', '你好,我是你的AI助手,有什么可以帮你?', 'done') }sessionId的用处是服务端关联上下文和费用统计,首条欢迎语让用户一进来就知道页面可用。
2.2 用 scroll-view 把滚动和定位握在自己手里
聊天页面不要用页面级滚动,要用scroll-view配合scroll-into-view。原因很简单:页面级滚动无法方便地定位到某条消息的DOM位置,而scroll-into-view可以直接把指定ID的消息滚到可视区顶部。
<!-- pages/chat/chat.wxml --> <scroll-view class="message-scroll" scroll-y scroll-into-view="{{scrollIntoId}}" scroll-with-animation="{{scrollWithAnim}}" bindscroll="onScroll" > <view wx:for="{{messages}}" wx:key="id" id="msg_{{item.id}}" class="message-row {{item.role}}" > <text class="bubble">{{item.content}}</text> <text wx:if="{{item.status === 'loading'}}" class="cursor">▊</text> <text wx:if="{{item.status === 'error'}}" class="retry" bindtap="onRetry">// pages/chat/chat.js scrollToBottom() { const last = this.data.messages[this.data.messages.length - 1] if (!last) return this.setData({ scrollIntoId: 'msg_' + last.id, scrollWithAnim: true }) }参数说明:scroll-into-view的值必须对应页面内某个view的id,id用msg_前缀拼接,避免纯数字ID在某些机型上失效。scroll-with-animation控制在滚动时是否带动画。这里有一个容易忽略的点:流式输出时每条delta都触发滚动动画,动画之间会互相打架,视觉上反而一顿一顿的。所以流式期间我会关闭动画:
// 流式期间直接跳到底部,不开动画 appendDelta(text) { this.setData({ scrollWithAnim: false, scrollIntoId: 'msg_' + this.currentMsgId }) }再补充一个体验细节:用户正在上翻历史消息时,底部来了新消息直接把他拉回底部,很打断阅读。常见做法是在bindscroll里判断scrollTop是否接近底部:接近则自动滚,不接近则设一个标志位,等用户手动回到底部后再恢复自动滚动。这个代码量不大但很提升好感度,建议加上。
2.3 输入区与发送态的联动
输入区用textarea,设置confirm-type为send,键盘右下角就是发送键,聊天场景不需要换行。
<!-- pages/chat/chat.wxml 输入区 --> <view class="input-bar"> <textarea value="{{inputText}}" bindinput="bindInput" bindconfirm="onConfirm" confirm-type="send" maxlength="2000" placeholder="输入你的问题..." adjust-position="{{true}}" ></textarea> <button size="mini" disabled="{{sending || !inputText.trim()}}" bindtap="onSend" >发送</button> </view>// pages/chat/chat.js data: { inputText: '', sending: false } bindInput(e) { this.setData({ inputText: e.detail.value }) } onSend() { const text = this.data.inputText.trim() if (!text || this.data.sending) return this.setData({ sending: true, inputText: '' }) this.appendMessage('user', text, 'done') this.requestAnswer() // 请求逻辑在第4章 } onConfirm() { this.onSend() }参数说明:maxlength设2000是给输入留余量,避免超长文本把请求体撑爆。sending和inputText双条件控制按钮禁用状态,防止用户在请求返回前重复点击。textarea的adjust-position设为true,键盘弹起时会自动把输入区顶上来;如果发现键盘顶起后页面错位,再配合页面级onKeyboardHeightChange做手动位移。这里的小技巧是:不要把textarea放在position: fixed的容器里还用padding-bottom顶安全区,否则在部分安卓机上键盘弹起时会闪一下。
3. 网络边界:微信的域名白名单到底拦住了什么
很多人在模拟器里调试时HTTP请求一切正常,一换真机预览就报request:fail,这不是代码问题,是微信在小程序网络层加了硬性限制。搞清楚这几条限制,能省掉一整天的定位时间。
3.1 四个硬限制:HTTPS、443端口、域名白名单、证书一致性
| 限制项 | 说明 | 影响 |
|---|---|---|
| 协议 | 正式环境必须HTTPS | 本地HTTP只能在开发者工具里调试 |
| 端口 | 不填端口,只走默认443 | 后端监听8443等端口无法直接接入 |
| 域名 | 必须在小程序后台配置request合法域名 | 未配置直接拦截 |
| 证书 | 域名证书链必须完整且与域名匹配 | 证书过期或不完整同样报错 |
第一条和第二条经常一起出现。wx.request在真机上不支持IP地址访问,后端监听8080、8443这类端口也不能直接连。也就是说,你用http://192.168.1.10:8080/chat在开发者工具里跑通了,换到手机预览一定失败。这个限制对个人开发者尤其难受,因为很多人前期没有正式域名,习惯用局域网IP联调。
解决路径只有一条:把后端服务挂到一个标准HTTPS域名下,并把这个域名配到小程序后台的“开发管理 → 服务器域名 → request合法域名”里。注意:合法的request域名只支持HTTPS(开发调试阶段例外),且域名不能带路径后缀,IP地址一律不允许。
3.2 联调期:开发者工具“不校验合法域名”的正确用法
开发者工具提供了跳过域名校验的开关,这是联调期的第一把钥匙。
操作路径:微信开发者工具右上角“详情 → 本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。勾上之后,模拟器里HTTP和局域网IP都能直接请求,不用配白名单。
但这里有个认知误区要纠正:这个开关只对开发者工具和真机调试模式生效。你用“预览”二维码在手机上打开,哪怕是在开发者工具里勾了跳过校验,手机端依然会校验域名。真机调试模式(预览二维码里带“调试”字样)可以带上这个豁免,但普通预览不行。
所以联调期的正确节奏是:
- 模拟器勾选跳过校验,先跑通页面逻辑和交互。
- 真机调试模式验证触摸、键盘、滚动等真机行为。
- 正式联调时把后端域名配到request合法域名,用普通预览二维码验证。
不要在第一步花太多时间纠结域名问题,页面逻辑没跑通之前,网络层不是主要矛盾。
3.3 上线前:把服务收敛到一个可配白名单的HTTPS域名
把各种接口收敛到一个域名下,是维护成本最低的做法。比如:
| 小程序请求路径 | 对应后端路由 | 作用 |
|---|---|---|
| https://yourdomain.com/chat | POST /chat | 非流式对话 |
| https://yourdomain.com/chat-ws | WSS /chat-ws | 流式对话 |
| https://yourdomain.com/history | GET /history | 历史会话 |
域名建议用独立的二级域名,比如api.yourdomain.com,而不是把接口直接挂在主站域名下。这样将来换证书、换服务商都不影响主站,SSL证书最好开启自动续期,忘记续期导致真机全部请求失败的案例我见过不止一次。
如果你自己不想管域名和证书,还有一个常见替代方案:用小程序的云函数/云托管做中转。云函数调用外部LLM接口不受request合法域名限制,小程序端通过wx.cloud.callFunction调用云函数,也不需要配置域名白名单。这个方案的特点是上线快、不需要运维后端,但要注意云函数的超时限制和并发费用,长对话场景要关注成本。
4. 把LLM接进来:从一次性请求到流式对话
页面就绪后,真正的核心来了:小程序端如何安全地调用LLM。这里有一条铁律:API Key不能放前端。小程序包里的任何代码都能被解包读出来,把密钥写在代码里等于公开。后续所有方案都走中转服务,小程序只和你自己的后端通信。
4.1 先分清LLM接口的两种返回形态
绝大多数LLM服务商提供两种调用方式:
- 非流式:POST请求发出后,服务端在完整生成结束后一次性返回JSON。实现简单,但首字等待时间长,对话稍长就会感觉“卡住”。
- 流式(SSE):服务端把生成内容按token分块推送,客户端边收边渲染。体验接近真人打字,但小程序没有原生的SSE消费能力。
这里的核心限制是:小程序没有EventSource对象,wx.request拿到的永远是完整响应,不会把SSE流逐段返回给你。很多后端同学直接把LLM服务商的流式SDK接到小程序后端接口上,结果小程序端只等来一个“整体等待后突然出现全部文本”的响应,这不是接对了,是把流式数据当普通JSON响应收下了。
所以我的判断是:快速验证走非流式,正式产品走流式,流式必须要有一层“SSE → WebSocket”的转换。
4.2 最小可跑通:wx.request 走非流式
先用最简单的方式打通整条链路,哪怕体验差点,先证明业务通顺:
// pages/chat/chat.js requestAnswer 非流式版本 requestAnswer() { // 先放一条空的assistant消息,状态为loading const assistantId = this.appendMessage('assistant', '', 'loading') this.currentMsgId = assistantId // 组装上下文:只带已完成的对话,不带半截的流式内容 const history = this.data.messages .filter(m => m.role !== 'system' && m.status === 'done') .slice(-10) // 只保留最近10轮,控制请求体大小 .map(m => ({ role: m.role, content: m.content })) wx.request({ url: 'https://yourdomain.com/chat', // 自己的中转服务 method: 'POST', header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (wx.getStorageSync('token') || '') }, data: { sessionId: this.data.sessionId, messages: history }, timeout: 60000, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 0) { const answer = res.data.data.answer this.updateMessage(assistantId, { content: answer, status: 'done' }) } else { const msg = (res.data && res.data.msg) ? res.data.msg : '服务异常' this.updateMessage(assistantId, { content: '请求失败:' + msg, status: 'error' }) } }, fail: () => { this.updateMessage(assistantId, { content: '网络请求失败,请检查域名白名单或网络状态', status: 'error' }) }, complete: () => { this.setData({ sending: false }) } }) }参数说明:history的filter和slice要配合使用,filter去掉loading和error状态的消息,防止把半截文本和错误信息发给模型;slice(-10)是轮数上限,这个值不是死的,取决于模型的上下文窗口。timeout设60000是因为LLM生成几十秒很常见,设短了长回答会被timeout打断。fail分支里不要只提示“网络错误”,把域名白名单提示带上,能减少大量咨询。
后端中转服务(Node.js)最小实现:
// server/chat.js Node.js 非流式中转 const express = require('express') const router = express.Router() router.post('/chat', async (req, res) => { const { sessionId, messages } = req.body // 从服务端环境变量读取LLM密钥,绝不下发到小程序端 const llmResponse = await callLLM({ model: process.env.LLM_MODEL, // 模型名从环境变量读取 messages, temperature: 0.7, max_tokens: 1024 }) res.json({ code: 0, data: { answer: llmResponse.choices[0].message.content } }) })逻辑说明:callLLM是你选择的服务商SDK或fetch封装,这里没有贴具体服务商代码,因为各家SDK不同,但结构一致。密钥从process.env读取,这样代码仓库里不会出现真实密钥。返给小程序的数据统一包一层code字段,前端判断业务成功与否,而不是把HTTP状态码直接透传。
4.3 流式体验:用WebSocket把增量文本转发到小程序
如果你想产品化,必须上流式。先看后端怎么把LLM的SSE流转成WebSocket消息:
// server/ws-chat.js Node.js WebSocket 流式中转 const WebSocketServer = require('ws').Server const wss = new WebSocketServer({ port: 8080, path: '/chat-ws' }) wss.on('connection', (ws) => { ws.on('message', async (raw) => { const req = JSON.parse(raw) // callLLMStream 返回一个异步迭代器,逐段拿到文本 const stream = callLLMStream({ model: req.model || process.env.LLM_MODEL, messages: req.messages, temperature: 0.7 }) try { for await (const chunk of stream) { // 把增量文本包装成事件,推给小程序 ws.send(JSON.stringify({ event: 'delta', text: chunk.text })) } ws.send(JSON.stringify({ event: 'done' })) } catch (err) { ws.send(JSON.stringify({ event: 'error', message: err.message })) } }) })小程序端:
// pages/chat/chat.js requestAnswer 流式版本 requestAnswer() { const assistantId = this.appendMessage('assistant', '', 'loading') this.currentMsgId = assistantId this.pendingText = '' this.socketTask = wx.connectSocket({ url: 'wss://yourdomain.com/chat-ws', header: { token: wx.getStorageSync('token') || '' } }) this.socketTask.onOpen(() => { // 必须等连接建立之后再发送消息 this.socketTask.send({ data: JSON.stringify({ sessionId: this.data.sessionId, messages: this.buildContext() }) }) }) this.socketTask.onMessage((frame) => { const data = JSON.parse(frame.data) if (data.event === 'delta') { this.appendDelta(data.text) } else if (data.event === 'done') { this.finishAnswer() this.closeSocket() } else if (data.event === 'error') { this.updateMessage(this.currentMsgId, { content: data.message, status: 'error' }) this.closeSocket() } }) this.socketTask.onError(() => { this.updateMessage(this.currentMsgId, { content: '连接被中断,请重试', status: 'error' }) }) }这里有几个必须注意的点。第一,必须等onOpen回调触发后再send,否则消息发不出去,这是微信WebSocket的硬性要求。第二,token放header而不是URL参数,一方面避免URL长度溢出,另一方面URL会出现在网络日志里,敏感信息不要放。第三,LLM首字可能很久才返回,WebSocket连接在长时间没有数据时可能被中间链路断开,建议onOpen之后起一个定时器,每25秒发送一次ping保持连接,收到数据后重置计时器;连接关闭时清掉定时器。
生产环境部署时,Node服务监听8080端口没问题,但小程序端只能连wss的443端口。你需要在网关层把wss://yourdomain.com/chat-ws路由到后端8080的/chat-ws路径,证书配置在网关层,小程序端不用感知真实端口。这是一个典型部署拓扑,如果只开8080让小程序直接连,真机一定连不上。
4.4 该调的参数:temperature、max_tokens 与上下文轮数
对接LLM不是把请求发出去就结束了,参数的调节直接决定输出质量和成本。
| 参数 | 建议值 | 说明 |
|---|---|---|
| temperature | 0.7 通用,0.2 事实问答,0.9 创意文案 | 越高越发散,越低越保守 |
| max_tokens | 512 ~ 1024 起步 | 过小回答被截断,过大会拉长等待时间 |
| top_p | 0.9 | 与temperature二选一调整,不要同时大幅调整 |
| system prompt | 服务端配置 | 角色设定放服务端,小程序端只传用户输入 |
| 上下文轮数 | 最近10轮或总字符2000以内 | 控制请求体大小和费用 |
max_tokens是最容易出问题的参数。设小了,长回答会被硬截断,用户看到一句话说到一半就没了;设大了,等待时间变长,流式首字也变慢。我的做法是先从1024起步,观察业务里最长回答的token数,再决定是往上加到2048还是往下调到512。
上下文轮数的裁剪建议在服务端做,不要在客户端拼好再传。客户端难以准确估算token数,服务端可以记录每轮对话的实际token消耗,按“预算上限”动态截断。另一个细节:重要信息比如用户昵称、偏好设定,要单独放进system prompt,不要让它混在对话历史里。对话历史一长,system prompt容易被挤出上下文窗口,模型就开始“失忆”。我在对接时还习惯给每个会话做一份费用日志,记录每次请求的token数,这样月底对账时不用靠猜。
5. 避坑:小程序对接LLM的5个高频问题现场
这一章是实际对接里翻车最频繁的位置,每一条都按“现象 → 原因 → 解决”来写,你可以直接把它当排查手册用。
5.1 模拟器正常,真机发起请求就是 request:fail
现象:开发者工具里点发送,接口正常返回;用手机扫预览码,请求秒失败,控制台报错request:fail或url not in domain list。
原因:开发者工具勾选的“不校验合法域名”只在工具和真机调试模式下生效,普通预览码不豁免。真机必须走HTTPS且域名必须配置在request合法域名列表里。
处理:到小程序后台把HTTPS域名加进request合法域名,确认SSL证书没过期、证书链完整。注意合法域名不支持IP和自定义端口,开发期就按这个约束来,别先在局域网IP上调通了再迁移域名,迁移时会冒出一堆证书和端口问题。
5.2 说好的打字机效果,变成等待十几秒后整段蹦出来
现象:后端已经接了LLM的流式接口,小程序端也写了onMessage,但用户看到的是转圈几十秒后直接出现完整回答。
原因:小程序没有SSE消费通道,wx.request拿到的是完整响应。如果后端把LLM服务商的SSE流原样从小程序接口返回,小程序只会当成一次普通响应处理,等流结束才触发。
处理:确认链路里有“SSE → WebSocket”的转换,而不是把SSE透传给小程序。服务端用流式SDK逐段读取LLM内容,每读一段就通过WebSocket推一段。联调时可以用小程序的Network面板看WebSocket帧,正确情况下会看到连续的delta帧,而不是一次性的大响应。
5.3 对话一长,模型开始答非所问
现象:前几轮回答得很准,十轮以后模型开始重复提问内容、忽略用户最新指令,甚至开始编造不存在的设定。
原因:上下文没做好裁剪,所有历史消息全量塞给LLM,超出上下文窗口后最早的内容被丢弃,最新指令上下文也可能被截断。还有一种情况是system prompt被对话历史挤出了窗口。
处理:服务端做上下文守卫,限制轮数或字符数;重要信息单独固化到system prompt里,不要让它随对话滑动。日志里记录每次请求实际发送的token数,超限时自动裁剪并打印告警。这里不要偷懒用“只取最后N条消息”的简单策略,因为N条消息的token数波动很大,按token数裁剪更可靠。
5.4 消息变多后,页面滚动卡顿、偶尔白屏
现象:聊天超过50条后,低端机滑动有明显掉帧,流式输出时更明显,极端情况页面直接白屏。
原因:messages数组全量渲染,流式输出时每来一个delta就setData一次整个数组。当单次setData的JSON序列化体积超过限制时会直接报错,长文本回答很容易触发。
处理:页面上只渲染最近30条,更早消息折叠成“查看历史”,或者用二维结构只更新有变化的项。流式渲染必须节流,把多次delta合并成一次setData,这个在第6章给具体代码。不要把整篇超长回答一次性写入content再setData,分段追加是更稳妥的做法。
5.5 接口通了但服务端总是返回 401 / forbidden
现象:联调时后端日志里频繁出现鉴权失败,但postman里用同样的参数请求正常。
原因:最常见的是小程序端token没传,或者token存的位置不对。还有一种情况是API Key写在了小程序源码里,被第三方直接盗用刷额度,后端日志里会看到大量陌生IP调用。
处理:密钥全部放服务端,小程序端只保存用户自己的token,每次请求在header里带Authorization。服务端做用量限制,比如单用户每小时请求数、单日token上限,防止被盗刷。如果日志里出现陌生sessionId,立即轮换密钥并设置额度告警。
6. 把体验做上去:流式刷屏、会话缓存与一套自测方法
6.1 渲染节流:onMessage 不要每次都 setData
流式输出时,LLM的token推送频率很高,每秒可能来好几个delta,如果每个delta都setData一次,主线程会被渲染占满,页面开始掉帧。解决方法是节流:
// pages/chat/chat.js appendDelta(text) { this.pendingText += text if (this.renderTimer) return this.renderTimer = setTimeout(() => { this.updateMessage(this.currentMsgId, { content: this.pendingText }) this.pendingText = '' this.renderTimer = null }, 80) // 80ms 合并一次渲染,保留打字机观感 }参数说明:80ms左右的间隔既能保留打字机逐字出现的视觉感,又不会让低端机卡顿。如果用户正在滑动屏幕,可以把间隔临时加大到150ms,甚至暂停渲染,等滚动停止再去刷新。渲染掉帧在部分机型上属于玄学问题,但节流是目前验证过最有效的抓手,不要跳过。
6.2 会话缓存与一套轻量自测清单
历史会话写到本地缓存,保证用户杀掉小程序再进来,对话还在:
// 只保留最近50条,避免本地缓存无限膨胀 const history = this.data.messages.slice(-50) wx.setStorageSync('chat_history_' + this.data.sessionId, history)缓存要在finishAnswer和退后台时各写一次,退后台那次防止用户直接杀掉进程丢数据。读取时按sessionId读,没有历史就展示欢迎语。
自测清单我每次改完网络层都会过一遍:
- 模拟器勾不勾“不校验”都跑通一遍
- 真机预览走HTTPS,确认无域名白名单报错
- 弱网(开发者工具低速网络模式)下发送与重试
- 连续发送10条消息,监控帧率和白屏
- 杀进程重新进入,本地缓存正常恢复
- 长时间挂后台再回来,WebSocket能自动重连
我现在的习惯是:每改一次网络层就带着这份清单跑一遍再发版。小程序发版要走审核,问题在工具里发现比在用户手机上发现便宜得多。希望帮到你。
本文还有配套的精品资源,点击获取