小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案
小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案
文章目录
- 小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案
- 一、方案概述
- 二、技术架构
- 三、核心功能实现
- 1. 项目目录结构
- 2. 关键工具类实现
- 3. 页面实现
- 4. 全局配置
- 四、关键功能说明
- 一、离线数据存储:持久化留存,关闭不丢失
- 二、网络监听机制:智能感知,自动同步
- 三、任务管理流程:状态清晰,操作灵活
- 四、资源清理策略:自动+手动,高效释放空间
- 五、使用说明
- 1. 表单页面:离线可填,数据自动留存
- 2. 任务管理页面:状态可视化,操作自主可控
- 3. 网络恢复同步:自动触发,无需干预
- 4. 任务成功后处理:自动清理,释放空间
- 六、扩展建议
- 1. 任务优先级设置:灵活排序,重点先行
- 2. 大文件分片上传:断点续传,高效稳定
- 3. 表单模板功能:复用模板,快速填报
- 4. 数据同步进度条:可视化反馈,清晰可控
- 5. 任务过期清理机制:自动减负,避免冗余
- 6. 数据备份与恢复功能:双重保障,防丢防盗

一、方案概述
本方案实现一个支持离线操作的表单与拍照上传系统,核心功能包括:
-
弱网/无网环境下的表单数据本地缓存
-
拍照文件本地存储与上传队列管理
-
网络状态实时监听与自动重试机制(最多3次)
-
任务状态可视化管理
-
成功任务自动清理本地缓存
二、技术架构
-
框架:微信小程序原生框架
-
本地存储:
wx.setStorageSync(表单数据) + 本地文件系统(图片) -
网络监听:
wx.onNetworkStatusChange -
状态管理:全局变量+本地存储结合
-
上传机制:Promise封装+队列管理
三、核心功能实现
1. 项目目录结构
├── app.js // 入口文件、网络监听
├── app.json // 全局配置
├── app.wxss // 全局样式
├── pages/
│ ├── form/ // 表单页面
│ │ ├── form.js
│ │ ├── form.json
│ │ ├── form.wxml
│ │ └── form.wxss
│ └── task/ // 任务管理页面
│ ├── task.js
│ ├── task.json
│ ├── task.wxml
│ └── task.wxss
└── utils/├── storage.js // 本地存储工具├── upload.js // 上传管理工具└── network.js // 网络工具
2. 关键工具类实现
utils/storage.js - 本地存储工具
// 存储键名常量
const STORAGE_KEYS = {FORM_TASKS: 'offline_form_tasks', // 表单任务列表UPLOAD_QUEUE: 'upload_queue' // 上传队列
}// 获取所有任务
function getTasks() {const tasks = wx.getStorageSync(STORAGE_KEYS.FORM_TASKS)return tasks ? JSON.parse(tasks) : []
}// 保存任务
function saveTask(task) {const tasks = getTasks()// 生成唯一IDtask.id = Date.now() + Math.floor(Math.random() * 1000)task.status = 'pending' // pending, uploading, success, failedtask.retryCount = 0task.createTime = new Date().toISOString()tasks.unshift(task)wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))return task
}// 更新任务状态
function updateTaskStatus(taskId, status, data = {}) {const tasks = getTasks()const index = tasks.findIndex(t => t.id === taskId)if (index !== -1) {tasks[index].status = statustasks[index].retryCount = data.retryCount !== undefined ? data.retryCount : tasks[index].retryCounttasks[index].completeTime = status === 'success' ? new Date().toISOString() : tasks[index].completeTimewx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))return tasks[index]}return null
}// 删除任务
function deleteTask(taskId) {let tasks = getTasks()tasks = tasks.filter(t => t.id !== taskId)wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))// 如果有图片,删除本地缓存const task = tasks.find(t => t.id === taskId)if (task?.images?.length) {task.images.forEach(imgPath => {wx.removeSavedFile({ filePath: imgPath })})}
}// 清空成功任务
function clearSuccessTasks() {let tasks = getTasks()const successTasks = tasks.filter(t => t.status === 'success')// 删除成功任务的图片successTasks.forEach(task => {if (task.images?.length) {task.images.forEach(imgPath => {wx.removeSavedFile({ filePath: imgPath })})}})tasks = tasks.filter(t => t.status !== 'success')wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))
}module.exports = {getTasks,saveTask,updateTaskStatus,deleteTask,clearSuccessTasks,STORAGE_KEYS
}
utils/network.js - 网络工具
// 检查网络状态
function checkNetwork() {return new Promise((resolve) => {wx.getNetworkType({success(res) {const networkType = res.networkTyperesolve(networkType !== 'none')},fail() {resolve(false)}})})
}// 监听网络状态变化
function watchNetworkChange(callback) {wx.onNetworkStatusChange(res => {callback(res.isConnected)})
}module.exports = {checkNetwork,watchNetworkChange
}
utils/upload.js - 上传管理工具
const { getTasks, updateTaskStatus, deleteTask } = require('./storage')
const { checkNetwork } = require('./network')// 模拟上传接口(实际项目替换为真实接口)
function uploadFormData(formData, images) {return new Promise((resolve, reject) => {// 模拟网络请求setTimeout(() => {// 随机成功失败,模拟网络问题if (Math.random() > 0.3) {resolve({ success: true, data: { id: Date.now() } })} else {reject(new Error('上传失败'))}}, 1500)})
}// 上传单张图片
function uploadImage(tempFilePath) {return new Promise((resolve, reject) => {// 这里使用微信的上传APIwx.uploadFile({url: 'https://your-server.com/upload/image', // 替换为真实接口filePath: tempFilePath,name: 'image',success(res) {if (res.statusCode === 200) {const data = JSON.parse(res.data)resolve(data.url) // 返回图片URL} else {reject(new Error('图片上传失败'))}},fail(err) {reject(err)}})})
}// 处理单个任务上传
async function processTask(task) {const isConnected = await checkNetwork()if (!isConnected) {return { success: false, reason: '无网络' }}try {// 更新任务状态为上传中updateTaskStatus(task.id, 'uploading')// 上传图片const imageUrls = []if (task.images && task.images.length) {for (const imgPath of task.images) {const url = await uploadImage(imgPath)imageUrls.push(url)}}// 上传表单数据const formData = {...task.formData,images: imageUrls}const result = await uploadFormData(formData, imageUrls)if (result.success) {// 上传成功,更新状态updateTaskStatus(task.id, 'success')return { success: true }} else {throw new Error('表单上传失败')}} catch (error) {// 处理失败const retryCount = task.retryCount + 1const status = retryCount >= 3 ? 'failed' : 'pending'updateTaskStatus(task.id, status, { retryCount })return { success: false, reason: error.message,retryCount}}
}// 处理所有待上传任务
async function processAllTasks() {const tasks = getTasks()const pendingTasks = tasks.filter(t => t.status === 'pending' && t.retryCount < 3)// 依次处理任务,避免并发过高for (const task of pendingTasks) {await processTask(task)}
}module.exports = {processTask,processAllTasks,uploadImage
}
3. 页面实现
pages/form/form.js - 表单页面
const { saveTask } = require('../../utils/storage')
const { checkNetwork } = require('../../utils/network')
const { processTask } = require('../../utils/upload')Page({data: {formData: {name: '',description: '',date: ''},images: [],isSubmitting: false,networkStatus: true},onLoad() {// 初始化检查网络状态checkNetwork().then(isConnected => {this.setData({ networkStatus: isConnected })})// 监听网络变化wx.onNetworkStatusChange(res => {this.setData({ networkStatus: res.isConnected })})},// 表单输入处理handleInput(e) {const { field } = e.currentTarget.datasetthis.setData({[`formData.${field}`]: e.detail.value})},// 拍照takePhoto() {const that = thiswx.chooseImage({count: 3 - that.data.images.length, // 最多3张sizeType: ['original', 'compressed'],sourceType: ['camera'],success(res) {// 临时路径const tempFilePaths = res.tempFilePaths// 保存到本地,确保离线可用tempFilePaths.forEach(tempPath => {wx.saveFile({tempFilePath: tempPath,success(savedRes) {const savedPath = savedRes.savedFilePaththat.setData({images: [...that.data.images, savedPath]})}})})}})},// 预览图片previewImage(e) {const { index } = e.currentTarget.datasetwx.previewImage({current: this.data.images[index],urls: this.data.images})},// 删除图片deleteImage(e) {const { index } = e.currentTarget.datasetconst newImages = [...this.data.images]newImages.splice(index, 1)this.setData({ images: newImages })},// 提交表单async submitForm() {const { name, date } = this.data.formDataif (!name || !date) {wx.showToast({title: '请填写必填项',icon: 'none'})return}this.setData({ isSubmitting: true })// 保存任务到本地const task = {formData: this.data.formData,images: this.data.images}const savedTask = saveTask(task)// 检查网络状态,有网则尝试立即上传const isConnected = await checkNetwork()if (isConnected) {wx.showLoading({ title: '提交中...' })try {const result = await processTask(savedTask)wx.hideLoading()if (result.success) {wx.showToast({ title: '提交成功' })} else {wx.showToast({ title: '提交失败,将稍后重试', icon: 'none' })}} catch (error) {wx.hideLoading()wx.showToast({ title: '提交失败', icon: 'none' })}} else {wx.showToast({ title: '已保存离线数据', icon: 'none' })}// 重置表单this.setData({formData: { name: '', description: '', date: '' },images: [],isSubmitting: false})}
})
pages/form/form.wxml - 表单页面模板
<view class="container"><!-- 网络状态提示 --><view wx:if="{{!networkStatus}}" class="network-tip">当前无网络,数据将保存到本地</view><form class="form-container"><view class="form-item"><text class="label">姓名 *</text><input type="text" placeholder="请输入姓名" data-field="name"bindinput="handleInput"value="{{formData.name}}"/></view><view class="form-item"><text class="label">日期 *</text><picker mode="date" start="2020-01-01" end="{{today}}"data-field="date"value="{{formData.date}}"bindchange="handleInput"><view class="picker-view">{{formData.date || '请选择日期'}}</view></picker></view><view class="form-item"><text class="label">描述</text><textarea placeholder="请输入描述信息" data-field="description"bindinput="handleInput"value="{{formData.description}}"rows="3"/></view><!-- 图片上传区域 --><view class="form-item"><text class="label">照片</text><view class="image-upload"><view class="add-image" bindtap="takePhoto"wx:if="{{images.length < 3}}"><text>+</text></view><view class="image-item" wx:for="{{images}}" wx:key="index"><image src="{{item}}" mode="cover"bindtap="previewImage"data-index="{{index}}"/><view class="delete-btn" bindtap="deleteImage"data-index="{{index}}">×</view></view></view></view><button class="submit-btn" bindtap="submitForm"disabled="{{isSubmitting}}">{{isSubmitting ? '提交中...' : '提交'}}</button></form><!-- 跳转到任务管理 --><navigator url="/pages/task/task" class="task-link">查看上传任务 →</navigator>
</view>
pages/task/task.js - 任务管理页面
const { getTasks, updateTaskStatus, deleteTask,clearSuccessTasks
} = require('../../utils/storage')
const { processTask, processAllTasks } = require('../../utils/upload')
const { checkNetwork } = require('../../utils/network')Page({data: {tasks: [],networkStatus: true},onLoad() {this.loadTasks()// 检查网络状态checkNetwork().then(isConnected => {this.setData({ networkStatus: isConnected })// 有网则尝试上传所有任务if (isConnected) {this.processAllTasks()}})// 监听网络变化wx.onNetworkStatusChange(res => {this.setData({ networkStatus: res.isConnected })if (res.isConnected) {this.processAllTasks()}})},onShow() {this.loadTasks()},// 加载任务列表loadTasks() {const tasks = getTasks()this.setData({ tasks })},// 处理所有任务async processAllTasks() {wx.showLoading({ title: '同步中...' })try {await processAllTasks()this.loadTasks()} catch (error) {console.error('处理任务失败', error)} finally {wx.hideLoading()}},// 重试单个任务async retryTask(e) {const { id } = e.currentTarget.datasetconst tasks = getTasks()const task = tasks.find(t => t.id === id)if (!task) return// 重置重试次数updateTaskStatus(id, 'pending', { retryCount: 0 })wx.showLoading({ title: '重试中...' })try {await processTask(task)this.loadTasks()} catch (error) {wx.showToast({ title: '重试失败', icon: 'none' })} finally {wx.hideLoading()}},// 删除任务deleteTask(e) {const { id } = e.currentTarget.datasetwx.showModal({title: '确认删除',content: '确定要删除此任务吗?',success: (res) => {if (res.confirm) {deleteTask(id)this.loadTasks()}}})},// 清空成功任务clearSuccessTasks() {wx.showModal({title: '确认清空',content: '确定要清空所有成功的任务吗?',success: (res) => {if (res.confirm) {clearSuccessTasks()this.loadTasks()wx.showToast({ title: '已清空' })}}})},// 预览图片previewImage(e) {const { images, index } = e.currentTarget.datasetwx.previewImage({current: images[index],urls: images})}
})
pages/task/task.wxml - 任务管理页面模板
<view class="container"><!-- 网络状态提示 --><view wx:if="{{!networkStatus}}" class="network-tip">当前无网络,无法同步数据</view><!-- 操作栏 --><view class="operation-bar"><button class="sync-btn" bindtap="processAllTasks"wx:if="{{networkStatus}}">同步所有任务</button><button class="clear-btn" bindtap="clearSuccessTasks">清空成功任务</button></view><!-- 任务列表 --><view class="task-list"><view wx:if="{{tasks.length === 0}}" class="empty-tip">暂无任务</view><view class="task-item" wx:for="{{tasks}}" wx:key="id"><view class="task-header"><text class="task-name">{{item.formData.name}}</text><text class="task-status {{item.status}}">{{item.status === 'pending' ? '等待上传' : item.status === 'uploading' ? '上传中' : item.status === 'success' ? '已完成' : '上传失败'}}</text></view><view class="task-info"><view class="info-item">日期: {{item.formData.date}}</view><view class="info-item">照片: {{item.images.length}}张<text class="preview-link" bindtap="previewImage"data-images="{{item.images}}"data-index="0"wx:if="{{item.images.length > 0}}">预览</text></view><view class="info-item">创建时间: {{item.createTime.slice(0, 16)}}</view><view class="info-item" wx:if="{{item.status === 'failed'}}">失败原因: 上传失败,已重试{{item.retryCount}}次</view></view><view class="task-actions"><button class="retry-btn" bindtap="retryTask"data-id="{{item.id}}"wx:if="{{item.status === 'failed' || (item.status === 'pending' && networkStatus)}}">重试</button><button class="delete-btn" bindtap="deleteTask"data-id="{{item.id}}">删除</button></view></view></view>
</view>
4. 全局配置
app.js - 入口文件
const { watchNetworkChange } = require('./utils/network')
const { processAllTasks } = require('./utils/upload')App({onLaunch() {// 监听网络变化,网络恢复时自动同步watchNetworkChange(isConnected => {if (isConnected) {console.log('网络已恢复,开始同步任务')processAllTasks()}})}
})
app.json - 全局配置
{"pages": ["pages/form/form","pages/task/task"],"window": {"backgroundTextStyle": "light","navigationBarBackgroundColor": "#fff","navigationBarTitleText": "离线表单系统","navigationBarTextStyle": "black"},"permission": {"scope.camera": {"desc": "需要使用相机拍摄照片"}}
}
四、关键功能说明
一、离线数据存储:持久化留存,关闭不丢失
- 表单数据采用
wx.setStorageSync同步存储方案,确保数据实时写入本地。 - 图片文件通过
wx.saveFile接口保存至小程序本地文件系统,保障资源本地可访问。 - 所有表单数据与图片资源均实现全量持久化存储,即使关闭小程序或退出账号,数据也不会丢失,重新打开即可恢复。
二、网络监听机制:智能感知,自动同步
- 页面初始化阶段自动触发网络状态检测,快速识别当前网络连通情况。
- 启用全局网络状态监听,实时捕捉网络切换(如Wi-Fi/4G切换、断网/复网)事件。
- 当网络从离线恢复至在线状态时,系统将自动触发待同步任务队列,无需手动操作即可完成数据上传。
三、任务管理流程:状态清晰,操作灵活
- 任务生命周期:采用「pending(待上传)→ uploading(上传中)→ success(上传成功)/ failed(上传失败)」的状态流转逻辑,状态可视化呈现。
- 自动重试机制:上传失败的任务将触发自动重试,默认最多重试3次,降低偶发网络问题导致的上传失败概率。
- 手动操作支持:提供手动重试(针对失败任务)和手动删除(支持失败/成功/待上传任务)功能,满足个性化操作需求。
四、资源清理策略:自动+手动,高效释放空间
- 自动清理:任务上传成功后,系统将自动清理本地缓存的对应图片文件,减少无效存储占用。
- 手动清理:提供「清空成功任务」一键操作功能,可批量删除所有状态为“上传成功”的任务记录及关联冗余数据,快速释放本地存储空间。
五、使用说明
1. 表单页面:离线可填,数据自动留存
- 支持在线/离线状态下填写表单信息,同时可直接拍摄照片或上传本地图片。
- 无网络环境时,表单数据将通过本地存储机制自动保存,图片资源同步留存至本地文件系统。
- 无需手动触发保存操作,填写及拍照完成后即时落地,避免数据丢失。
2. 任务管理页面:状态可视化,操作自主可控
- 集中展示所有任务的实时状态,包括待上传、上传中、上传成功、上传失败,状态分类清晰易辨。
- 提供针对性操作入口:支持手动触发待上传任务同步、对失败任务发起重试,也可按需删除任意状态任务。
- 任务列表直观呈现关键信息(如创建时间、任务类型),方便快速定位目标任务。
3. 网络恢复同步:自动触发,无需干预
- 系统实时监听网络状态,当网络从离线恢复至在线时,将自动启动同步流程。
- 所有处于“待上传”状态的任务将按顺序批量上传,无需用户手动操作。
- 同步过程中实时更新任务状态,同步结果可在任务管理页面查看。
4. 任务成功后处理:自动清理,释放空间
- 当任务上传成功后,系统将自动识别并清理该任务对应的本地缓存图片。
- 清理过程后台静默执行,不影响用户操作,有效释放本地存储空间,避免冗余资源堆积。
- 清理完成后,仅保留任务记录(不含本地图片缓存),确保页面加载流畅。
六、扩展建议
1. 任务优先级设置:灵活排序,重点先行
- 支持在创建任务时,选择「高/中/低」三级优先级,默认优先级为「中」。
- 任务管理页面支持按优先级筛选、排序,高优先级任务置顶展示,方便快速聚焦关键任务。
- 网络恢复同步时,系统将优先处理高优先级任务,确保核心数据优先上传完成。
2. 大文件分片上传:断点续传,高效稳定
- 针对单张超过50MB的图片或大体积文件,自动启用分片上传机制,将文件拆分為10MB/片进行传输。
- 支持断点续传,若上传过程中网络中断或小程序退出,重新连接后可从已上传分片继续传输,无需重复上传完整文件。
- 分片传输过程中实时校验数据完整性,避免因分片丢失导致上传失败,提升大文件上传成功率。
3. 表单模板功能:复用模板,快速填报
- 支持创建自定义表单模板,可保存常用字段(如固定填写项、默认值),生成模板库供重复使用。
- 提供模板编辑、删除、重命名功能,可根据业务需求灵活调整模板内容。
- 新建任务时,可直接选择已有模板快速填充表单,减少重复录入操作,提升填报效率。
4. 数据同步进度条:可视化反馈,清晰可控
- 任务同步(单任务/批量任务)时,页面显示实时进度条,直观展示上传完成百分比。
- 进度条关联任务详情,同步过程中显示已上传大小、剩余时间(预估),让用户清晰掌握同步状态。
- 批量同步时,支持查看整体进度与单个任务进度,兼顾全局与细节反馈。
5. 任务过期清理机制:自动减负,避免冗余
- 支持自定义任务过期规则,可设置「待上传任务30天过期」「失败任务15天过期」「成功任务90天过期」(默认配置)。
- 过期任务将自动标记为「已过期」,并在任务列表单独分类,不影响正常任务查看。
- 系统每月自动清理已过期且超过7天未操作的任务及关联本地缓存,也支持手动一键清理所有过期任务。
6. 数据备份与恢复功能:双重保障,防丢防盗
- 支持本地数据一键备份,将表单数据、任务记录、模板信息备份至本地安全存储目录,生成备份文件(含备份时间戳)。
- 提供备份文件管理功能,可查看历史备份记录、手动创建新备份、删除无效备份。
- 当本地数据异常(如误删、数据损坏)时,可选择任意历史备份文件进行恢复,恢复后保留当前未备份的新增数据。
该方案完整实现了离线表单与拍照上传的核心功能,通过本地存储与网络监听结合的方式,确保在弱网/无网环境下也能正常收集数据,并在网络恢复后自动同步,极大提升了小程序在复杂网络环境下的可用性。

