固件文件解析工具(upgradeFileUtil)
升级文件(OTA 固件文件)导入与本地管理的单例工具模块,负责把用户从微信聊天会话中选中的固件文件复制到小程序本地存储目录,维护文件信息列表,并通过监听器机制把文件列表变化实时同步给页面 UI。
Purpose and Scope
本页面介绍 upgradeFileUtil.ts 的实现细节:文件目录规划、复制导入流程、信息缓存与持久化、监听器机制、删除流程以及错误码约定,并说明调用方 pageUpdate.ts 如何消费这些能力。
本页面不涉及以下内容(属于其他目录页面的职责):
- 蓝牙连接与设备管理(见
lib/bluetooth、lib/bluetoothOTAManager) - OTA 升级协议与升级流程(见
lib/jl_lib/jl_ota_2.1.1、OTA 升级页面) - 日志工具(见
lib/log)
Overview
微信小程序出于安全与沙箱限制,无法直接访问用户本地文件系统,只能通过 wx.chooseMessageFile 从聊天记录中选取文件,且这些文件位于临时目录,随时可能被回收。upgradeFileUtil 的职责就是把这些临时文件持久化到小程序用户数据目录(wx.env.USER_DATA_PATH/upgrade/),形成一份长期有效的“升级文件库”,供后续 OTA 升级流程反复读取。
核心设计要点:
- 单例导出:模块末尾直接
export var UpgradeFileUtil = new UpgradeFileManager(),全小程序共享同一个文件管理实例,避免多个页面各自维护文件列表造成状态不一致。 - 文件上限约束:升级文件整体最大只能 200MB(微信小程序本地存储上限),导入失败时通过错误码
1300202明确告知“剩余空间不足”。 - 存储同步:文件信息列表同时写入微信本地缓存(
wx.setStorageSync("UpgradeFileInfos")),下次启动小程序时自动恢复,实现跨会话持久化。 - 监听器解耦:页面通过
setListener注册回调,文件列表任何变更(新增/删除)都会触发onUpgradeFileInfoList,页面据此刷新 UI,无需页面主动轮询。
Architecture
flowchart TD
subgraph sg_Page["页面层 (pageUpdate.ts)"]
Page["pageUpdate Page"]
UI["文件列表 UI"]
end
subgraph sg_Lib["工具层 (upgradeFileUtil.ts)"]
Util["UpgradeFileUtil<br/>(单例 UpgradeFileManager)"]
List["_upgradeFileInfoList<br/>UpgradeFileInfo[]"]
Listener["_listener<br/>UpgradeFileListener"]
end
subgraph sg_Wx["微信小程序 API"]
FS["FileSystemManager<br/>access/mkdir/copyFile/unlink"]
Storage["Storage<br/>UpgradeFileInfos"]
UserData["wx.env.USER_DATA_PATH/upgrade/"]
end
Page -->|"addUpgradeFile(s)/removeUpgradeFile/getUpgradeFileInfos"| Util
Util -->|"维护"| List
Util -->|"回调 onUpgradeFileInfoList"| Listener
Listener -->|"通知刷新"| UI
Util -->|"读写"| FS
FS -->|"持久化"| UserData
Util -->|"setStorageSync 缓存"| Storage
模块共三层:页面层只关心“导入、删除、读取”三个操作与回调刷新;工具层内部完成文件复制、列表维护与通知分发;微信 API 层负责实际的文件系统操作与数据持久化。
为什么用单例
UpgradeFileManager 在构造时就会读取缓存并初始化目录。若每个页面各自 new 一个实例,会导致多个实例各自持有独立的 _upgradeFileInfoList,彼此不同步,且每个实例都会重复做目录检查。导出单例后,所有页面(当前只有 pageUpdate,未来可能增加)都操作同一份列表,天然一致。
主要实现
数据模型
UpgradeFileInfo 是文件列表的基本单元,四个字段全部可选,采用宽松结构以便兼容旧缓存数据:
| 字段 | 类型 | 含义 |
|---|---|---|
fileName | string? | 用户可读的文件名(单文件导入时允许用户重命名) |
time | number? | 导入时的时间戳(new Date().getTime()),同时用于生成存储文件名 |
filePath | string? | 复制到用户目录后的完整路径,也是删除时 unlink 的依据 |
fileSize | number? | 文件大小(字节),来源于 wx.chooseMessageFile 返回的 size |
Source: upgradeFileUtil.ts
目录初始化与缓存恢复
构造函数做了两件事:确保 upgrade 目录存在;从本地缓存恢复历史文件信息。
constructor() {
this.upgradeFolder = wx.env.USER_DATA_PATH + "/" + "upgrade" + "/"
const fs = wx.getFileSystemManager()
fs.access({
path: this.upgradeFolder,
success: () => {//文件夹存在
},
fail: () => {
fs.mkdirSync(this.upgradeFolder)
}
})
//读取缓存中的文件信息
const cacheUpgradeFileInfos = wx.getStorageSync("UpgradeFileInfos")
if (cacheUpgradeFileInfos !== "") {
this._upgradeFileInfoList = cacheUpgradeFileInfos
}
}
Source: upgradeFileUtil.ts
设计意图:fs.access 是异步探测目录是否存在的标准做法,失败(目录不存在)时用 mkdirSync 同步创建;创建路径用 USER_DATA_PATH 拼接,保证写入位置在小程序沙箱允许的范围内。缓存恢复用 getStorageSync 同步读取,避免页面首次渲染时列表为空造成的闪烁。
核心流程
文件导入流程
sequenceDiagram
participant U as 用户
participant P as pageUpdate 页面
participant M as UpgradeFileManager
participant FS as FileSystemManager
participant ST as Storage
U->>P: 点击“添加OTA文件”
P->>P: wx.chooseMessageFile(count:10)
P-->>P: 拿到 tempFiles 临时文件数组
alt 多个文件
P->>M: addUpgradeFiles(infos)
loop 每个临时文件
M->>M: destPath = upgradeFolder + time + "_" + index
M->>FS: copyFile(srcPath→destPath)
FS-->>M: 成功 → push UpgradeFileInfo
end
M->>ST: setStorageSync("UpgradeFileInfos", list)
M-->>P: resolve(true) / reject(1300202 | -1)
else 单个文件
P->>P: wx.showModal 弹窗让用户重命名
P->>M: addUpgradeFile(fileName, path, size)
M->>FS: copyFile(srcPath→destPath)
FS-->>M: 成功 → push UpgradeFileInfo
M->>ST: setStorageSync(...)
M-->>P: resolve(true)
end
M-->>P: 通过 listener 回调 onUpgradeFileInfoList
P->>P: 刷新文件列表 UI
_addUpgradeFile 复制与错误归类
所有导入路径最终都汇聚到私有方法 _addUpgradeFile,它把“复制文件”封装成一个 Promise,并对失败原因做归一化:
private _addUpgradeFile(fs: WechatMiniprogram.FileSystemManager, fileName: string, time: number, fileSrcPath: string, destPath: string, fileSize: number) {
return new Promise<UpgradeFileInfo>((resolve, reject) => {
const upgradeFileInfo = new UpgradeFileInfo()
upgradeFileInfo.fileName = fileName
upgradeFileInfo.time = time
upgradeFileInfo.filePath = destPath
upgradeFileInfo.fileSize = fileSize
fs.copyFile({
srcPath: fileSrcPath,
destPath: upgradeFileInfo.filePath,
fail: (error) => {
loge("copyFileSync", error);
if (error.errMsg === "copyFile:fail the maximum size of the file storage limit is exceeded") {//剩余空间不足
reject(1300202)
} else {
reject(-1)
}
}, success: () => {
resolve(upgradeFileInfo)
}
})
})
}
Source: upgradeFileUtil.ts
关键点:
- 目标路径使用
time + "_" + index命名,天然保证同一批次内文件名唯一,且按时间排序可反推出导入顺序。 - 错误码约定:
1300202表示“超出文件存储上限”(对应微信返回的固定错误文案),-1表示其他未知失败。页面层只对1300202做专门提示(“上限200MB”),其余错误走通用失败处理。 copyFile成功回调里才resolve,保证 Promise 的 resolve 语义与文件落盘完成严格对应——如果先 resolve 再复制,调用方可能立刻去读文件而读到不完整的文件。
批量导入 addUpgradeFiles
addUpgradeFiles(infos: { fileName: string, fileSrcPath: string, fileSize: number }[]) {
return new Promise<boolean>(async (resolve, reject) => {
const fs = wx.getFileSystemManager()
for (let index = 0; index < infos.length; index++) {
const info = infos[index];
const time = (new Date()).getTime()
const destPath = this.upgradeFolder + time + "_" + index
try {
const result = await this._addUpgradeFile(fs, info.fileName, time, info.fileSrcPath, destPath, info.fileSize)
this._upgradeFileInfoList.push(result)
} catch (error) {
this._onUpgradeFileInfoList(this._upgradeFileInfoList)
reject(error)
return
}
}
this._onUpgradeFileInfoList(this._upgradeFileInfoList)
})
}
Source: upgradeFileUtil.ts
设计意图:要么全部成功,要么全部失败。循环内任一文件复制失败,立即通知一次列表变化(保证 UI 与真实磁盘一致)并 reject,不继续处理剩余文件,避免出现“列表里有一条记录但文件不存在”的脏数据。成功时在循环结束后统一通知一次,减少 UI 刷新次数。
删除流程 removeUpgradeFile
removeUpgradeFile(info: UpgradeFileInfo) {
for (let index = 0; index < this._upgradeFileInfoList.length; index++) {
const element = this._upgradeFileInfoList[index];
if (element.filePath === info.filePath) {
this._upgradeFileInfoList.splice(index, 1)
if (info.filePath) {
const fs = wx.getFileSystemManager()
try {
fs.unlinkSync(info.filePath)
} catch (error) {
}
}
continue
}
}
this._onUpgradeFileInfoList(this._upgradeFileInfoList)
}
Source: upgradeFileUtil.ts
设计意图:以 filePath 作为唯一匹配键(同一路径不可能重复导入,因为命名含时间戳)。unlinkSync 用 try/catch 吞掉删除失败——磁盘文件删不掉不应阻塞列表更新,记录仍从列表移除,避免用户看到无法操作的僵尸条目。删除结束后无论是否真的删到文件都会通知监听器,保证 UI 同步。
通知与持久化 _onUpgradeFileInfoList
private _onUpgradeFileInfoList(infoList: UpgradeFileInfo[]): void {
wx.setStorageSync("UpgradeFileInfos", infoList)
this._listener?.onUpgradeFileInfoList(JSON.parse(JSON.stringify(infoList)))
}
Source: upgradeFileUtil.ts
这是所有变更的“唯一出口”:先持久化缓存,再通过深拷贝(JSON.parse(JSON.stringify(...)))把列表副本推给监听器。深拷贝的意义在于——页面拿到的是独立对象,后续对页面侧数据的修改不会反向污染工具内部的 _upgradeFileInfoList。getUpgradeFileInfos() 也采用同样的深拷贝返回,遵循“内部状态不对外暴露引用”的封装原则。
使用示例
以下示例均提取自调用方页面 pageUpdate.ts。
页面加载时恢复列表并注册监听器
页面 onLoad 时先同步拉取一次当前文件列表渲染 UI,再注册监听器接收后续所有变更通知:
onLoad() {
sBluetoothManager = app.globalData.bluetoothManager
// ...蓝牙事件回调注册...
this._onUpgradeFileInfoList(UpgradeFileUtil.getUpgradeFileInfos())
UpgradeFileUtil.setListener({
onUpgradeFileInfoList: (infoList: any[]) => {
this._onUpgradeFileInfoList(infoList)
}
})
}
Source: pageUpdate.ts
先 getUpgradeFileInfos() 后 setListener 的顺序很重要:先渲染存量数据,再订阅增量事件,避免事件回调在页面尚未初始化完成时触发。
多文件批量导入(文件选择器)
wx.chooseMessageFile 选择 1 个以上文件时走批量导入,跳过重命名:
wx.chooseMessageFile({
count: 10,
type: 'file',
success: res => {
const addFileArray = res.tempFiles
if (addFileArray.length > 1) {//多个文件跳过重命名
const infos = new Array()
for (let index = 0; index < addFileArray.length; index++) {
const element = addFileArray[index];
infos.push({ fileName: element.name, fileSrcPath: element.path, fileSize: element.size })
}
UpgradeFileUtil.addUpgradeFiles(infos).then((res)=>{
wx.showToast({
title: '导入成功',
icon: 'success'
})
}).catch((error) => {
if (error == 1300202) {
wx.showToast({
title: '导入失败,小程序剩余使用空间不足(上限200MB)',
icon: 'none'
})
}
})
} else if (addFileArray.length == 1) {//单个文件重命名
const file = addFileArray[0]
wx.showModal({
title: "请输入文件名",
content: file.name,
editable: true,
success: (res) => {
if (res.confirm == true) {
UpgradeFileUtil.addUpgradeFile(res.content, file.path, file.size).then((res) => {
wx.showToast({
title: '导入成功',
icon: 'success'
})
}).catch((error) => {
if (error == 1300202) {
wx.showToast({
title: '导入失败,小程序剩余使用空间不足(上限200MB)',
icon: 'none'
})
}
})
}
}
})
}
}
})
Source: pageUpdate.ts
这段代码完整展示了工具的三个入参(fileName / fileSrcPath / fileSize)如何从 wx.chooseMessageFile 的 tempFiles 映射而来,以及页面层如何针对错误码 1300202 给出用户可理解的提示文案。
删除文件
用户长按或点击删除文件条目时,直接把列表中的 UpgradeFileInfo 对象交给工具删除:
UpgradeFileUtil.removeUpgradeFile(file)
Source: pageUpdate.ts
删除触发后,工具会同步 unlink 磁盘文件、更新缓存,并通过监听器回调让页面自动刷新列表,页面无需再手动刷新。
配置说明
该模块无外部配置文件,所有“配置”均为硬编码常量与微信平台约束:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
upgradeFolder | string | wx.env.USER_DATA_PATH + "/upgrade/" | 固件文件持久化目录 |
存储键 UpgradeFileInfos | string | "UpgradeFileInfos" | 文件信息列表在 Storage 中的键名 |
| 文件上限 | number | 200MB | 微信小程序本地存储总上限,超限时 copyFile 报固定错误,工具映射为错误码 1300202 |
| 单次选择文件数 | number | 10 | 页面层 wx.chooseMessageFile 的 count 参数 |
| 目标文件命名 | string | {time}_{index} | 时间戳 + 序号,保证唯一性与有序性 |
API 参考
模块通过单例 UpgradeFileUtil(类型 UpgradeFileManager)对外暴露以下方法。
setListener(listener?: UpgradeFileListener): void
注册/替换文件列表变更监听器。传入 undefined 可解除监听。
参数:
listener(UpgradeFileListener?):包含onUpgradeFileInfoList(infoList: UpgradeFileInfo[]): void回调的对象
说明: 同一时刻只保留一个监听器(覆盖式赋值),页面卸载时如不解除,回调可能指向已销毁的页面实例;当前调用方 pageUpdate 为单页使用,未做卸载解除。
Source: upgradeFileUtil.ts
getUpgradeFileInfos(): UpgradeFileInfo[]
返回当前文件信息列表的深拷贝(JSON.parse(JSON.stringify(...))),调用方修改返回值不会影响内部状态。
返回: UpgradeFileInfo[]
Source: upgradeFileUtil.ts
removeUpgradeFile(info: UpgradeFileInfo): void
按 info.filePath 匹配并删除列表项,同时尝试 unlinkSync 删除磁盘文件(失败静默忽略),最后通知监听器并写缓存。
参数:
info(UpgradeFileInfo):要删除的文件信息,只需filePath匹配即可
返回: 无(同步方法)
Source: upgradeFileUtil.ts
addUpgradeFiles(infos: { fileName: string; fileSrcPath: string; fileSize: number }[]): Promise<boolean>
批量导入升级文件,循环内任一文件失败则整体失败(已复制的文件保留在列表与磁盘中)。
参数:
infos:数组,每项含fileName(展示名)、fileSrcPath(临时文件路径)、fileSize(字节)
返回: Promise<boolean>,全部成功 resolve true
Rejects:
1300202:超出文件存储上限(剩余空间不足)-1:其他复制失败
Source: upgradeFileUtil.ts
addUpgradeFile(fileName: string, fileSrcPath: string, fileSize: number): Promise<boolean>
单文件导入,目标路径固定为 upgradeFolder + time + "_0"。单文件场景允许页面先弹窗让用户重命名,再调用本方法。
参数:
fileName(string):展示文件名(可为用户重命名后的名称)fileSrcPath(string):临时文件路径fileSize(number):文件大小(字节)
返回: Promise<boolean>,成功 resolve true
Rejects: 同 addUpgradeFiles(1300202 / -1)
Source: upgradeFileUtil.ts
失败模式与边界情况
| 场景 | 表现 | 处理方式 |
|---|---|---|
| 存储空间不足(超过 200MB 上限) | copyFile 返回 copyFile:fail the maximum size of the file storage limit is exceeded | 映射为 reject(1300202),页面提示“导入失败,小程序剩余使用空间不足(上限200MB)” |
| 其他复制失败(路径无效、权限等) | copyFile fail 回调 | 映射为 reject(-1),页面走通用失败分支(仅 toast 成功/失败,未细分) |
| 目录不存在 | 构造函数 fs.access fail | mkdirSync 同步创建 upgrade 目录 |
| 删除磁盘文件失败 | unlinkSync 抛异常 | try/catch 吞掉异常,列表照常移除,避免僵尸条目 |
| 缓存为空 | getStorageSync("UpgradeFileInfos") 返回 "" | 保持空数组,不解析 |
| 缓存为旧格式/字段缺失 | UpgradeFileInfo 全字段可选 | 不抛错,缺失字段按 undefined 处理 |
| 批量导入中途失败 | 循环内 catch | 立即通知一次列表变化并整体 reject,保证 UI 与已落盘状态一致 |
并发与一致性
- 单线程模型:小程序 JS 为单线程,
_upgradeFileInfoList的读写不存在真正的并发竞态;但addUpgradeFiles使用await串行复制,期间若用户并发触发删除,删除会作用于已 push 的记录,行为一致。 - 通知时机:批量导入的失败路径与成功路径都会通知监听器,且失败路径的通知发生在 reject 之前,保证页面先刷新到真实状态。
- 缓存一致性:所有列表变更(增/删)最终都走
_onUpgradeFileInfoList,先写 Storage 再回调监听器,保证持久化与 UI 刷新顺序稳定。
性能与运维注意事项
- 复制是唯一 I/O 瓶颈:批量导入为逐文件串行
copyFile,单次最多 10 个文件,文件较大时耗时线性增长;页面在导入期间可自行展示 loading(showLoadingView/dismissLoadingView),避免用户重复点击。 - 深拷贝开销:
getUpgradeFileInfos与每次通知都用JSON.parse(JSON.stringify())深拷贝整个列表。文件数量级为个位数到数十个时开销可忽略;若未来文件数增长到数百,可考虑浅拷贝或增量更新。 - 存储键占用:每次变更都会整体重写
UpgradeFileInfos,包含fileName、filePath等字符串字段;小程序 Storage 单键大小上限约 1MB,当前数据量远低于此,无需分片。 - 缓存恢复的容错:构造时若缓存内容被污染(非数组),
_upgradeFileInfoList会被赋成脏数据,调用方_onUpgradeFileInfoList遍历时可能异常。当前实现未做类型校验,属于已知的健壮性边界。 - 用户数据目录:
wx.env.USER_DATA_PATH在用户删除小程序后清空,upgrade目录与文件随之消失,属预期行为;下次导入时会自动重建目录。
扩展点
- 监听器模式:
UpgradeFileListener接口使该模块可与任意页面/组件对接。目前只支持单一监听器;如需多页面同时展示文件列表,可扩展为监听器数组(onUpgradeFileInfoList分发到所有注册者)。 - 错误码归一化:
1300202/-1的二元错误体系可扩展为枚举,将微信平台错误文案逐步映射为业务错误码,便于页面统一处理与上报。 - 文件名冲突策略:当前用
time_index命名天然避免冲突;若未来需要保留用户重命名后的磁盘文件名,可把fileName纳入命名规则(注意非法字符过滤)。 - 元数据扩展:
UpgradeFileInfo字段全可选且为普通类,可平缓增加md5、version、deviceType等字段,用于 OTA 前的固件合法性校验(当前校验逻辑不在本模块内)。
测试情况
仓库中未发现针对 upgradeFileUtil.ts 的独立单元测试文件。模块的可测试性依赖于微信小程序运行时(wx.getFileSystemManager、wx.env.USER_DATA_PATH、wx.getStorageSync),在真机/开发者工具中可验证的行为包括:
- 首次导入时自动创建
upgrade目录 - 导入成功后重启小程序,文件列表从 Storage 恢复
- 空间不足时错误码
1300202正确透出并展示提示 - 删除后磁盘文件消失且列表同步刷新
Related Links
- upgradeFileUtil.ts 源码 — 本页面全部实现来源
- pageUpdate.ts 调用方 — 文件导入/删除/列表刷新的页面级用法
- 蓝牙 OTA 管理(
lib/bluetoothOTAManager)— 升级文件被读取后通过蓝牙通道发送给设备 - OTA 升级协议库(
lib/jl_lib/jl_ota_2.1.1)— 固件包解析与升级流程 - 日志工具(
lib/log)—_addUpgradeFile失败时通过loge记录错误