小程序离线包与弱网数据同步实战指南 2026
目录
小程序离线包与弱网数据同步实战指南 2026#
弱网、断网、地铁通勤、电梯间、偏远地区——用户不会因为网络差就放弃使用你的小程序,但他们会因为「白屏 + 转圈 + 数据丢失」而卸载。本文把小程序离线能力拆成三层:离线包(代码/资源)、数据缓存(读)、弱网同步(写),给出可落地的选型决策树与完整代码。
一、先厘清边界:小程序里「离线」能做什么、不能做什么#
原生 App 的「离线包」通常指把页面资源打包下发、拦截请求本地命中。小程序受平台约束,能力边界不同,开工前必须认清四条硬限制:
| 能力 | 微信小程序 | 说明 |
|---|---|---|
| 代码包离线 | 支持(限包体) | 主包 + 分包合计上限随基础库演进,2026 年主流为 2MB 主包 + 20MB 总包(微信);分包预下载可提前拉取 |
| 静态资源离线 | 支持(Storage 缓存) | 图片/JSON 等写入本地 Storage 后离线可读,有 10MB/单 key 限制(微信 wx.setStorageSync 单 key 上限 1MB,总上限 10MB) |
| 动态接口离线 | 不自动支持 | wx.request 断网直接失败,需要自行做缓存兜底 + 队列重放 |
| 服务端推送 | 不直接支持 | 需借助订阅消息 + 云开发数据库 watch 等组合方案 |
结论先行:小程序离线化的核心不是「让所有请求都离线」,而是「读路径走缓存、写路径走队列」,让弱网/断网时界面仍有数据可展示、用户操作不丢失。
二、六大方案对比#
| 方案 | 适用场景 | 实现成本 | 首屏收益 | 风险点 |
|---|---|---|---|---|
| 1. 本地缓存兜底(Storage) | 列表/详情页读多写少 | 低 | 中 | 缓存过期导致脏数据 |
| 2. 网络优先 + 缓存兜底 | 通用请求层 | 低 | 中 | 弱网时响应慢,需超时控制 |
| 3. 离线队列 + 重连重放 | 表单提交/点赞/埋点等写操作 | 中 | 无 | 幂等与顺序问题 |
| 4. 乐观更新 + 冲突解决 | 编辑/投票等高频写 | 中 | 无 | 并发冲突、回滚复杂 |
| 5. 分包预下载(preloadRule) | 关键二级页预取 | 低 | 高 | 消耗用户流量,需克制 |
| 6. 云开发离线能力 | 已用微信云开发的项目 | 中 | 中 | 绑定云开发,跨平台迁移难 |
不要平均使用六种方案。大多数小程序只需要「方案 2 + 方案 3」就能覆盖 80% 的弱网场景,再按页面重要性叠加「方案 5」。
三、选型决策树#
你的页面是读多还是写多?
├─ 读多(列表/详情/配置)
│ └─ 是否允许展示「可能过期」的数据?
│ ├─ 允许 → 方案 2(网络优先 + 缓存兜底)
│ └─ 不允许(强一致,如余额/订单状态)→ 不做缓存,只做超时 + 重试 + 明确错误态
├─ 写多(表单/评论/点赞/埋点)
│ └─ 写操作能否幂等重放?
│ ├─ 能(点赞/收藏/埋点)→ 方案 3(离线队列 + 重连重放)
│ └─ 不能(下单/转账)→ 不做离线,断网即阻断并提示,避免脏提交
└─ 关键二级页(支付页/详情页)→ 叠加方案 5(分包预下载)
四、完整代码:离线优先请求层(原生微信小程序)#
下面是一套可直接复制的请求层,实现「网络优先 + 缓存兜底 + 超时控制 + 离线队列」。核心思路:读请求先走缓存秒开,再发网络请求刷新并回填缓存;写请求失败时入队,网络恢复后按序重放。
// utils/offline-request.js
const CACHE_PREFIX = "offline_cache:";
const QUEUE_KEY = "offline_queue";
const CACHE_TTL = 5 * 60 * 1000; // 读缓存默认 5 分钟
function readCache(key) {
try {
const raw = wx.getStorageSync(CACHE_PREFIX + key);
if (!raw) return null;
const item = JSON.parse(raw);
if (Date.now() - item.ts > item.ttl) return null;
return item.data;
} catch (e) {
return null;
}
}
function writeCache(key, data, ttl = CACHE_TTL) {
try {
wx.setStorageSync(CACHE_PREFIX + key, JSON.stringify({ data, ts: Date.now(), ttl }));
} catch (e) {
// 超出 10MB 上限时清理最旧缓存后重试一次
clearOldestCache();
try {
wx.setStorageSync(CACHE_PREFIX + key, JSON.stringify({ data, ts: Date.now(), ttl }));
} catch (e2) { /* 放弃写缓存,不影响主流程 */ }
}
}
// 读请求:缓存秒开 + 网络刷新
function requestWithCache({ url, method = "GET", data = {}, cacheKey, ttl = CACHE_TTL, timeout = 5000 }) {
return new Promise((resolve, reject) => {
const cached = cacheKey ? readCache(cacheKey) : null;
let settled = false;
const done = (fn, value) => {
if (!settled) { settled = true; fn(value); }
};
if (cached) resolve(cached); // 秒开,后续网络结果通过 onFresh 回调刷新
wx.request({
url,
method,
data,
timeout,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
if (cacheKey) writeCache(cacheKey, res.data, ttl);
done(resolve, res.data);
} else {
done(cached ? () => {} : reject, new Error(`HTTP ${res.statusCode}`));
}
},
fail(err) {
if (cached) {
done(resolve, cached); // 断网兜底
} else {
done(reject, err);
}
},
});
});
}
// 写请求:失败入队,网络恢复后重放(要求幂等)
function enqueueWrite(mutation) {
const queue = wx.getStorageSync(QUEUE_KEY) || [];
queue.push({ ...mutation, id: `${Date.now()}_${Math.random().toString(36).slice(2)}` });
wx.setStorageSync(QUEUE_KEY, queue);
}
function flushQueue() {
const queue = wx.getStorageSync(QUEUE_KEY) || [];
if (queue.length === 0) return;
const task = queue[0];
wx.request({
url: task.url,
method: task.method || "POST",
data: task.data,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
wx.setStorageSync(QUEUE_KEY, queue.slice(1)); // 成功移出队首
flushQueue();
}
},
fail() { /* 网络仍不可用,保留队列,下次再试 */ },
});
}
module.exports = { requestWithCache, enqueueWrite, flushQueue };
五、乐观更新与冲突解决#
对于「点赞数、收藏状态」这类高频写,等网络返回再改 UI 会显得卡顿。做法是先本地改 UI(乐观更新),再异步提交;失败回滚。多人编辑类场景需要冲突策略:
// 乐观更新示例(点赞)
function toggleLike(itemId, liked) {
const prev = liked;
updateUI(itemId, !prev); // 先改 UI
wx.request({
url: `https://api.example.com/like/${itemId}`,
method: "POST",
data: { liked: !prev },
success() {},
fail() {
updateUI(itemId, prev); // 失败回滚
wx.showToast({ title: "网络异常,请重试", icon: "none" });
},
});
}
冲突解决三档,按复杂度递增选择:
- Last-Write-Wins(LWW):每个写请求带客户端时间戳或递增版本号,服务端保留最新。适合单用户、低冲突场景,实现最简单。
- 版本号 CAS:读取时带
version,提交时校验version匹配才写入,不匹配返回 409 让客户端重新拉取合并。适合文档编辑。 - 操作日志(OT/CRDT):把写操作建模成可交换的操作,服务端合并。适合多人实时协作,复杂度最高,一般小程序用不到。
六、弱网监听与重连重放#
微信提供 wx.onNetworkStatusChange 监听网络状态,结合 wx.getNetworkType 主动探测。重连后触发队列重放:
// app.js 启动时注册
wx.onNetworkStatusChange((res) => {
if (res.isConnected) {
flushQueue(); // 网络恢复,重放离线写队列
}
});
// 弱网(非 wifi/4g/5g 的 2g/3g)时降低图片质量或停用预加载
wx.getNetworkType({
success(res) {
if (res.networkType === "2g" || res.networkType === "3g") {
globalData.weakNetwork = true;
}
},
});
超时与重试:wx.request 的 timeout 默认 60s,弱网场景建议按请求类型差异化——读请求 35s 超时并缓存兜底,写请求 810s 并只重试一次(写重试必须保证服务端幂等,否则可能重复下单/重复支付)。
七、分包预下载(preloadRule)#
「分包预下载」是微信官方提供的最直接的「代码离线预取」能力:进入某页时自动后台下载指定分包,用户跳转时零等待。在 app.json 中配置:
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
}
}
}
network 支持 all(不限网络)与 wifi(仅 wifi 预下载,省流量)。建议关键转化路径(首页 → 详情/支付分包)用 wifi,其余用 all 并克制数量——预下载同样消耗用户流量,过多的 packages 会拖慢首页本身。
八、三平台 API 差异#
| 能力 | 微信 | 支付宝 | 抖音 |
|---|---|---|---|
| 本地读写 | wx.setStorageSync / wx.getStorageSync | my.setStorageSync / my.getStorageSync | tt.setStorageSync / tt.getStorageSync |
| 网络请求 | wx.request | my.request | tt.request |
| 网络状态监听 | wx.onNetworkStatusChange | my.onNetworkStatusChange | tt.onNetworkStatusChange |
| 获取网络类型 | wx.getNetworkType | my.getNetworkType | tt.getNetworkType |
| 分包预下载 | app.json preloadRule | 支持分包,预下载能力需查最新文档 | 支持分包,预下载能力需查最新文档 |
保守原则:上表仅列三家官方文档明确公开的能力;支付宝/抖音的分包预下载能力随版本演进,接入前以对应开放平台最新文档为准,不要照搬微信的
preloadRule字段名。
九、10 个高频踩坑#
- Storage 同步 API 卡主线程:
wx.setStorageSync是同步阻塞调用,大对象/高频写入会掉帧。大对象改异步wx.setStorage,或控制写入频率。 - 单 key 超 1MB 静默失败:微信单 key 上限约 1MB,写入超限会失败或截断,务必 try/catch 并分片。
- 缓存过期后仍展示旧数据:缓存必须带 TTL,且关键数据(余额/订单状态)不要缓存。
- 写队列无幂等导致重复下单:重放前给每个写请求加幂等键(
clientId),服务端去重。 - 乐观更新失败不回滚:UI 改了但网络失败,必须回滚并提示,否则状态与服务器不一致。
onNetworkStatusChange重复注册:全局只注册一次,页面级注册要在onUnload里wx.offNetworkStatusChange解绑,否则回调叠加。- 预下载滥用流量:
network: "all"的预下载会消耗非 wifi 流量,可能招致用户投诉,关键路径才配。 - 弱网下图片不降级:2g/3g 下仍加载原图导致白屏,应按网络类型切换缩略图/占位图。
- 断网错误态不友好:只弹「网络异常」不给重试入口,用户只能退出。错误态要带「重试」按钮并保留已输入内容。
- 缓存 key 冲突:不同接口共用同名 key 互相覆盖,key 要按「业务域 + 资源 id」命名。
十、15 项上线检查清单#
- 读请求有缓存兜底,且缓存带 TTL 与过期清理
- 强一致数据(余额/订单状态)明确不做缓存
- 写请求有超时与重试策略,重试请求服务端幂等
- 离线写队列有幂等键,重放按 FIFO 且单任务串行
-
onNetworkStatusChange只注册一次,页面解绑干净 - 断网错误态带「重试」按钮,保留用户已输入内容
- 大对象写入使用异步 Storage 或分片,避开单 key 上限
- 弱网(2g/3g)自动降级图片质量或停用预加载
- 关键路径配置
preloadRule,且network选择合理 - 乐观更新场景失败有回滚逻辑
- 缓存 key 按业务域命名,无冲突
- 三平台 API 差异已按微信/支付宝/抖音分别适配或封装
- 首屏只读缓存秒开,网络刷新回填不影响交互
- 离线队列有上限(如 200 条),防止 Storage 被打满
- 弱网重试的写操作有日志/埋点,便于排查重复提交
十一、缓存失效策略#
「缓存兜底」最大的敌人不是断网,而是缓存过期后还展示旧数据。三种失效策略按场景选择:
- TTL 绝对过期:写入时记录时间戳,读取时超龄即视为失效。实现最简单,但无法感知「服务端数据已变」。适合配置、字典等低频变更数据。
- 版本号失效:服务端下发
version字段,客户端缓存连同version一起存;请求带version或响应比对,不一致则全量刷新。适合列表结构会变的数据。 - 主动失效:写操作成功后,主动删除/更新相关读缓存(如点赞后更新详情缓存)。适合读写关联紧密的页面,能保证写后读一致。
// 版本号失效示例:响应带 version,客户端只在版本变化时回填缓存
function requestVersioned({ url, cacheKey }) {
const cached = readCache(cacheKey);
return new Promise((resolve) => {
wx.request({
url,
success(res) {
const fresh = res.data;
const staleVersion = cached ? cached.version : undefined;
if (fresh.version !== staleVersion) {
writeCache(cacheKey, fresh, CACHE_TTL);
resolve(fresh);
} else {
resolve(cached); // 版本未变,用本地缓存,省一次渲染
}
},
fail() {
resolve(cached || null); // 断网兜底
},
});
});
}
十二、跨端封装(Taro/uni-app)#
Taro/uni-app 用同构代码跨三端,但三端 Storage 与网络 API 前缀不同。封装一层适配器,让业务代码不感知平台:
// utils/storage-adapter.js(uni-app 示例,Taro 同理换 Taro.setStorageSync)
function getStorage(key) {
// #ifdef MP-WEIXIN
return wx.getStorageSync(key);
// #endif
// #ifdef MP-ALIPAY
return my.getStorageSync(key);
// #endif
// #ifdef MP-TOUTIAO
return tt.getStorageSync(key);
// #endif
}
function setStorage(key, value) {
// #ifdef MP-WEIXIN
return wx.setStorageSync(key, value);
// #endif
// #ifdef MP-ALIPAY
return my.setStorageSync(key, value);
// #endif
// #ifdef MP-TOUTIAO
return tt.setStorageSync(key, value);
// #endif
}
条件编译注释(
// #ifdef MP-WEIXIN)是 uni-app 的预处理指令,打包时只保留目标平台分支,不会把三端 API 混进同一产物。Taro 用process.env.TARO_ENV判断即可。
十三、官方来源核对#
- 微信小程序:
wx.setStorageSync/wx.request/wx.onNetworkStatusChange/ 分包预下载preloadRule,见微信官方文档「存储」「网络」「分包加载」章节。 - 支付宝小程序:
my.setStorageSync/my.request/my.onNetworkStatusChange,见支付宝开放平台小程序文档。 - 抖音小程序:
tt.setStorageSync/tt.request/tt.onNetworkStatusChange,见抖音开放平台小程序文档。 - 分包预下载与包体上限会随基础库版本调整,以各平台最新公告为准。