感觉好久没写东西了,最近一段时间也是事儿比较多。
而且我感觉自从AI开发以后,好像很多东西我也不用了解的那么透彻了。
直接交给AI齐活了,唉!技术人的喜讯还是悲哀?
不知道大家用没用过 uni-app 开发,我之前用 uni-app 开发的 Android App 一直感觉太慢了,比较卡,最近发现uni-app的升级版本 uni-app x 迭代的比较快,所以搞一下。
正好最近有个App的小项目,直接用uni-app x实现的,感觉和原生相比,虽然还有一定的差距,但是已经不太明显了。
一、功能需求与技术亮点
1. 核心需求
实现APP端本地JSON文件的两大核心能力:
-
JSON导出:读取本地Storage缓存数据,自动生成规范命名的JSON文件,保存至APP沙盒自定义目录
-
JSON导入:选择本地JSON文件,校验文件格式、解析数据,支持「追加导入/覆盖导入」两种模式,兼容单对象、数组格式JSON
2. 技术亮点
-
纯前端实现,零后端接口依赖,离线可用
-
基于uni-app X原生文件系统API,适配Android、IOS双端
-
完善的数据格式化、异常捕获、格式校验机制,避免脏数据
-
支持导入模式选择,兼顾数据保留与全覆盖场景
-
自动记录导入/导出时间、数据条数,可视化数据状态
二、实现原理
uni-app X 操作本地文件核心依赖 uni.getFileSystemManager() 文件管理器API,配合本地Storage实现数据流转:
-
导出流程:读取Storage缓存数据 → 格式化JSON字符串 → 创建本地目录 → 写入JSON文件 → 记录导出时间
-
导入流程:选择本地文件 → 校验JSON格式 → 读取文件内容 → 解析标准化数据 → 按模式导入Storage → 记录导入时间
三、代码实现
html
<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1" scroll-y>
<!-- #endif -->
<view class="setting-container">
<view class="function-grid">
<view class="function-item" @click="onImport">
<text class="function-title">导入数据</text>
<text class="function-desc">从本地JSON文件导入</text>
</view>
<view class="function-item" @click="onExport">
<text class="function-title">导出数据</text>
<text class="function-desc">备份为本地JSON文件</text>
</view>
</view>
<view class="info-section">
<view class="info-item">
<text class="info-label">当前数据条数</text>
<text class="info-value">{{ dataCount }}</text>
</view>
<view class="info-item">
<text class="info-label">最后导出时间</text>
<text class="info-value">{{ lastExportTime }}</text>
</view>
<view class="info-item">
<text class="info-label">最后导入时间</text>
<text class="info-value">{{ lastImportTime }}</text>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
typescript
// 定义本地数据结构体
type Contact = {
id : string
user : string
password : string
username : string
other : string
}
// 本地缓存key常量
const importMetaKey = 'setting__TYPE_lastImportTime'
const exportMetaKey = 'setting__TYPE_lastExportTime'
// 响应式数据
const dataCount = ref<number>(0)
const lastExportTime = ref<string>('从未导出')
const lastImportTime = ref<string>('从未导入')
// 初始化文件管理器
const fs = uni.getFileSystemManager()
// 过滤有效业务数据缓存key(过滤系统配置缓存)
function getContactStorageKeys() : string[] {
const storageInfo = uni.getStorageInfoSync()
const keys = storageInfo.keys
const result : string[] = []
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key.indexOf('__TYPE') < 0) {
result.push(key)
}
}
return result
}
// 格式化当前时间
function formatNow() : string {
const now = new Date()
let year = now.getFullYear().toString()
let month = (now.getMonth() + 1).toString().padStart(2, '0')
let day = now.getDate().toString().padStart(2, '0')
let hours = now.getHours().toString().padStart(2, '0')
let minutes = now.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 生成自定义导出文件名(时间戳唯一命名)
function buildExportFileName() : string {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0')
return `data_backup_${year}${month}${day}_${hours}${minutes}${seconds}.json`
}
// 确保导出目录存在(不存在则创建)
function ensureExportDir(dirPath : string) : void {
try {
fs.mkdir({ dirPath, recursive: true })
} catch (e) {
console.log('目录已存在,无需创建')
}
}
// 校验文件是否为JSON格式
function isJsonFileName(fileName : string) : boolean {
return fileName.toLowerCase().endsWith('.json')
}
// 安全获取JSON字段(空值兜底)
function getJsonStringField(data : UTSJSONObject, key : string) : string {
const value = data[key] as string | null
return value ?? ''
}
// 标准化单条导入数据
function normalizeImportContact(data : UTSJSONObject) : Contact | null {
return {
id: generateUniqueId(),
user: getJsonStringField(data, 'user'),
password: getJsonStringField(data, 'password'),
username: getJsonStringField(data, 'username'),
other: getJsonStringField(data, 'other')
}
}
// 解析JSON文本为标准数据数组(兼容单对象/数组格式)
function parseImportContacts(jsonText : string) : Contact[] {
const parsed = JSON.parse(jsonText) as UTSJSONObject | UTSJSONObject[] | null
const result : Contact[] = []
if (!parsed) return result
// 数组格式遍历解析
if (parsed instanceof Array) {
parsed.forEach(item => {
const normalized = normalizeImportContact(item)
if (normalized) result.push(normalized)
})
} else {
// 单个对象直接解析
const single = normalizeImportContact(parsed)
if (single) result.push(single)
}
return result
}
// 获取本地所有缓存业务数据
function getStoredContactList() : Contact[] {
const keys = getContactStorageKeys()
const result : Contact[] = []
keys.forEach(key => {
const rawValue = uni.getStorageSync(key) as string
if (!rawValue) return
try {
const list = parseImportContacts(rawValue)
result.push(...list)
} catch (e) {
console.log('跳过无效数据:', key)
}
})
return result
}
// 保存导入数据到本地缓存(支持覆盖/追加模式)
function saveImportedContacts(list : Contact[], replaceMode : boolean) : number {
// 覆盖模式:清空原有数据
if (replaceMode) {
const oldKeys = getContactStorageKeys()
oldKeys.forEach(key => uni.removeStorageSync(key))
}
let savedCount = 0
list.forEach(contact => {
contact.id = contact.id || generateUniqueId()
uni.setStorageSync(contact.id, JSON.stringify(contact))
savedCount++
})
return savedCount
}
// 导入数据二次确认弹窗
function confirmImportContacts(list : Contact[], fileName : string) : void {
if (list.length === 0) {
uni.showToast({ title: '未解析到有效数据', icon: 'none' })
return
}
// 选择导入模式
uni.showActionSheet({
itemList: ['追加导入(保留现有数据)', '覆盖导入(清空后导入)'],
success: (sheetRes) => {
const replaceMode = sheetRes.tapIndex === 1
const modeText = replaceMode ? '覆盖' : '追加'
uni.showModal({
title: '确认导入',
content: `文件:${fileName}\n共解析 ${list.length} 条数据,${modeText}模式导入?`,
confirmText: '开始导入',
success: (modalRes) => {
if (modalRes.confirm) {
const savedCount = saveImportedContacts(list, replaceMode)
const nowText = formatNow()
lastImportTime.value = nowText
uni.setStorageSync(importMetaKey, nowText)
uni.showToast({ title: `成功导入 ${savedCount} 条数据`, icon: 'success' })
}
}
})
}
})
}
// 读取选中的本地JSON文件
function readSelectedJsonFile(filePath : string, fileName : string) : void {
fs.readFile({
filePath,
encoding: 'utf-8',
success: (res) => {
try {
const contacts = parseImportContacts(res.data as string)
confirmImportContacts(contacts, fileName)
} catch (e) {
uni.showToast({ title: 'JSON解析失败,格式错误', icon: 'none' })
}
},
fail: () => uni.showToast({ title: '文件读取失败', icon: 'none' })
})
}
// 导入入口方法
function onImport() : void {
uni.chooseFile({
type: 'all',
count: 1,
success: (res) => {
const file = res.tempFiles[0]
if (!isJsonFileName(file.name)) {
uni.showToast({ title: '请选择JSON格式文件', icon: 'none' })
return
}
readSelectedJsonFile(file.path, file.name)
},
fail: () => uni.showToast({ title: '文件选择失败', icon: 'none' })
})
}
// 导出入口方法
function onExport() : void {
const contacts = getStoredContactList()
if (contacts.length === 0) {
uni.showToast({ title: '暂无可导出数据', icon: 'none' })
return
}
// 拼接导出路径、生成文件
const jsonText = JSON.stringify(contacts)
const exportDir = uni.env.SANDBOX_PATH + '/data-backup'
ensureExportDir(exportDir)
const fileName = buildExportFileName()
const filePath = `${exportDir}/${fileName}`
// 写入本地文件
fs.writeFile({
filePath,
data: jsonText,
encoding: 'utf-8',
success: () => {
const nowText = formatNow()
lastExportTime.value = nowText
uni.setStorageSync(exportMetaKey, nowText)
uni.showModal({
title: '导出成功',
content: `已导出 ${contacts.length} 条数据\n文件路径:${filePath}`,
showCancel: false
})
},
fail: () => uni.showToast({ title: '文件导出失败', icon: 'none' })
})
}
总结
这部分代码我也让AI帮忙给完善了一下,感觉健壮性还是比较好的。
有其他需要的兄弟也可以私信我~