CHAPTER 05 · 10

网络与数据持久化

掌握 HTTP 网络请求、关系型数据库 RDB、首选项 Preferences 与文件沙箱,构建离线可用的鸿蒙应用。

HarmonyOS 的网络请求

HarmonyOS NEXT 提供了 @ohos.net.http 模块作为原生 HTTP 客户端,同时也支持使用第三方库(如基于 Axios 封装的适配层)。发起网络请求前必须在 module.json5 中声明网络权限。

必须声明网络权限 在 module.json5 的 requestPermissions 中添加 "ohos.permission.INTERNET",否则网络请求会被系统拒绝,且不会有任何弹框提示。这是初学者最常见的坑。

基础 HTTP 请求

import http from '@ohos.net.http'

// 封装一个通用的 HTTP 客户端
class HttpClient {
  private baseUrl: string
  private headers: Record<string, string>

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
    this.headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    }
  }

  setToken(token: string) {
    this.headers['Authorization'] = `Bearer ${token}`
  }

  async get<T>(path: string): Promise<T> {
    // 每次请求创建一个新的 httpRequest 实例
    const request = http.createHttp()
    try {
      const response = await request.request(
        `${this.baseUrl}${path}`,
        {
          method: http.RequestMethod.GET,
          header: this.headers,
          connectTimeout: 10000,
          readTimeout: 10000
        }
      )
      if (response.responseCode !== 200) {
        throw new Error(`HTTP ${response.responseCode}`)
      }
      return JSON.parse(response.result as string) as T
    } finally {
      request.destroy()   // 必须销毁,防止内存泄漏
    }
  }

  async post<T>(path: string, body: object): Promise<T> {
    const request = http.createHttp()
    try {
      const response = await request.request(
        `${this.baseUrl}${path}`,
        {
          method: http.RequestMethod.POST,
          header: this.headers,
          extraData: JSON.stringify(body)
        }
      )
      return JSON.parse(response.result as string) as T
    } finally {
      request.destroy()
    }
  }
}

// 全局单例
export const apiClient = new HttpClient('https://api.example.com')

在组件中使用网络请求

interface Article {
  id: number
  title: string
  content: string
  createdAt: string
}

@Entry
@Component
struct ArticleListPage {
  @State articles: Article[] = []
  @State loading: boolean = false
  @State error: string = ''

  aboutToAppear() {
    this.loadArticles()
  }

  async loadArticles() {
    this.loading = true
    this.error = ''
    try {
      this.articles = await apiClient.get<Article[]>('/articles')
    } catch (e) {
      this.error = '加载失败,请重试'
    } finally {
      this.loading = false
    }
  }

  build() {
    Column() {
      if (this.loading) {
        LoadingProgress().width(40).height(40)
      } else if (this.error) {
        Text(this.error).fontColor('#CF0A2C')
        Button('重试').onClick(() => this.loadArticles())
      } else {
        List({ space: 12 }) {
          ForEach(this.articles, (article: Article) => {
            ListItem() {
              ArticleRow({ article: article })
            }
          }, (article: Article) => article.id.toString())
        }
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

关系型数据库 RDB

HarmonyOS NEXT 提供了 @ohos.data.relationalStore 模块,基于 SQLite 实现。适合存储结构化数据,支持 SQL 查询、事务、索引等完整数据库功能:

RdbStore
关系型数据库的操作句柄,通过 relationalStore.getRdbStore() 获取。一个应用通常只需要一个 RdbStore 实例(单例),在整个应用生命周期内复用。
ValuesBucket
插入或更新数据时使用的键值对对象,相当于一行数据。键是列名(string),值是对应的数据(number | string | boolean | Uint8Array)。
RdbPredicates
查询条件构建器,用链式方法描述 WHERE 子句、排序、分页等。避免手写 SQL 字符串拼接,防止 SQL 注入。
import { relationalStore } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

// 数据库管理类(单例模式)
class DatabaseManager {
  private static instance: DatabaseManager
  private rdbStore: relationalStore.RdbStore | null = null

  static getInstance(): DatabaseManager {
    if (!DatabaseManager.instance) {
      DatabaseManager.instance = new DatabaseManager()
    }
    return DatabaseManager.instance
  }

  // 初始化数据库(建表)
  async init(context: common.UIAbilityContext) {
    const config: relationalStore.StoreConfig = {
      name: 'AppDatabase.db',   // 数据库文件名
      securityLevel: relationalStore.SecurityLevel.S1
    }

    // 建表 SQL(如果表不存在则创建)
    const createArticleTable = `
      CREATE TABLE IF NOT EXISTS articles (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        content TEXT,
        created_at INTEGER DEFAULT (strftime('%s', 'now')),
        is_favorite INTEGER DEFAULT 0
      )
    `

    this.rdbStore = await relationalStore.getRdbStore(context, config)
    await this.rdbStore.executeSql(createArticleTable)
    console.log('数据库初始化完成')
  }

  // 插入文章
  async insertArticle(title: string, content: string): Promise<number> {
    const bucket: relationalStore.ValuesBucket = {
      title: title,
      content: content,
      is_favorite: 0
    }
    return await this.rdbStore!.insert('articles', bucket)
  }

  // 查询所有文章
  async getAllArticles(): Promise<Article[]> {
    const predicates = new relationalStore.RdbPredicates('articles')
    predicates.orderByDesc('created_at')   // 按时间倒序
    predicates.limitAs(50)                 // 最多 50 条

    const cursor = await this.rdbStore!.query(predicates, ['id', 'title', 'content', 'created_at'])
    const articles: Article[] = []

    while (cursor.goToNextRow()) {
      articles.push({
        id: cursor.getLong(cursor.getColumnIndex('id')),
        title: cursor.getString(cursor.getColumnIndex('title')),
        content: cursor.getString(cursor.getColumnIndex('content')),
        createdAt: new Date(cursor.getLong(cursor.getColumnIndex('created_at')) * 1000).toISOString()
      })
    }
    cursor.close()    // 必须关闭游标,防止内存泄漏
    return articles
  }

  // 条件查询:搜索标题
  async searchArticles(keyword: string): Promise<Article[]> {
    const predicates = new relationalStore.RdbPredicates('articles')
    predicates.contains('title', keyword)  // LIKE '%keyword%'

    const cursor = await this.rdbStore!.query(predicates)
    // ... 同样的遍历逻辑
    cursor.close()
    return []
  }

  // 更新(切换收藏状态)
  async toggleFavorite(articleId: number, isFavorite: boolean) {
    const predicates = new relationalStore.RdbPredicates('articles')
    predicates.equalTo('id', articleId)

    await this.rdbStore!.update({ is_favorite: isFavorite ? 1 : 0 }, predicates)
  }

  // 删除
  async deleteArticle(articleId: number) {
    const predicates = new relationalStore.RdbPredicates('articles')
    predicates.equalTo('id', articleId)
    await this.rdbStore!.delete(predicates)
  }
}

export const db = DatabaseManager.getInstance()

Preferences — 轻量键值存储

@ohos.data.preferences 提供轻量级键值存储,适合保存配置项、用户偏好等简单数据。相比 RDB,它不支持复杂查询,但读写更快,API 更简单。(注意与第3章的 PersistentStorage 的区别:Preferences 是纯文件级 API,更底层;PersistentStorage 是 ArkUI 框架层的封装,与组件状态深度集成。)

import preferences from '@ohos.data.preferences'

class UserPrefs {
  private store: preferences.Preferences | null = null

  async init(context: Context) {
    this.store = await preferences.getPreferences(context, 'user_prefs')
  }

  // 设置值(支持 string / number / boolean / string[] / number[])
  async set(key: string, value: preferences.ValueType) {
    await this.store!.put(key, value)
    await this.store!.flush()   // 将内存数据持久化到磁盘(异步)
  }

  // 读取值,提供默认值
  get<T extends preferences.ValueType>(key: string, defaultValue: T): T {
    return this.store!.getSync(key, defaultValue) as T
  }

  // 监听数据变化(实时更新 UI)
  onChange(key: string, callback: () => void) {
    this.store!.on('change', (changedKey: string) => {
      if (changedKey === key) callback()
    })
  }
}

// 使用示例
const prefs = new UserPrefs()
await prefs.init(context)

// 保存用户 token
await prefs.set('auth_token', 'eyJhbGciOiJIUzI1NiJ9...')

// 读取(同步,适合组件初始化时读取)
const token = prefs.get('auth_token', '')

文件操作与沙箱

HarmonyOS NEXT 采用严格的沙箱机制:每个应用只能访问自己沙箱目录内的文件,无法直接读写其他应用的文件或系统文件。理解应用沙箱结构对文件存储至关重要:

应用沙箱目录结构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /data/storage/el2/base/ (应用私有目录,加密保护) ├── files/ ← context.filesDir,应用文件 ├── database/ ← RDB 数据库文件存放位置 ├── preferences/ ← Preferences 文件存放位置 └── cache/ ← context.cacheDir,缓存文件(可被系统清理) /data/storage/el1/base/ (设备级存储,重启保留) └── files/ ← 适合重要但不加密的数据 说明: • el1:设备重启后可用,但不加密 • el2:默认级别,用户解锁后才可访问,加密保护
import fs from '@ohos.file.fs'

class FileManager {
  private filesDir: string
  private cacheDir: string

  constructor(context: Context) {
    this.filesDir = context.filesDir
    this.cacheDir = context.cacheDir
  }

  // 写入文本文件
  async writeText(filename: string, content: string) {
    const path = `${this.filesDir}/${filename}`
    const file = await fs.open(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
    await fs.write(file.fd, content)
    await fs.close(file.fd)
  }

  // 读取文本文件
  async readText(filename: string): Promise<string> {
    const path = `${this.filesDir}/${filename}`
    try {
      const file = await fs.open(path, fs.OpenMode.READ_ONLY)
      const stat = await fs.stat(path)
      const buffer = new ArrayBuffer(stat.size)
      await fs.read(file.fd, buffer)
      await fs.close(file.fd)
      return new TextDecoder().decode(buffer)
    } catch (e) {
      return ''  // 文件不存在返回空
    }
  }

  // 保存图片到缓存(如网络下载的封面图)
  async saveImageToCache(filename: string, data: ArrayBuffer) {
    const path = `${this.cacheDir}/${filename}`
    const file = await fs.open(path, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE)
    await fs.write(file.fd, data)
    await fs.close(file.fd)
    return path   // 返回可在 Image 组件中使用的本地路径
  }

  // 列出目录下所有文件
  async listFiles(dir: string = this.filesDir): Promise<string[]> {
    const entries = await fs.listFile(dir)
    return entries
  }
}

数据层架构建议

一个健壮的数据层应该将网络请求、本地存储和业务逻辑分离,遵循仓库模式(Repository Pattern):

推荐数据层架构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ UI 组件(.ets) │ 调用 ▼ ViewModel / Service(业务逻辑层) │ 调用 ▼ Repository(数据仓库层) ├── 网络请求优先 → ApiClient → 服务端 └── 失败时读本地 → RdbStore / Preferences 本地有缓存 ────────────────────── 原则: • UI 只关心"数据是什么",不关心"数据从哪来" • Repository 决定从网络还是本地读数据 • 网络成功后写入本地缓存,实现离线可用
选型建议