主题切换
文件系统与本地存储
预计阅读 15 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+
微信小游戏提供完整的文件系统能力和本地存储 API,用于资源缓存、用户存档、日志存储等场景。本章讲解核心用法与最佳实践。
存储方案对比
| API | 容量 | 适用场景 | 数据类型 |
|---|---|---|---|
wx.setStorageSync | ≤ 10MB | 用户设置、存档、token 等小数据 | 字符串/对象(被序列化) |
wx.getFileSystemManager | 受设备限制,通常 200MB+ | 图片缓存、音频缓存、大文件 | Buffer / 二进制 |
| 云开发数据库 | 按套餐 | 跨设备同步的存档 | JSON 文档 |
wx.Storage — 同步键值存储
适合小量数据快速读写:
typescript
class GameStorage {
/** 保存存档 */
static save(key: string, data: unknown): void {
try {
wx.setStorageSync(key, JSON.stringify(data))
} catch (err) {
console.error('存档保存失败', err)
// 存储空间不足时降级处理
this.clearOldData()
}
}
/** 读取存档 */
static load<T>(key: string): T | null {
try {
const raw = wx.getStorageSync(key)
return raw ? (JSON.parse(raw) as T) : null
} catch {
return null
}
}
/** 存储使用情况 */
static getInfo(): { currentSize: number; limitSize: number } {
const info = wx.getStorageInfoSync()
return {
currentSize: info.currentSize, // KB
limitSize: info.limitSize, // KB
}
}
/** 清理过期数据 */
private static clearOldData(): void {
wx.getStorageInfo({
success: (info) => {
const keysToDelete = info.keys.slice(0, Math.floor(info.keys.length * 0.3))
keysToDelete.forEach((key) => {
if (!key.startsWith('perm_')) {
wx.removeStorageSync(key)
}
})
},
})
}
}Storage 注意事项
setStorageSync是同步操作,不要在游戏主循环中频繁调用 — 会阻塞渲染线程。- 单个 key 的数据建议 < 500KB,总存储量不超过 10MB。
- 敏感数据(如 token)建议加密后存储。
wx.getFileSystemManager — 文件系统
核心路径
typescript
const fs = wx.getFileSystemManager()
// 用户文件目录(持久化,可读写)
const userPath = wx.env.USER_DATA_PATH // 如: wxfile://usr/
// 临时文件目录(可能被系统清理)
// 通过 wx.getFileSystemManager() 操作的临时路径写文件
typescript
// 写入用户存档
const saveGameData = (data: object): void => {
const filePath = `${wx.env.USER_DATA_PATH}/save.json`
fs.writeFile({
filePath,
data: JSON.stringify(data),
encoding: 'utf8',
success: () => console.log('存档已保存'),
fail: (err) => console.error('存档失败', err),
})
}
// 写入二进制缓存(如从 CDN 下载的图片)
const cacheImage = async (url: string, key: string): Promise<string> => {
const cacheDir = `${wx.env.USER_DATA_PATH}/cache`
const cachePath = `${cacheDir}/${key}.png`
// 首次运行时创建缓存目录(mkdirSync 第二个参数为 recursive)
try {
fs.accessSync(cacheDir)
} catch {
fs.mkdirSync(cacheDir, true)
}
// 检查是否已缓存
try {
fs.accessSync(cachePath)
return cachePath // 命中缓存
} catch {
// 未缓存,下载
}
return new Promise((resolve, reject) => {
wx.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// 移动到缓存目录
fs.saveFile({
tempFilePath: res.tempFilePath,
filePath: cachePath,
success: () => resolve(cachePath),
fail: reject,
})
}
},
fail: reject,
})
})
}读文件
typescript
// 读取用户存档
const loadGameData = <T = object>(): T | null => {
const filePath = `${wx.env.USER_DATA_PATH}/save.json`
try {
const data = fs.readFileSync(filePath, 'utf8')
return JSON.parse(data) as T
} catch {
return null
}
}目录管理与清理
typescript
const FSHelper = {
/** 确保目录存在 */
ensureDir(dirPath: string): void {
try {
fs.accessSync(dirPath)
} catch {
fs.mkdirSync(dirPath, true)
}
},
/** 获取目录大小 */
getDirSize(dirPath: string): number {
const files = fs.readdirSync(dirPath)
return files.reduce((total, file) => {
try {
const stat = fs.statSync(`${dirPath}/${file}`)
return total + stat.size
} catch {
return total
}
}, 0)
},
/** 清理过期缓存(如超过 7 天的临时文件) */
cleanExpiredCache(maxAgeMs: number = 7 * 24 * 3600 * 1000): number {
const now = Date.now()
const cacheDir = `${wx.env.USER_DATA_PATH}/cache`
let freedBytes = 0
try {
const files = fs.readdirSync(cacheDir)
files.forEach((file) => {
const filePath = `${cacheDir}/${file}`
try {
const stat = fs.statSync(filePath)
if (now - stat.lastModifiedTime > maxAgeMs) {
freedBytes += stat.size
fs.unlinkSync(filePath)
}
} catch {
/* 跳过无法操作的文件 */
}
})
} catch {
/* 缓存目录可能不存在 */
}
return freedBytes
},
}实战:图片缓存层
typescript
class ImageCache {
private static CACHE_DIR = `${wx.env.USER_DATA_PATH}/imgCache`
private static MAX_CACHE_MB = 50
/** 初始化缓存目录并清理超额 */
static init(): void {
FSHelper.ensureDir(this.CACHE_DIR)
this.enforceLimit()
}
/** 获取图片(优先缓存) */
static async get(url: string): Promise<string> {
const cachePath = this.cachePath(url)
// 检查缓存
try {
fs.accessSync(cachePath)
return cachePath
} catch {
/* 未缓存,继续下载 */
}
// 下载并缓存
return new Promise((resolve, reject) => {
wx.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
fs.saveFile({
tempFilePath: res.tempFilePath,
filePath: cachePath,
success: () => resolve(cachePath),
fail: reject,
})
} else {
reject(new Error(`HTTP ${res.statusCode}`))
}
},
fail: reject,
})
})
}
private static cachePath(url: string): string {
// 用 URL 生成唯一文件名
const hash = url.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0)
return `${this.CACHE_DIR}/${Math.abs(hash)}.cache`
}
/** 限制缓存大小,超过时清除最老的文件 */
static enforceLimit(): void {
const limitBytes = this.MAX_CACHE_MB * 1024 * 1024
const files = fs
.readdirSync(this.CACHE_DIR)
.map((f) => `${this.CACHE_DIR}/${f}`)
.map((p) => ({ path: p, ...fs.statSync(p) }))
.sort((a, b) => a.lastModifiedTime - b.lastModifiedTime)
let totalSize = files.reduce((s, f) => s + f.size, 0)
while (totalSize > limitBytes && files.length > 0) {
const oldest = files.shift()!
totalSize -= oldest.size
fs.unlinkSync(oldest.path)
}
}
}文件与存储最佳实践
- 存档加密 — 敏感数据(如玩家进度)使用 AES 或至少简单混淆后存储。
- 定期备份 — 关键存档上传到云数据库或服务端做容灾备份。
- 缓存版本化 — 资源 URL 包含版本号,避免旧缓存影响新资源加载。
- 低频写入 — Storage/文件写入合并批处理,避免在游戏循环中操作。
- 错误兜底 — 所有文件操作必须
try-catch,存储满时优雅降级。
📝 课后练习
练习 1:存储选型
题目: 以下三种数据分别最适合用哪种存储方式?说明理由。
- 玩家最高分(< 1KB,需持久化)
- 关卡地图数据(每关 500KB,20 关)
- 玩家头像缓存(50KB,100+ 个头像)
参考答案:
wx.setStorageSync— 数据极小、读写频繁、需要同步读取,wx.setStorageSync最方便- 文件系统(
wx.getFileSystemManager) — 数据量大,按需加载单独文件,配合分包管理 - 文件系统 + LRU 缓存 — 头像数量多但不需要全量保存(参见本章 ImageCache 示例),文件系统适合二进制数据存储
练习 2:存档安全
题目: 玩家存档被篡改(修改了金币数),如何检测和防范?
参考答案:
- 校验和 — 存档末尾附加
hash(数据 + 密钥),读取时校验 - 服务端备份 — 关键数据(如金币、付费道具)在服务端同步存储,以服务端为准
- 混淆 — 至少使用 base64 + 简单异或,增加破解成本
- 不信任前端 — 核心数据(付费状态、排名)永远以服务端为准
- 防回滚 — 存入版本号或时间戳,拒绝旧版本存档覆盖新版本