鸿蒙数据持久化与网络
What — 是什么
鸿蒙数据持久化与网络是应用开发的基础能力,包括键值存储(Preferences)、关系型数据库(RDB)、文件管理、HTTP 网络请求、WebSocket 通信等。鸿蒙通过 @kit.ArkData 和 @kit.NetworkKit 提供统一的 API,支持从简单配置存储到复杂数据管理的全场景需求。
核心概念:
- Preferences:轻量级键值对存储,类似 SharedPreferences/AsyncStorage
- RDB:关系型数据库,基于 SQLite,支持事务和复杂查询
- DataShare:跨应用数据共享机制
- 分布式数据库:跨设备自动同步的数据库
- @ohos.net.http:HTTP 网络请求模块
- @ohos.net.webSocket:WebSocket 通信模块
数据存储方案对比:
| 方案 | 适用场景 | 数据量 | 复杂度 |
|---|---|---|---|
| Preferences | 配置/设置/Token | < 几KB | 低 |
| RDB (SQLite) | 结构化数据/复杂查询 | 无上限 | 中 |
| 分布式数据库 | 跨设备同步数据 | 无上限 | 中 |
| 文件系统 | 文件/图片/缓存 | 无上限 | 低 |
| DataShare | 跨应用数据共享 | 无上限 | 高 |
Why — 为什么
适用场景:
- 用户配置和偏好设置存储
- 离线数据缓存
- 结构化业务数据管理
- RESTful API 网络请求
- WebSocket 实时通信
对比 Flutter/React Native:
| 维度 | 鸿蒙 | Flutter | React Native |
|---|---|---|---|
| KV 存储 | Preferences | SharedPreferences | AsyncStorage |
| 数据库 | RDB (SQLite) | sqflite/drift | WatermelonDB |
| 网络请求 | @ohos.net.http | dio/http | axios/fetch |
| WebSocket | @ohos.net.webSocket | web_socket_channel | ws |
| 类型安全 | 强类型(ArkTS) | 强类型(Dart) | 弱类型(JS) |
How — 怎么用
1. Preferences 键值存储
import { preferences } from '@kit.ArkData'
// ===== 初始化 =====
let pref: preferences.Preferences
async function initPreferences(context: Context) {
pref = await preferences.getPreferences(context, 'my_settings')
}
// ===== 读写操作 =====
// 写入
await pref.put('isDarkMode', true)
await pref.put('username', 'Alice')
await pref.put('fontSize', 16)
await pref.flush() // 持久化到磁盘
// 读取
const isDark = await pref.get('isDarkMode', false) as boolean
const name = await pref.get('username', '') as string
const size = await pref.get('fontSize', 14) as number
// 删除
await pref.delete('username')
await pref.flush()
// 清空
await pref.clear()
await pref.flush()
// ===== 监听变更 =====
pref.on('change', (data: preferences.PreferenceChangeData) => {
console.log(`Key "${data.key}" changed`)
})
// ===== 完整封装 =====
class SettingsManager {
private pref: preferences.Preferences | null = null
async init(context: Context) {
this.pref = await preferences.getPreferences(context, 'settings')
}
async get<T>(key: string, defaultValue: T): Promise<T> {
if (!this.pref) return defaultValue
return (await this.pref.get(key, defaultValue)) as T
}
async set<T>(key: string, value: T): Promise<void> {
if (!this.pref) return
await this.pref.put(key, value)
await this.pref.flush()
}
async remove(key: string): Promise<void> {
if (!this.pref) return
await this.pref.delete(key)
await this.pref.flush()
}
}
2. RDB 关系型数据库
import { relationalStore } from '@kit.ArkData'
// ===== 创建数据库 =====
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'myapp.db',
securityLevel: relationalStore.SecurityLevel.S1
}
let store: relationalStore.RdbStore
// SQL 建表
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
`
// 初始化
async function initDatabase(context: Context) {
store = await relationalStore.getRdbStore(context, STORE_CONFIG)
await store.executeSql(CREATE_TABLE_SQL)
}
// ===== 增(Insert)=====
async function addUser(user: User): Promise<number> {
const valueBucket: relationalStore.ValuesBucket = {
name: user.name,
email: user.email,
age: user.age
}
const rowId = await store.insert('users', valueBucket)
return rowId
}
// ===== 删(Delete)=====
async function deleteUser(id: number): Promise<number> {
const predicates = new relationalStore.RdbPredicates('users')
predicates.equalTo('id', id)
return await store.delete(predicates)
}
// ===== 改(Update)=====
async function updateUser(id: number, updates: Partial<User>): Promise<number> {
const valueBucket: relationalStore.ValuesBucket = {}
if (updates.name !== undefined) valueBucket.name = updates.name
if (updates.email !== undefined) valueBucket.email = updates.email
if (updates.age !== undefined) valueBucket.age = updates.age
const predicates = new relationalStore.RdbPredicates('users')
predicates.equalTo('id', id)
return await store.update(valueBucket, predicates)
}
// ===== 查(Query)=====
async function getAllUsers(): Promise<User[]> {
const predicates = new relationalStore.RdbPredicates('users')
predicates.orderByDesc('created_at')
const resultSet = await store.query(predicates)
const users: User[] = []
while (resultSet.goToNextRow()) {
users.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
email: resultSet.getString(resultSet.getColumnIndex('email')),
age: resultSet.getLong(resultSet.getColumnIndex('age'))
})
}
resultSet.close()
return users
}
// 条件查询
async function getUsersByAge(minAge: number): Promise<User[]> {
const predicates = new relationalStore.RdbPredicates('users')
predicates.greaterThanOrEqualTo('age', minAge)
predicates.limitAs(20)
predicates.offsetAs(0)
const resultSet = await store.query(predicates)
// ... 同上解析
return []
}
// ===== 原始 SQL =====
async function rawQuery() {
const resultSet = await store.querySql(
'SELECT * FROM users WHERE age > ? ORDER BY name',
[18]
)
// ... 解析结果
}
// ===== 事务 =====
store.beginTransaction()
try {
await store.insert('users', user1Bucket)
await store.insert('users', user2Bucket)
store.commit()
} catch (e) {
store.rollBack()
}
// ===== 数据库版本迁移 =====
store = await relationalStore.getRdbStore(context, {
name: 'myapp.db',
securityLevel: relationalStore.SecurityLevel.S1,
version: 2
})
store.version = 2
// 在 onUpgrade 回调中执行 ALTER TABLE 等迁移 SQL
3. 文件管理
import { fs, picker } from '@kit.CoreFileKit'
import { common } from '@kit.AbilityKit'
// ===== 沙箱目录 =====
const context = getContext(this) as common.UIAbilityContext
context.filesDir // /data/storage/el2/base/haps/entry/files
context.cacheDir // 缓存目录
context.tempDir // 临时目录
context.distributedFilesDir // 分布式文件目录
// ===== 文件读写 =====
// 写文本
const filePath = context.filesDir + '/note.txt'
fs.writeTextSync(filePath, 'Hello HarmonyOS')
// 读文本
const content = fs.readTextSync(filePath)
// 写二进制
const binPath = context.filesDir + '/data.bin'
fs.writeSync(fs.openSync(binPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE).fd, buffer)
// 追加
fs.writeTextSync(filePath, '\nNew line', { append: true })
// ===== 文件操作 =====
// 复制
fs.copyFileSync(srcPath, destPath)
// 移动
fs.moveFileSync(srcPath, destPath)
// 删除
fs.unlinkSync(filePath)
// 判断存在
const exists = fs.accessSync(filePath)
// 创建目录
fs.mkdirSync(context.filesDir + '/images')
// 列出目录
const files = fs.listFileSync(context.filesDir + '/images')
// ===== 文件选择器 =====
async function pickFile() {
const documentPicker = new picker.DocumentViewPicker()
const result = await documentPicker.select({
maxSelectNumber: 5,
fileSuffixFilters: ['.jpg', '.png', '.pdf']
})
result.forEach((uri) => {
console.log('Selected: ' + uri)
})
}
// 保存文件
async function saveFile() {
const savePicker = new picker.DocumentSaveOptions()
savePicker.newFileNames = ['output.txt']
const documentPicker = new picker.DocumentViewPicker()
const uri = await documentPicker.save(savePicker)
if (uri) {
fs.writeTextSync(uri, 'Saved content')
}
}
4. HTTP 网络请求
import { http } from '@kit.NetworkKit'
// ===== 基础 GET 请求 =====
async function fetchData() {
const response = await http.request(
'https://api.example.com/users',
{
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' },
connectTimeout: 10000,
readTimeout: 10000
}
)
if (response.responseCode === 200) {
const data = JSON.parse(response.result as string)
console.log('Users: ' + JSON.stringify(data))
}
}
// ===== POST 请求 =====
async function createUser(name: string, email: string) {
const response = await http.request(
'https://api.example.com/users',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
extraData: JSON.stringify({ name, email })
}
)
return response
}
// ===== 网络请求封装 =====
class HttpClient {
private baseUrl: string
private token: string = ''
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
setToken(token: string) {
this.token = token
}
async request<T>(method: http.RequestMethod, path: string, data?: Object): Promise<T> {
const options: http.HttpRequestOptions = {
method,
header: {
'Content-Type': 'application/json',
...(this.token ? { 'Authorization': `Bearer ${this.token}` } : {})
},
connectTimeout: 15000,
readTimeout: 15000
}
if (data && (method === http.RequestMethod.POST || method === http.RequestMethod.PUT)) {
options.extraData = JSON.stringify(data)
}
try {
const response = await http.request(`${this.baseUrl}${path}`, options)
if (response.responseCode === 401) {
this.token = ''
throw new Error('Unauthorized')
}
if (response.responseCode >= 400) {
const error = JSON.parse(response.result as string)
throw new Error(error.message || `HTTP ${response.responseCode}`)
}
return JSON.parse(response.result as string) as T
} catch (e) {
console.error(`Request failed: ${method} ${path}`, e)
throw e
}
}
get<T>(path: string) { return this.request<T>(http.RequestMethod.GET, path) }
post<T>(path: string, data?: Object) { return this.request<T>(http.RequestMethod.POST, path, data) }
put<T>(path: string, data?: Object) { return this.request<T>(http.RequestMethod.PUT, path, data) }
delete<T>(path: string) { return this.request<T>(http.RequestMethod.DELETE, path) }
}
// 使用
const api = new HttpClient('https://api.example.com')
api.setToken('my_token')
const users = await api.get<User[]>('/users')
await api.post('/users', { name: 'Alice' })
5. WebSocket 通信
import { webSocket } from '@kit.NetworkKit'
// ===== 创建 WebSocket 连接 =====
let ws = webSocket.createWebSocket()
// 监听事件
ws.on('open', (err, data) => {
console.log('WebSocket connected')
// 发送消息
ws.send('Hello Server')
})
ws.on('message', (err, data) => {
console.log('Received: ' + data)
// 解析 JSON 消息
try {
const msg = JSON.parse(data as string)
handleMessage(msg)
} catch (e) {
console.error('Parse error', e)
}
})
ws.on('close', (err, data) => {
console.log('WebSocket closed: ' + data.reason)
// 自动重连
setTimeout(() => connect(), 3000)
})
ws.on('error', (err) => {
console.error('WebSocket error: ' + err.message)
})
// 连接
function connect() {
ws.connect('wss://example.com/ws', {
header: { 'Authorization': 'Bearer ' + token }
})
}
// 发送消息
function send(type: string, payload: Object) {
ws.send(JSON.stringify({ type, payload, timestamp: Date.now() }))
}
// 关闭
function disconnect() {
ws.close()
}
6. JSON 序列化
// ===== 手动序列化 =====
interface UserJSON {
name: string
email: string
age: number
}
class User {
name: string = ''
email: string = ''
age: number = 0
static fromJson(json: UserJSON): User {
const user = new User()
user.name = json.name
user.email = json.email
user.age = json.age
return user
}
toJson(): UserJSON {
return {
name: this.name,
email: this.email,
age: this.age
}
}
}
// 使用
const jsonStr = '{"name":"Alice","email":"a@b.com","age":25}'
const user = User.fromJson(JSON.parse(jsonStr) as UserJSON)
const output = JSON.stringify(user.toJson())
7. 网络状态检测
import { connection } from '@kit.NetworkKit'
// ===== 检查当前网络状态 =====
async function checkNetwork() {
const netHandle = await connection.getDefaultNet()
const capabilities = await connection.getNetCapabilities(netHandle)
const hasWifi = capabilities.bearerTypes.includes(connection.NetBearType.BEARER_WIFI)
const hasCellular = capabilities.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR)
const hasInternet = capabilities.capabilities.includes(connection.NetCap.NET_CAPABILITY_INTERNET)
console.log(`WiFi: ${hasWifi}, Cellular: ${hasCellular}, Internet: ${hasInternet}`)
}
// ===== 监听网络变化 =====
connection.on('netAvailable', (data) => {
console.log('Network available')
})
connection.on('netLost', (data) => {
console.log('Network lost')
// 提示用户网络断开
})
connection.on('netCapabilitiesChange', (data) => {
console.log('Network capabilities changed')
})
8. 安全存储与加密
import { cryptoFramework } from '@kit.CryptoArchitectureKit'
// ===== AES 加密 =====
async function encrypt(plainText: string, key: string): Promise<string> {
const cipher = cryptoFramework.createCipher('AES256|CBC|PKCS7')
const symKey = await generateKey(key)
const iv = cryptoFramework.createBlob({ data: new Uint8Array(16) }) // 初始化向量
await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, { params: iv })
const input = cryptoFramework.createBlob({ data: new TextEncoder().encode(plainText) })
const output = await cipher.doFinal(input)
return buffer.from(output.data).toString('base64')
}
// ===== 安全存储 Token =====
// 使用 HUKS(Universal KeyStore)存储密钥
// 或简单使用 Preferences + 加密
async function saveToken(token: string) {
const encrypted = await encrypt(token, APP_SECRET)
await pref.put('auth_token', encrypted)
await pref.flush()
}
async function getToken(): Promise<string> {
const encrypted = await pref.get('auth_token', '') as string
if (!encrypted) return ''
return await decrypt(encrypted, APP_SECRET)
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Preferences 数据丢失 | 未调用 flush | 修改后必须 flush |
| RDB 查询崩溃 | 列索引越界 | 用 getColumnIndex 先检查 |
| HTTP 请求超时 | 网络或服务端问题 | 设置合理超时 + 重试 |
| WebSocket 断连 | 网络波动 | 实现自动重连 + 心跳 |
| 文件权限被拒 | 沙箱限制 | 使用文件选择器 |
| JSON 解析失败 | 后端返回非 JSON | 先检查 responseCode |
| 数据库锁死 | 事务未提交/回滚 | 确保 commit/rollBack |
最佳实践
- 轻量配置用 Preferences,结构化数据用 RDB
- 网络请求统一封装,处理 Token、错误、重试
- WebSocket 实现自动重连 + 心跳保活
- RDB 操作用事务保证原子性
- 文件操作注意沙箱路径限制
- 敏感数据加密存储
- 监听网络状态,离线时使用缓存
- JSON 序列化为每个 Model 写 fromJson/toJson
面试题
Q1: Preferences 和 RDB 分别适合什么场景?
Preferences 是轻量级键值对存储,适合简单配置数据(主题、语言、Token),数据量建议不超过几 KB,API 简单(get/put/flush),但不支持复杂查询。RDB 是基于 SQLite 的关系型数据库,适合结构化业务数据(用户表、订单表),支持 SQL 查询、事务、索引、联合查询等,数据量无上限。选择原则:配置项/小数据用 Preferences;业务数据/需要查询用 RDB;跨设备同步用分布式数据库。
Q2: 如何封装鸿蒙的 HTTP 请求?和 Flutter 的 dio 有什么区别?
封装思路类似 dio:创建 HttpClient 类,封装 baseUrl、默认 header(Content-Type/Authorization)、超时配置、错误处理。和 dio 的区别:①鸿蒙用 @ohos.net.http 的 http.request(),每次请求创建新对象(无连接复用);dio 内置连接池。②鸿蒙无拦截器机制,需要在 request 方法中手动处理;dio 有 Interceptors 链式调用。③鸿蒙不支持 FormData 上传文件,需要用 multipart 上传;dio 原生支持。④鸿蒙的响应是同步解析 result,dio 返回 Response 对象。总体上鸿蒙的 HTTP 模块更底层,需要更多手动封装。
Q3: RDB 的事务机制如何使用?为什么需要事务?
事务通过 beginTransaction/commit/rollBack 三个方法使用:beginTransaction 开始事务 → 执行多条 SQL → commit 提交 → 出错时 rollBack 回滚。事务保证 ACID:原子性(要么全部成功要么全部失败)、一致性(数据库状态始终合法)、隔离性(事务间互不干扰)、持久性(提交后数据不丢失)。典型场景:银行转账(扣款+入账必须同时成功)、订单创建(插入订单+扣减库存)。不用事务时如果中间步骤失败,会导致数据不一致。
Q4: WebSocket 如何实现自动重连和心跳?
自动重连:在 on(‘close’) 和 on(‘error’) 回调中用 setTimeout 延迟重连(3-5 秒),设置重连次数上限,超出后提示用户。心跳机制:定时(30秒)发送 ping 消息,如果连续 N 次未收到 pong 响应则判定断连并触发重连。实现要点:①重连前先 close 旧连接;②使用指数退避(1s/2s/4s/8s)避免频繁重连;③重连成功后重新订阅频道;④页面 onHide 时停止心跳,onShow 时恢复。这套方案和 Web 端的 WebSocket 重连逻辑一致。
Q5: 鸿蒙的文件沙箱机制是怎样的?和 Android 有什么区别?
鸿蒙应用文件沙箱:每个应用有独立文件目录(filesDir/cacheDir/tempDir),应用只能访问自己的沙箱目录,不能直接访问其他应用的文件。跨应用文件访问需要通过文件选择器(picker)或 DataShare。和 Android 区别:①Android 的 scoped storage 限制外部存储访问,鸿蒙更严格——所有文件都在沙箱内;②鸿蒙的分布式文件目录(distributedFilesDir)自动跨设备同步,Android 无此能力;③鸿蒙的文件选择器是系统级 UI,Android 的 Storage Access Framework 类似;④鸿蒙沙箱路径固定格式,Android 因设备而异。
Q6: 如何在鸿蒙中实现 Token 自动刷新?
方案:在 HTTP 封装层拦截 401 响应 → 调用刷新 Token 接口 → 用新 Token 重试原请求。关键点:①并发请求时只触发一次刷新——用 Promise 缓存刷新请求,其他请求 await 同一个 Promise;②刷新失败则跳转登录页——清除 Token,通过 AppStorage 通知 UI;③刷新成功后重试原请求——保存原始请求参数,用新 Token 重新发起。实现:HttpClient 的 request 方法中,responseCode === 401 时调用 refreshToken(),刷新成功后递归调用 request 重试。
Q7: 鸿蒙如何检测网络状态?离线时如何处理?
使用 @kit.NetworkKit 的 connection 模块:getDefaultNet 获取当前网络,getNetCapabilities 获取能力(WiFi/蜂窝/是否有 Internet)。监听:connection.on(‘netAvailable’/‘netLost’/‘netCapabilitiesChange’) 实时感知网络变化。离线处理策略:①缓存优先——网络请求前检查网络状态,离线时从 RDB/Preferences 读取缓存;②队列机制——离线时的写操作存入队列,网络恢复后批量执行;③UI 提示——全局监听 netLost 事件,显示”网络已断开”提示条;④数据同步——在线时将本地修改同步到服务端。
Q8: 鸿蒙数据持久化和 Flutter 的数据持久化方案有什么异同?
异同对比:①KV 存储——鸿蒙 Preferences vs Flutter SharedPreferences,功能类似(get/put/flush),API 风格不同(鸿蒙 await 异步,Flutter 也是异步);②数据库——鸿蒙 RDB(系统内置 SQLite)vs Flutter sqflite/drift(第三方包),鸿蒙开箱即用,Flutter 需选库;③文件系统——鸿蒙沙箱目录明确(filesDir/cacheDir),Flutter 用 path_provider 获取;④安全存储——鸿蒙用 HUKS 密钥管理,Flutter 用 flutter_secure_storage;⑤分布式——鸿蒙内置分布式数据库自动跨设备同步,Flutter 无此能力。总体上鸿蒙的数据持久化更内建、更统一,Flutter 更灵活但需要选型。
相关链接: