鸿蒙数据持久化与网络

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:

维度鸿蒙FlutterReact Native
KV 存储PreferencesSharedPreferencesAsyncStorage
数据库RDB (SQLite)sqflite/driftWatermelonDB
网络请求@ohos.net.httpdio/httpaxios/fetch
WebSocket@ohos.net.webSocketweb_socket_channelws
类型安全强类型(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 更灵活但需要选型。


相关链接: