在移动应用开发过程中,日志记录是排查问题、分析用户行为的重要手段。对于 UniApp 开发的 App 来说,实现日志本地存储并在需要时导出,能极大地方便问题定位。本文将介绍如何在 UniApp 的 App 端实现日志的本地文件存储功能。
功能需求分析
一个完善的本地日志存储方案应具备以下功能:
- 支持写入不同级别日志(info、warn、error 等)
- 自动记录日志时间和级别
- 按日期分割日志文件,避免单个文件过大
- 支持设置日志文件最大保存天数
- 提供日志文件清理功能
- 确保在 App 重启后日志不会丢失
实现方案
下面是一个完整的日志工具类实现,基于 UniApp 的文件系统 API:
javascript
// logger.js
const LOG_LEVELS = {
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR'
}
class Logger {
constructor() {
this.maxLogDays = 7 // 默认保存7天日志
this.logDir = 'logs' // 日志目录
this.init()
}
async init() {
// 确保日志目录存在
try {
const dirInfo = await this.getDirInfo(this.logDir)
if (!dirInfo) {
await this.createDir(this.logDir)
}
} catch (e) {
console.error('初始化日志目录失败:', e)
}
// 启动时清理过期日志
this.cleanOldLogs()
}
// 设置日志最大保存天数
setMaxLogDays(days) {
if (days > 0) {
this.maxLogDays = days
}
}
// 获取当前日期字符串 (格式: YYYY-MM-DD)
getCurrentDateStr() {
const date = new Date()
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
// 获取当前时间字符串
getCurrentTimeStr() {
const date = new Date()
return `${this.getCurrentDateStr()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}.${date.getMilliseconds().toString().padStart(3, '0')}`
}
// 获取日志文件名
getLogFileName() {
return `app_${this.getCurrentDateStr()}.log`
}
// 写入日志
async log(level, message, data = null) {
try {
const time = this.getCurrentTimeStr()
let logContent = `[${time}] [${level}] ${message}`
if (data) {
logContent += ` | ${typeof data === 'object' ? JSON.stringify(data) : data}`
}
logContent += '\n'
const fileName = this.getLogFileName()
const filePath = `${this.logDir}/${fileName}`
// 检查文件是否存在
const fileInfo = await this.getFileInfo(filePath)
if (fileInfo) {
// 文件存在,追加内容
await this.appendFile(filePath, logContent)
} else {
// 文件不存在,创建新文件
await this.writeFile(filePath, logContent)
}
} catch (e) {
console.error('写入日志失败:', e)
}
}
// 快捷方法
debug(message, data) {
this.log(LOG_LEVELS.DEBUG, message, data)
}
info(message, data) {
this.log(LOG_LEVELS.INFO, message, data)
}
warn(message, data) {
this.log(LOG_LEVELS.WARN, message, data)
}
error(message, data) {
this.log(LOG_LEVELS.ERROR, message, data)
}
// 清理过期日志
async cleanOldLogs() {
try {
const now = new Date()
const threshold = now.setDate(now.getDate() - this.maxLogDays)
const dirInfo = await this.getDirInfo(this.logDir)
if (!dirInfo) return
const files = await this.readDir(this.logDir)
for (const file of files) {
const fileName = file.name
// 解析文件名中的日期
const dateStr = fileName.match(/app_(\d{4}-\d{2}-\d{2})\.log/)?.[1]
if (dateStr) {
const fileDate = new Date(dateStr)
if (fileDate.getTime() < threshold) {
// 文件日期早于阈值,删除
await this.deleteFile(`${this.logDir}/${fileName}`)
}
}
}
} catch (e) {
console.error('清理日志失败:', e)
}
}
// 手动清理所有日志
async clearAllLogs() {
try {
const files = await this.readDir(this.logDir)
for (const file of files) {
await this.deleteFile(`${this.logDir}/${file.name}`)
}
} catch (e) {
console.error('清除所有日志失败:', e)
}
}
// 获取所有日志文件列表
async getLogFiles() {
try {
const files = await this.readDir(this.logDir)
return files.filter(file => file.name.endsWith('.log'))
} catch (e) {
console.error('获取日志文件列表失败:', e)
return []
}
}
// 读取日志文件内容
async readLogFile(fileName) {
try {
const filePath = `${this.logDir}/${fileName}`
const content = await this.readFile(filePath)
return content
} catch (e) {
console.error('读取日志文件失败:', e)
return null
}
}
// ========== 文件系统操作封装 ==========
// 获取目录信息
async getDirInfo(dirPath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${dirPath}`,
entry => resolve(entry),
error => resolve(null)
)
})
}
// 创建目录
async createDir(dirPath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
'_doc',
rootEntry => {
rootEntry.getDirectory(
dirPath,
{ create: true, exclusive: false },
dirEntry => resolve(dirEntry),
error => reject(error)
)
},
error => reject(error)
)
})
}
// 获取文件信息
async getFileInfo(filePath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${filePath}`,
entry => resolve(entry),
error => resolve(null)
)
})
}
// 写入文件
async writeFile(filePath, content) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
'_doc',
rootEntry => {
rootEntry.getFile(
filePath,
{ create: true, exclusive: false },
fileEntry => {
fileEntry.createWriter(
writer => {
writer.onwriteend = () => resolve()
writer.onerror = e => reject(e)
writer.write(content)
},
error => reject(error)
)
},
error => reject(error)
)
},
error => reject(error)
)
})
}
// 追加内容到文件
async appendFile(filePath, content) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${filePath}`,
fileEntry => {
fileEntry.createWriter(
writer => {
writer.onwriteend = () => resolve()
writer.onerror = e => reject(e)
writer.seek(writer.length)
writer.write(content)
},
error => reject(error)
)
},
error => reject(error)
)
})
}
// 读取文件内容
async readFile(filePath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${filePath}`,
fileEntry => {
fileEntry.file(
file => {
const reader = new plus.io.FileReader()
reader.onloadend = e => resolve(e.target.result)
reader.onerror = e => reject(e)
reader.readAsText(file)
},
error => reject(error)
)
},
error => reject(error)
)
})
}
// 读取目录内容
async readDir(dirPath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${dirPath}`,
dirEntry => {
const reader = dirEntry.createReader()
reader.readEntries(
entries => resolve(entries),
error => reject(error)
)
},
error => reject(error)
)
})
}
// 删除文件
async deleteFile(filePath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/${filePath}`,
entry => {
entry.remove(
() => resolve(),
error => reject(error)
)
},
error => reject(error)
)
})
}
}
// 创建全局单例
const logger = new Logger()
export default logger
使用方法
在 UniApp 项目中使用该日志工具非常简单,以下是使用示例:
javascript
import logger from './logger.js'
// 记录不同级别日志
logger.debug('这是一条调试信息', { key: 'value' })
logger.info('这是一条普通信息')
logger.warn('这是一条警告信息')
logger.error('这是一条错误信息', new Error('示例错误'))
// 设置日志最大保存天数
logger.setMaxLogDays(30) // 保存30天日志
// 手动清理日志
logger.cleanOldLogs()
// 清除所有日志
logger.clearAllLogs()
使用时,只需在项目中引入该方法,即可在任何需要记录日志的地方调用相应方法。