简介:一套面向微信小程序网络长连接场景的完整源码包,适合需要在小程序中实现实时通信、设备控制或消息推送的开发者,也适合对网络协议与小程序底层能力感兴趣的中级前端学习者。源码采用 Go 语言编写服务端,小程序端目录涵盖客户端页面逻辑、页面结构、样式与配置等文件,能从服务端监听与连接处理、小程序端接口调用,到页面展示和交互,形成完整可运行的闭环示例。压缩包共三十五个文件,整体约三十九 KB,核心通信逻辑集中在 Go 文件中,小程序部分以 JS 逻辑脚本为主,其余 WXML、WXSS、JSON、HTML、Markdown 等文件分别承担页面结构、样式、配置、说明文档与开源许可,目录清晰便于对照学习。目前已有两千零一十七人浏览学习,可借助项目说明与开源许可快速上手,围绕长连接的建立、数据收发、心跳保活与异常重连等关键点进行扩展,搭建稳定可靠的长连接通信方案并缩短前期调研时间。
1. 微信小程序 TCP/IP 长连接是什么:先搞清楚它能解决什么问题
做了几年小程序,你会发现一个尴尬:wx.request只能发一次请求等一次响应,服务端想主动推消息给你,要么靠轮询,要么靠 WebSocket 长连接。而标题里说的“TCP/IP 长连接”,放到微信小程序这个容器里,落地形态几乎都是 WebSocket(wss://),因为小程序并没有向开发者开放裸的net.Socket——你能用的,是wx.connectSocket这条建立在 TCP/IP 之上的、带帧格式和子协议协商的通道。所以这事的本质是:在小程序里用 WebSocket 协议维持一条长连接,让服务端能实时把数据推给用户。
这篇文章写给两类人:一类是刚接触小程序实时通信,被“TCP/IP 长连接”这个词唬住、不知道从哪下手的;另一类是已经用轮询顶着、想换成真长连接却又担心稳定性的人。我尽量把从建连、心跳、断线重连到真机验证的一整套源码逻辑拆开讲,你照着抄,能把一条能上线的长连接跑起来——而不是停留在“能连上、过十分钟就死”的演示层面。
2. 先把技术边界划清楚:小程序里的“TCP 长连接”和你想的不一样
2.1 小程序留给你的几条通道,哪条是真长连接
在小程序里,和网络相关的 API 基本就这几类:wx.request、wx.uploadFile、wx.downloadFile、wx.connectSocket(以及配套的SocketTask)。前三个都是“短连接”:请求发出、响应回来、连接就释放了。只有wx.connectSocket建立后能持续保持连接,服务端可以主动往客户端推数据,这就是我们说的“长连接”。
这里有一个关键细节:wx.connectSocket走的是 WebSocket 协议,WebSocket 握手之后,数据在传输层走的仍然是 TCP。换句话说,小程序不给你直接操作 TCP socket 的接口,但 WebSocket 本质上就是“跑在 TCP 之上的带帧协议”。你要的“TCP/IP 长连接”,在小程序里最现实的路径就是 WebSocket over TLS,也就是wss://域名连接。
我见过不少人一开始想找类似wx.createTCPSocket的接口直连服务器端口,结果翻遍文档也没找到——方向错了。如果你确实需要的是原始 TCP 二进制流、自定义私有协议,那小程序这条路走不通;但你需要的只是“服务端能随时推数据、客户端能持续收”,那 WebSocket 就是标准答案,没有第二个可比选项。
2.2 为什么不是轮询、不是普通 HTTP 请求
早期很多实时场景都用轮询:客户端每隔几秒发一次wx.request,问服务端“有没有新数据”。这种做法在小程序里有两个硬伤:一是每次请求都要走一遍完整的 HTTP 建连,开销大、费电、费流量;二是服务端数据刷新和新用户请求之间有时间差,你永远拿到的不是“实时”数据。
WebSocket 长连接的核心价值是“服务端主动推送”:比如扫码支付结果、订单状态变更、聊天消息、工单进度,服务端一有结果就直接通过连接推给客户端,客户端不用反复去问。这里我用一个仓储物流的看板场景举例:仓库操作员的小程序看板要实时显示分拣线上每个工位的任务完成数,如果 5 秒轮询一次,操作员看到的数据最多滞后 5 秒,而且几十个操作员同时轮询,服务端压力也大。改成 WebSocket 长连接之后,服务端每次任务完成、计数变更,直接推送增量数据,客户端界面立刻刷新,这就是“长连接”在小程序里最典型、也最值得做的场景。
2.3 最小链路:本地起一个 WebSocket 服务端,小程序端连上去
我一般先用 Node.js 起一个最简服务端,验证链路通了再去写业务逻辑。服务端用ws库,这是目前 Node 生态里最常见的 WebSocket 实现,安装方式如下:
npm init -y npm install ws服务端代码非常短,核心就四件事:创建 WebSocket server、监听客户端连接、收到消息后回一条、把新连接数打出来。
// server.js const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws) => { console.log('客户端已连接,当前连接数:', wss.clients.size); // 收到客户端消息,原样回传,方便验证链路 ws.on('message', (data) => { const msg = data.toString(); console.log('收到消息:', msg); ws.send(`服务端已收到:${msg}`); }); ws.on('close', () => { console.log('客户端断开,当前连接数:', wss.clients.size); }); ws.on('error', (err) => { console.error('连接错误:', err); }); }); console.log('WebSocket 服务端已启动:ws://localhost:8080');这里要注意,小程序真机上不认ws://localhost,也不能连 IP 直连(除非你在开发者工具里关掉域名校验)。这个服务端只用于本地调试链路,真机测试必须换成wss://域名 + 合法证书。
小程序端的连接代码同样精简:
// 小程序端,连接示例 const socket = wx.connectSocket({ url: 'ws://localhost:8080', // 本地调试用,真机必须替换 wss:// 域名 header: { 'Content-Type': 'application/json' }, success: () => { console.log('连接发起成功'); }, fail: (err) => { console.error('连接发起失败', err); } });wx.connectSocket返回的是一个SocketTask对象,后续的收发、关闭、错误监听都在这个 task 上挂。一个很容易踩的坑是:success回调只代表“连接请求发出去了”,不代表“连接建立成功”,真正建立成功要看socket.onOpen。
socket.onOpen(() => { console.log('连接真正建立成功'); // 在这里发心跳、发登录鉴权 socket.send({ data: JSON.stringify({ type: 'ping' }) }); }); socket.onMessage((res) => { const data = JSON.parse(res.data); console.log('收到服务端推送:', data); }); socket.onError((err) => { console.error('连接错误:', err); }); socket.onClose((res) => { console.log('连接关闭:', res.code, res.reason); });socket.send的data字段可以是字符串,也可以是 ArrayBuffer。如果你做的是二进制协议(比如音频流、图片分片传输),就要转成 ArrayBuffer;如果只是 JSON 文本消息,字符串直接发即可。可以抄作业的点:把上面这段onOpen / onMessage / onError / onClose四个回调看成连接的生命周期钩子,业务逻辑只在onMessage里做数据分发,不要在onOpen里塞太多东西。
3. 把长连接源码拆开:心跳、断线重连与消息格式,一个都不能少
3.1 心跳机制:连接为什么必须“养”,不养会怎样
WebSocket 连接建立之后,如果长时间没有数据流动,网络链路中的中间设备(比如运营商的 NAT 网关、机房防火墙)会认为这条连接已经“没人用”,主动把它回收掉。表现就是:小程序这边看着 socket 还在,服务端那边也没收到断开事件,但消息已经发不过去了——这就是所谓的“假死连接”。
解决办法是心跳。常见做法是:客户端每隔一段时间(比如 30 秒)发一个ping消息,服务端收到后回pong,客户端收到pong就认为连接活着。如果连续几次没收到pong,就判定连接已死,主动关闭并触发重连。
我一般这样组织心跳逻辑:
// 心跳管理器 class Heartbeat { constructor(socketTask, interval = 30000) { this.socketTask = socketTask; this.interval = interval; this.timer = null; this.failCount = 0; this.maxFail = 3; } start() { this.stop(); this.timer = setInterval(() => { this.socketTask.send({ data: JSON.stringify({ type: 'ping', ts: Date.now() }), fail: (err) => { console.error('心跳发送失败', err); this.failCount++; if (this.failCount >= this.maxFail) { console.error('心跳连续失败,判定连接假死'); this.socketTask.close({ code: 4001, reason: 'heartbeat timeout' }); } } }); }, this.interval); } resetFailCount() { this.failCount = 0; } stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } } }这段代码的逻辑是:failCount用来累计连续发送失败的次数,一旦超过maxFail(比如 3 次,也就是 90 秒没通),就主动close掉连接,触发onClose,让重连逻辑接手。这里有个参数值得你根据业务调整:心跳间隔太短会增加服务端压力,太长则“假死”的时间窗口太大,一般 20 到 60 秒之间,看你的网络环境和服务端负载。
配合心跳的是服务端回pong。服务端收到ping后要立刻回pong,这个逻辑必须在服务端做,不能只靠客户端单方面发。ws库的处理也很简单,在message监听里判断消息类型即可。
3.2 断线重连:指数退避 + 重连上限,别让小程序卡死
断线重连不是“断了就立刻重连”,那样在网络不稳定时会变成“疯狂重连”:连接刚建立就断开、断开又重连,消耗资源不说,还可能在服务端形成大量半开连接。我一般用指数退避策略:第一次重连等 1 秒,第二次等 2 秒,第三次等 4 秒,以此类推,最大间隔封顶(比如 30 秒)。同时设定最大重试次数,超过之后不再自动重连,转而提示用户“网络异常,请手动刷新”。
具体重连逻辑放在onClose里触发:
class ReconnectManager { constructor(connectFn) { this.connectFn = connectFn; // 传入建立连接的函数 this.retryCount = 0; this.maxRetry = 8; this.baseDelay = 1000; // 第一次重连等待 1 秒 this.maxDelay = 30000; this.stopFlag = false; } onClose() { if (this.stopFlag) return; if (this.retryCount >= this.maxRetry) { console.error('重连次数已达上限,停止自动重连'); return; } const delay = Math.min(this.baseDelay * Math.pow(2, this.retryCount), this.maxDelay); console.log(`第 ${this.retryCount + 1} 次重连,${delay / 1000} 秒后执行`); this.retryCount++; setTimeout(() => { if (this.stopFlag) return; this.connectFn(); // 重新执行 wx.connectSocket }, delay); } onOpen() { this.retryCount = 0; // 连接成功后重置计数 } stop() { this.stopFlag = true; } }这段代码有一个关键设计:connectFn不是现成的wx.connectSocket,而是你自己封装的一个函数,里面要重新创建 SocketTask、重新挂上onOpen / onMessage / onError / onClose回调,还要重新启动心跳。绝不能复用旧的socketTask——连接一旦关闭,这个 task 就废了,必须重新创建。
另外一个容易漏的细节:切后台/回前台要处理。小程序切到后台超过一定时间,系统可能会杀掉网络连接;从后台回前台时,要主动检查连接状态,如果发现不是CONNECTING或OPEN,就触发一次重连。监听方式是用wx.onAppShow / wx.onAppHide,或者在页面的onShow / onHide里处理。这个边界不处理,用户锁屏几分钟再回来,连接十有八九已经死了。
3.3 粘包与消息格式:长连接里的消息怎么分隔
TCP 是流式协议,WebSocket 虽然解决了“消息边界”的问题(每一帧都有长度字段),但小程序端的 SocketTask 在接收数据时,onMessage回调拿到的res.data可能并不是你服务端发送时的完整字符串。这事在小程序里尤其隐蔽:服务端一次ws.send了一个很大的 JSON,客户端onMessage可能被拆成多次回调触发(取决于底层实现和微信版本),也可能一次回调里塞进两次send的数据。
最稳妥的做法是:不要在消息体里裸传 JSON 字符串然后直接JSON.parse,而是定义一套自己的帧格式。我常用的结构是:
// 消息帧格式:前缀长度 + 消息体 // 4 字节大端序消息长度 + UTF-8 编码的 JSON 消息体服务端发送时这样封装:
// 服务端发送带长度前缀的消息 function sendJson(ws, obj) { const jsonStr = JSON.stringify(obj); const payload = Buffer.alloc(4 + jsonStr.length); payload.writeUInt32BE(jsonStr.length, 0); // 前 4 字节写长度 payload.write(jsonStr, 4); // 后面写消息体 ws.send(payload); }客户端接收时,onMessage拿到的res.data如果是二进制(真机上默认就是 ArrayBuffer),需要自己拼缓冲、解析长度、切消息:
// 客户端接收缓冲处理(简化版) let receiveBuffer = new Uint8Array(0); function handleBinaryData(data) { // 1. 把新数据追加到缓冲 const newBuffer = new Uint8Array(receiveBuffer.length + data.byteLength); newBuffer.set(receiveBuffer, 0); newBuffer.set(new Uint8Array(data), receiveBuffer.length); receiveBuffer = newBuffer; // 2. 循环解析完整消息 while (receiveBuffer.length >= 4) { const view = new DataView(receiveBuffer.buffer); const msgLength = view.getUint32(0); // 读前 4 字节长度 if (receiveBuffer.length < 4 + msgLength) { break; // 数据还不够一个完整消息,等待下个分包 } // 3. 切出一个完整消息 const msgBytes = receiveBuffer.slice(4, 4 + msgLength); const msgStr = decodeURIComponent(escape(String.fromCharCode(...msgBytes))); const msgObj = JSON.parse(msgStr); dispatchMessage(msgObj); // 分发业务消息 // 4. 剩余部分继续循环 receiveBuffer = receiveBuffer.slice(4 + msgLength); } }上面这段代码用了最简单的DataView读取长度,然后用字节切片分隔消息。decodeURIComponent(escape(...))是处理 UTF-8 中文字符的一个直白做法,虽然效率不高,但胜在代码短、逻辑清晰,适合先跑通再优化。
注意:如果你在开发者工具里先把res.data转成了字符串,就别再走二进制解析了。二进制的ArrayBuffer和字符串的解析路径完全不同,你必须在onMessage回调里先判断res.data的类型。真机上res.data往往是 ArrayBuffer,开发工具里可能是字符串,这个差异会让不少人在“本地好的、真机全乱码”的坑里爬半天。统一做法是:发送时固定用字符串(send({ data: JSON.stringify(obj) })),接收时也按字符串处理——这样虽然放弃了二进制效率,但换来的是跨环境行为一致。如果服务端是别人写的、发的是二进制帧,那就老老实实做接收缓冲解析,别两头混用。
4. 从能连到能上线:把长连接源码组织成一个可维护的小模块
4.1 源码别堆在页面里:拆成连接管理 + 消息分发 + 页面订阅
很多人一开始把wx.connectSocket写在某个页面的onLoad里,结果第二个页面也要用这条连接,就复制粘贴一份,改改 URL,结果两个页面各建一条连接,互相抢心跳、服务端怒收 N 份重复推送。正确做法是把长连接封装成一个独立的模块(socket-manager.js),全局只保留一条连接,页面按需订阅消息。
模块的核心结构是这样:
// socket-manager.js class SocketManager { constructor() { this.socketTask = null; this.isConnected = false; this.heartbeat = null; this.reconnect = null; this.listeners = {}; // 消息类型 -> 回调函数集合 this.pendingQueue = []; // 连接未就绪时的待发送消息 this.globalUrl = ''; // 由业务启动时初始化 } init(url) { this.globalUrl = url; this.connect(); } connect() { // 创建 SocketTask、挂回调、启动心跳、重置重连计数的完整流程 // 具体代码见前文,这里只给占位 } onMessage(type, handler) { if (!this.listeners[type]) { this.listeners[type] = []; } this.listeners[type].push(handler); } offMessage(type, handler) { // 移除订阅 } send(type, payload) { const msg = JSON.stringify({ type, data: payload, ts: Date.now() }); if (!this.isConnected) { this.pendingQueue.push(msg); // 未连接则排队 return; } this.socketTask.send({ data: msg }); } dispatchMessage(msgObj) { // 从 socket 拿到完整消息后,按 type 分发给订阅者 const handlers = this.listeners[msgObj.type]; if (handlers) { handlers.forEach(h => h(msgObj.data)); } } } module.exports = new SocketManager(); // 单例,全局复用这个模块把四个关键职责收敛到了一起:连接生命周期管理、心跳与重连、消息分发、待发消息队列。页面里不需要知道自己用的是不是长连接、连接断没断,只做“订阅自己关心的消息类型”这一件事。
页面里使用长连接的完整流程长这样:
// 页面中接入 const socketManager = require('../../utils/socket-manager'); Page({ onLoad() { socketManager.onMessage('order_update', this.onOrderUpdate); socketManager.send('auth', { token: wx.getStorageSync('token') }); }, onOrderUpdate(data) { this.setData({ orderStatus: data.status }); // 收到服务端推送后做 UI 更新 }, onUnload() { socketManager.offMessage('order_update', this.onOrderUpdate); } });一定要在onUnload里做offMessage,否则页面销毁后回调还挂在模块里,会造成内存泄漏和重复执行 UI 更新的问题。这里注意消息订阅和wx.request不一样:长连接是常驻的,页面切换不会断连,所以必须在页面销毁时主动“退订”,而不是“断开连接”。
4.2 上线前必须做的几件事:域名白名单、证书、并发数
小程序对长连接有三条硬性约束,漏一条线上就翻车。
第一,必须是wss://域名,不能是ws://或 IP 直连。在 MP 管理后台的“开发设置”→“服务器域名”里配置 socket 合法域名,而且必须 HTTPS 证书有效。发布体验版之前,用真机试一下在“不校验域名”关闭时能不能连上,这个测试能过滤掉 90% 的域名配置问题。
第二,并发连接数有限制。一个用户同时最多只能保持 5 条 WebSocket 连接(这是微信的硬限制),超过之后新连接会失败。所以全局单例连接是必须的,不能让页面随意建连。
第三,切后台连接会被挂起。小程序切到后台后,网络连接会被系统保护性挂起,不一定立刻断开,但定时器会停止,心跳发不出去,回来时面临的基本就是一次重连。所以onShow时检查连接状态、必要时重连,是上线前必须写好的代码。
我通常的做法是在App.js的onLaunch里初始化socketManager,在onShow里做一次“连接活着吗”的检查:
// app.js const socketManager = require('./utils/socket-manager'); App({ onLaunch() { // 这里的 wss 地址是你的正式环境域名 socketManager.init('wss://your-domain.com/ws'); }, onShow() { if (socketManager.isConnected) { return; } socketManager.connect(); // 连不上或已断开就重连 } });注意onLaunch里初始化时机可能较早,此时用户还没登录、token 可能还没拿到。所以send('auth', ...)这类鉴权消息不要放在onLaunch里直接发,而是等连接onOpen之后再发。pendingQueue机制就是为这个场景兜底的:页面在未连接时调send,消息进队列,等连接建立后再统一发出。
5. 微信小程序长连接避坑指南:四件翻车现场和它们的后悔药
5.1 开发者工具连接正常,真机上死活连不上
现象:开发者工具里ws://localhost:8080秒连、消息收发正常;一扫码预览,连接直接onError,连失败回调都没给详细原因。
原因:开发者工具默认开启了“不校验合法域名”,真机没有这个豁免。真机要求必须wss://+ 已备案域名 + 域名已加入后台 socket 白名单 + 证书链完整。任何一个环节不满足,连接都会静默失败。
解决:先在微信公众平台后台把 socket 合法域名配上(不是 request 合法域名,是 socket 合法域名,很多人配错位置);然后在真机上用 Safari 或微信直接访问https://你的域名,确认证书没有报错;最后把ws://改成wss://。如果你用的是阿里云、腾讯云的免费证书,注意要下载 Nginx 格式或全链路证书,别有中间证书缺失的问题。
5.2 连接没断,但消息就是发不出去——假死连接
现象:看服务端日志,连接还在;小程序端onClose没触发,但页面上的数据已经 10 分钟没更新了。你手动切一下网络(Wi-Fi 切 4G),数据突然全刷出来了。
原因:这就是前面说的 NAT 超时把连接“静默回收”了。中间设备扔掉了映射关系,两端的 TCP 还物理存在,但数据已经无法互通。没有心跳或心跳过期判断不够快,就会遇到这种假死。
解决:加上心跳机制,连续 N 次发ping没收到pong(或发送接口回调fail)就主动close,让重连管理器接管。我见过很多项目“加了心跳但还是死”,原因是只发心跳、不判断心跳结果——ping发出去了,链路早断了,发送动作本身当然也失败,但没人看失败回调。心跳必须做“发送 + 回应校验”两个动作,缺一个都等于白做。
5.3 订阅同一消息的多个页面,收到推送后全部一起刷新
现象:A 页面订阅了order_update,B 页面也订阅了order_update,A 页面退到后台(未被销毁),B 页面收到推送,A 页面也触发了setData,控制台报setData警告甚至性能下降。
原因:多个页面共存时,各页面onLoad里都注册了回调,onUnload里未必都注销干净;或者你的offMessage只传了type,没有把具体的handler传进去,导致退订时删错了回调。
解决:offMessage(type, handler)必须保存原handler引用,按引用删除,不能只按type清空;页面onHide时也建议做一次退订(如果需要后台不刷新的话),onShow再重新订阅。这里的经验是:回调注册和注销要成对出现,跟事件绑定一个道理,漏一个就是泄漏。
5.4 消息偶发丢失,排查半天发现是发送时机不对
现象:用户登录成功后立刻发auth消息,服务端偶尔能正常鉴权,偶尔报socket not authed。业务上表现为“登录后偶发收不到推送”。
原因:wx.connectSocket的success不等于连接可用,onOpen才算建立完成。有些开发者把send写在success里,连接还没真正 ready 就发包,包就丢了。
解决:统一把鉴权、订阅、待发消息的发送时机挂在onOpen回调之后。我刚才给的pendingQueue机制就是干这个的,页面任何时机调send都不丢,连接 ready 后统一 flush。这里值得多说一句:WebSocket 的onOpen在浏览器体系中是标准事件,小程序里 SocketTask 同样遵循,不要自己额外加什么定时器去猜“应该连好了”。
6. 进阶:让长连接从“能用”到“好用”的几个关键参数与验证手段
6.1 服务端推送频率和客户端渲染频率要分开
长连接最容易犯的另一个错是在onMessage回调里直接setData。服务端如果高频推送(比如行情刷新每秒 10 条),小程序的渲染层根本跟不上,会卡顿、掉帧、甚至白屏。常见做法是:数据到达后进行节流或合帧,把 1 秒内的多条推送合并成一次界面刷新。小程序端可以用wx.nextTick配合一个简单的节流函数来实现,或者更直白些——在dispatchMessage里做合并缓冲:
// 简易合帧 let frameBuffer = []; let frameTimer = null; function pushAndRender(data) { frameBuffer.push(data); if (frameTimer) return; frameTimer = setTimeout(() => { const merged = mergeFrame(frameBuffer); this.setData({ list: merged }); frameBuffer = []; frameTimer = null; }, 500); // 500ms 合一次帧 }这里 500 毫秒是凭经验取的折中,太短合帧效果不明显,太长界面会觉得“卡”。如果是实时状态类的数据(比如设备在线状态),可以放宽到 1 秒;如果是聊天消息,必须即时渲染,不能合帧,否则用户感知强烈。
6.2 验证长连接稳定性的三个手段
第一个手段是“断网拔线测试”:真机连上长连接后,打开飞行模式 5 分钟,再关闭飞行模式,观察小程序能不能自动重连、重连后能不能立刻收到新推送。这条能验证你的心跳过期判断和重连机制是否有效。
第二个手段是“服务端主动踢连接测试”:在服务端写一个临时的setTimeout,连接建立 60 秒后主动ws.terminate(),看看客户端onClose是否触发、重连是否按退避节奏执行。很多项目只做了“客户端断开→服务端感知”的方向,反向测试没做,结果服务端重启时没有主动发关闭帧,客户端傻傻等心跳超时,恢复时间长达几十秒。
第三个手段是“切后台回前台测试”:真机连接后按 Home 键退回桌面,等 10 分钟再进小程序。观察连接是否还活着,活着的话能立刻收到推送,死掉的话能否快速重连。我在实际项目里遇到的规律是:iOS 切后台超过 30 秒,连接基本就没救了,必须等wx.onShow触发重连,而且重连不能等心跳超时,要在onShow里做一次“主动探测”或者直接重建连接。
6.3 最后说一句血泪教训
我做小程序长连接最惨的一次翻车,是上线当天才发现所有推送都要走wss://,而域名证书用的是一台内网测试机的自签名证书——真机上一片onError,整条业务的实时推送直接断掉。那之后我给自己立了一个规矩:任何长连接功能,第一天就把线上的 wss 地址配好,哪怕只是空转的接口也要先配上,不要等最后一天再换域名。域名白名单、证书有效期、真机验证这三件事,越早做越省事。希望这些经验能帮你少走几段弯路。
本文还有配套的精品资源,点击获取