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

一、方案概述
本方案实现一个支持离线操作的表单与拍照上传系统,核心功能包括:
-
弱网/无网环境下的表单数据本地缓存
-
拍照文件本地存储与上传队列管理
-
网络状态实时监听与自动重试机制(最多3次)
-
任务状态可视化管理
-
成功任务自动清理本地缓存
二、技术架构
-
框架:微信小程序原生框架
-
本地存储:
wx.setStorageSync(表单数据) + 本地文件系统(图片) -
网络监听:
wx.onNetworkStatusChange -
状态管理:全局变量+本地存储结合
-
上传机制:Promise封装+队列管理
三、核心功能实现
1. 项目目录结构
Plain
├── 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 - 本地存储工具
javascript
// 存储键名常量
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()
// 生成唯一ID
task.id = Date.now() + Math.floor(Math.random() * 1000)
task.status = 'pending' // pending, uploading, success, failed
task.retryCount = 0
task.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 = status
tasks[index].retryCount = data.retryCount !== undefined ? data.retryCount : tasks[index].retryCount
tasks[index].completeTime = status === 'success' ? new Date().toISOString() : tasks[index].completeTime
wx.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 - 网络工具
javascript
// 检查网络状态
function checkNetwork() {
return new Promise((resolve) => {
wx.getNetworkType({
success(res) {
const networkType = res.networkType
resolve(networkType !== 'none')
},
fail() {
resolve(false)
}
})
})
}
// 监听网络状态变化
function watchNetworkChange(callback) {
wx.onNetworkStatusChange(res => {
callback(res.isConnected)
})
}
module.exports = {
checkNetwork,
watchNetworkChange
}
utils/upload.js - 上传管理工具
javascript
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) => {
// 这里使用微信的上传API
wx.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 + 1
const 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 - 表单页面
javascript
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.dataset
this.setData({
[`formData.${field}`]: e.detail.value
})
},
// 拍照
takePhoto() {
const that = this
wx.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.savedFilePath
that.setData({
images: [...that.data.images, savedPath]
})
}
})
})
}
})
},
// 预览图片
previewImage(e) {
const { index } = e.currentTarget.dataset
wx.previewImage({
current: this.data.images[index],
urls: this.data.images
})
},
// 删除图片
deleteImage(e) {
const { index } = e.currentTarget.dataset
const newImages = [...this.data.images]
newImages.splice(index, 1)
this.setData({ images: newImages })
},
// 提交表单
async submitForm() {
const { name, date } = this.data.formData
if (!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 - 表单页面模板
xml
<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 - 任务管理页面
javascript
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.dataset
const 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.dataset
wx.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.dataset
wx.previewImage({
current: images[index],
urls: images
})
}
})
pages/task/task.wxml - 任务管理页面模板
xml
<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 - 入口文件
javascript
const { watchNetworkChange } = require('./utils/network')
const { processAllTasks } = require('./utils/upload')
App({
onLaunch() {
// 监听网络变化,网络恢复时自动同步
watchNetworkChange(isConnected => {
if (isConnected) {
console.log('网络已恢复,开始同步任务')
processAllTasks()
}
})
}
})
app.json - 全局配置
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. 数据备份与恢复功能:双重保障,防丢防盗
- 支持本地数据一键备份,将表单数据、任务记录、模板信息备份至本地安全存储目录,生成备份文件(含备份时间戳)。
- 提供备份文件管理功能,可查看历史备份记录、手动创建新备份、删除无效备份。
- 当本地数据异常(如误删、数据损坏)时,可选择任意历史备份文件进行恢复,恢复后保留当前未备份的新增数据。
该方案完整实现了离线表单与拍照上传的核心功能,通过本地存储与网络监听结合的方式,确保在弱网/无网环境下也能正常收集数据,并在网络恢复后自动同步,极大提升了小程序在复杂网络环境下的可用性。
