1. 引言:为什么选择Rawfile?

在HarmonyOS应用“面试通”中,常用单词模块需要预置一份完整的单词库数据。对于此类在应用生命周期内基本不变、无需修改的静态数据文件,系统提供了 Rawfile 目录进行存储。

相较于应用中的其他资源(如字符串、图片),Rawfile 目录中的文件在打包时不会被编译或压缩,开发者可以按原始格式直接读取其二进制内容,这为存储JSON、TXT或自定义格式的静态数据提供了极大的灵活性。

下图清晰地展示了在鸿蒙应用(特别是“面试通”项目)中,Rawfile 在整个资源体系中的定位和访问路径:

面试通应用
(常用单词模块)

资源访问决策

“需要动态修改的数据?”

使用应用数据库
或文件管理器

“需要国际化/适配的资源?”

使用“resources”目录
(字符串/图片/布局等)

“静态、原始的预置数据文件?”

“使用“rawfile”目录
(如:words.json)”

通过ResourceManager
获取Rawfile目录句柄

使用
ResourceManager.readRawFile

得到原始字节数据

选择解析方式

JSON/XML解析器

自定义文本/二进制解析

直接使用
(如音频文件)

“应用业务数据
(单词列表/释义/例句)”

可直接播放/展示的资源

渲染至ArkUI界面
(常用单词列表页)

在本例中,我们将遵循鸿蒙官方代码规范,使用ArkTS语言,一步步实现从rawfile中读取一个words.json文件,并将其解析、渲染到“常用单词”列表页面的完整过程。

2. 项目结构与数据准备

首先,在DevEco Studio的“面试通”项目工程中,我们按照规范放置资源文件。

目录结构:

src/main/
├── ets/
│   ├── pages/
│   │   └── CommonWordsPage.ets // 常用单词页面
│   └── utils/
│       └── RawfileParser.ets   // 封装的文件解析工具
└── resources/
    └── rawfile/
        ├── words.json          // 单词数据文件
        └── other_data.txt      // 其他可能的静态文件

words.json 数据文件示例:
这个文件模拟了“面试通”中可能预置的IT面试高频词汇。

{
  "version": "1.0",
  "categoryList": [
    {
      "id": 1,
      "name": "数据结构",
      "words": [
        {
          "id": "101",
          "english": "Algorithm",
          "chinese": "算法",
          "phonetic": "/ˈælɡərɪðəm/",
          "exampleSentence": "The efficiency of this algorithm is O(n log n)."
        },
        {
          "id": "102",
          "english": "Binary Tree",
          "chinese": "二叉树",
          "phonetic": "/ˈbaɪnəri triː/",
          "exampleSentence": "A binary tree is a hierarchical data structure."
        }
      ]
    },
    {
      "id": 2,
      "name": "网络协议",
      "words": [
        {
          "id": "201",
          "english": "HTTP",
          "chinese": "超文本传输协议",
          "phonetic": "/ˌeɪtʃ tiː tiː ˈpiː/",
          "exampleSentence": "HTTP is the foundation of data communication for the Web."
        }
      ]
    }
  ]
}

3. 核心实现:读取与解析

我们提倡关注点分离,将文件读取和解析的逻辑封装在独立的工具类中。

3.1 定义数据模型 (Data Model)
首先,在RawfileParser.ets或单独的文件中,使用TypeScript接口或类定义清晰的数据结构,这是ArkTS强类型优势的体现。

// 单词条目数据模型
export interface WordItem {
  id: string;
  english: string;
  chinese: string;
  phonetic: string;
  exampleSentence: string;
}

// 单词分类数据模型
export interface WordCategory {
  id: number;
  name: string;
  words: WordItem[];
}

// 单词库根数据模型
export interface WordLibrary {
  version: string;
  categoryList: WordCategory[];
}

3.2 封装Rawfile读取解析工具类
接下来,创建RawfileParser工具类。这里遵循鸿蒙API,使用ResourceManager来访问rawfile

// RawfileParser.ets
import common from '@ohos.app.ability.common';

export class RawfileParser {
  
  /**
   * 从rawfile中读取指定文件并解析为WordLibrary对象
   * @param context 应用上下文,通常从页面或Ability中获取
   * @param filename rawfile目录下的文件名,如 'words.json'
   * @returns 解析后的单词库数据 Promise
   */
  static async parseWordLibrary(context: common.Context, filename: string): Promise<WordLibrary> {
    try {
      // 1. 获取ResourceManager实例
      const resourceMgr = context.resourceManager;
      
      // 2. 读取Rawfile文件,获得原始字节数组 (Uint8Array)
      const rawFileData: Uint8Array = await resourceMgr.getRawFileContent(filename);
      
      // 3. 将Uint8Array转换为可操作的字符串
      // TextDecoder是Web标准API,在HarmonyOS中同样可用
      const dataString: string = new TextDecoder('utf-8').decode(rawFileData);
      
      // 4. 将JSON字符串解析为预定义的数据模型对象
      const wordLib: WordLibrary = JSON.parse(dataString) as WordLibrary;
      
      console.info(`[RawfileParser] Successfully parsed ${filename}, version: ${wordLib.version}`);
      return wordLib;
      
    } catch (error) {
      console.error(`[RawfileParser] Failed to parse ${filename}: ${JSON.stringify(error)}`);
      // 返回一个空结构或抛出错误,具体由业务逻辑决定
      throw new Error(`解析单词文件失败: ${error.message}`);
    }
  }

  /**
   * 辅助方法:获取rawfile中所有文件列表
   */
  static async getRawFileList(context: common.Context): Promise<Array<string>> {
    const resourceMgr = context.resourceManager;
    return await resourceMgr.getRawFileList();
  }
}

关键点说明:

  • 资源管理器 (ResourceManager): 是访问应用资源的统一入口。
  • 异步操作 (async/await):文件I/O是耗时操作,必须使用异步方式以避免阻塞UI线程。
  • 错误处理:使用try-catch包裹,对文件不存在、格式错误等情况进行妥善处理,增强应用健壮性。
  • 类型断言 (as WordLibrary):在JSON.parse后使用类型断言,让编译器知晓对象形状,便于后续的智能提示和类型检查。

4. 在ArkUI页面中使用

在“常用单词”页面 (CommonWordsPage.ets) 中,我们使用上述工具类来获取数据,并利用ArkUI的状态管理和声明式UI进行渲染。

4.1 页面状态与数据加载

// CommonWordsPage.ets
import { RawfileParser, WordCategory } from '../utils/RawfileParser';
import common from '@ohos.app.ability.common';

@Entry
@Component
struct CommonWordsPage {
  // 使用@State装饰器,使数据与UI联动
  @State categoryList: WordCategory[] = [];
  @State isLoading: boolean = true;
  @State loadError: string | null = null;

  // 获取UI上下文,用于访问资源
  private context: common.Context = getContext(this) as common.Context;

  // 在aboutToAppear生命周期中加载数据
  aboutToAppear() {
    this.loadWordData();
  }

  async loadWordData() {
    this.isLoading = true;
    this.loadError = null;
    try {
      const wordLib = await RawfileParser.parseWordLibrary(this.context, 'words.json');
      this.categoryList = wordLib.categoryList;
    } catch (error) {
      this.loadError = '加载单词数据失败,请重试。';
      console.error('Load word data failed:', error);
    } finally {
      this.isLoading = false;
    }
  }
}

4.2 声明式UI渲染
利用ArkUI的ListForEach等组件高效、灵活地构建列表界面。

// CommonWordsPage.ets (接上部分)
build() {
  Column() {
    // 顶部标题
    Text('常用单词')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 20, bottom: 20 });

    // 根据状态显示不同内容
    if (this.isLoading) {
      // 加载中状态
      ProgressIndicator()
        .color(Color.Blue)
        .width(50)
        .height(50);
      Text('正在加载单词库...').margin({ top: 10 });
    } else if (this.loadError) {
      // 错误状态
      Image($r('app.media.ic_error'))
        .width(60)
        .height(60);
      Text(this.loadError)
        .fontColor(Color.Red)
        .margin({ top: 10 });
      Button('重试')
        .onClick(() => this.loadWordData())
        .margin({ top: 20 });
    } else {
      // 正常列表状态
      List({ space: 15 }) {
        ForEach(this.categoryList, (category: WordCategory) => {
          ListItem() {
            // 每个分类可折叠的卡片
            WordCategoryCard({ category: category })
          }
        }, (category: WordCategory) => category.id.toString())
      }
      .scrollBar(BarState.Auto)
      .edgeEffect(EdgeEffect.Spring)
    }
  }
  .width('100%')
  .height('100%')
  .padding(20)
}

// 单词分类卡片组件
@Component
struct WordCategoryCard {
  @Prop category: WordCategory;
  @State isExpanded: boolean = false;

  build() {
    Column() {
      // 分类标题行,点击可展开/折叠
      Row() {
        Text(this.category.name)
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .flexGrow(1);
        Image(this.isExpanded ? $r('app.media.ic_collapse') : $r('app.media.ic_expand'))
          .width(20)
          .height(20);
      }
      .onClick(() => {
        this.isExpanded = !this.isExpanded;
      })
      .padding(15)
      .backgroundColor(Color.White)
      .borderRadius(8)
      .shadow({ radius: 4, color: '#CCC', offsetX: 1, offsetY: 1 })

      // 单词列表,展开时显示
      if (this.isExpanded) {
        Column() {
          ForEach(this.category.words, (word: WordItem) => {
            WordItemRow({ word: word })
          })
        }
        .padding({ left: 15, right: 15, bottom: 15 })
        .transition({ type: TransitionType.All, opacity: 0.99 })
      }
    }
  }
}

// 单词行组件
@Component
struct WordItemRow {
  @Prop word: WordItem;

  build() {
    Row() {
      Column({ space: 5 }) {
        // 英文单词和音标
        Row() {
          Text(this.word.english)
            .fontSize(16)
            .fontWeight(FontWeight.Bold);
          Text(`  [${this.word.phonetic}]`)
            .fontSize(14)
            .fontColor('#666');
        }
        // 中文释义
        Text(this.word.chinese)
          .fontSize(14)
          .fontColor(Color.Blue);
        // 例句
        Text(this.word.exampleSentence)
          .fontSize(13)
          .fontColor('#888')
          .margin({ top: 5 });
      }
      .flexGrow(1);
    }
    .padding(10)
    .borderRadius(5)
    .backgroundColor('#F9F9F9')
    .margin({ top: 8 })
  }
}

5. 效果对比、优势与最佳实践

通过以上实现,我们获得了清晰的代码结构和良好的用户体验。

5.1 效果对比:

对比项 传统硬编码方式 使用Rawfile解析方式
数据管理 数据混合在业务代码中,难以维护。 数据与代码分离,存放在独立的JSON文件中。
可维护性 修改单词需重新编译、发布整个应用。 热更新潜力,未来可设计为通过网络下载新JSON文件覆盖rawfile(需配合其他存储方式)。
协作性 非技术人员无法参与内容维护。 产品、运营人员可直接编辑JSON文件,参与内容生产。
类型安全 无类型检查,易出错。 强类型模型,编译时检查,编码时智能提示。

5.2 在“面试通”项目中的优势延伸:

  1. 模块化:单词模块独立,可轻松移植到其他学习类应用中。
  2. 可扩展性:JSON结构易于扩展,未来可轻松为单词添加“同义词”、“相关面试题”等字段。
  3. 性能:一次性读取解析,内存占用可控,列表渲染流畅。

5.3 鸿蒙开发最佳实践:

  • 路径规范:始终使用 ResourceManager API,而非硬编码文件路径,以保证兼容性。
  • 错误兜底:网络异常或文件损坏时,应有UI友好的错误提示和重试机制。
  • 大文件处理:如果Rawfile文件很大(如超过1MB),应考虑分片读取或使用流式处理,避免一次性加载导致内存压力。
  • 安全注意rawfile目录内容虽然不会编译,但会打包在HAP中,切勿存放敏感信息(如密钥、硬编码的个人信息)。

6. 总结

在鸿蒙“面试通”应用的开发中,通过利用rawfile目录存储静态的words.json数据文件,并结合ResourceManager进行读取和JSON.parse进行解析,我们成功实现了数据与UI逻辑的解耦。这种方法遵循了鸿蒙应用开发的资源管理规范,利用ArkTS的强类型特性保证了代码的健壮性,并通过ArkUI的声明式语法构建了高性能、可交互的单词列表界面。这种模式完全可以推广到应用的其他静态数据场景,如题库、城市列表、产品目录等,是构建可维护、可扩展HarmonyOS应用的重要基石。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐