记事本鸿蒙应用 — 技术文档


一、项目概述

1.1 项目背景

华为鸿蒙操作系统(HarmonyOS)自诞生以来,经历了多个版本的迭代演进,从最初的智慧屏场景扩展到如今的手机、平板、PC、智能穿戴、车机等全场景覆盖。随着 HarmonyOS NEXT 版本的发布,系统底座全线自研,去除了 AOSP 的依赖,标志着鸿蒙已经迈入完全独立自主的发展新阶段。在这个关键的时间节点上,构建丰富的原生鸿蒙应用生态成为当务之急。

记事本应用作为移动操作系统中最基础也是最高频使用的工具类应用之一,承载着用户日常记录灵感、整理知识、管理待办事项、撰写日记等多样化需求。一款优秀的记事本应用不仅需要有流畅的编辑体验和可靠的数据存储,还需要在隐私保护、多设备协同、离线可用性等方面满足用户的深层次期望。

基于上述背景,本项目选择原生鸿蒙技术栈,利用 ArkTS 语言与 ArkUI 框架,从零构建一款面向鸿蒙生态的记事本应用。项目不仅追求功能的完整性,更注重代码架构的合理性和技术方案的前瞻性,力求为鸿蒙开发者社区提供一个高质量的技术参考范本。

1.2 项目目标

本项目在设计和实现层面确立了以下核心目标:

  • 原生适配鸿蒙生态:基于 ArkTS 语言和 ArkUI 框架进行开发,充分利用 HarmonyOS 的分布式能力与系统 API,不依赖任何跨平台桥接技术,确保应用在鸿蒙设备上获得最佳的原生性能表现。
  • 完善的核心功能闭环:提供笔记的创建、编辑、保存、删除、搜索、分类等完整的生命周期管理能力,满足用户日常记事的全部基本需求。
  • 高效可靠的本地数据持久化:基于鸿蒙关系型数据库(RDB)实现结构化数据存储,确保数据写入的原子性、一致性和持久性,同时通过软删除机制为用户提供误删除恢复的可能性。
  • 流畅直观的用户体验:借助 ArkUI 声明式 UI 框架的响应式数据绑定能力和系统级的动画框架,实现页面切换无缝衔接、列表滚动丝滑流畅、操作反馈及时自然。
  • 隐私安全优先:所有数据默认存储在本地设备端,不强制用户注册账号或开启云同步,从根本上保障用户数据的隐私安全。
  • 良好的可扩展性:采用 MVVM 分层架构,模块间低耦合高内聚,为后续功能的迭代扩展(如云同步、语音笔记、图片附件、Markdown 渲染、标签分类等)预留清晰的扩展点。

二、技术架构

2.1 整体架构设计

本应用采用 MVVM(Model-View-ViewModel) 架构模式,这是鸿蒙官方推荐的应用架构方案之一。MVVM 模式的核心价值在于通过数据驱动的方式实现视图与业务逻辑的解耦,使代码结构清晰、职责分明、易于测试和维护。

架构共分为三个层级,自顶向下分别是:

第一层:View 层(视图层)

View 层由 ArkUI 的 @Component 自定义组件构成,负责页面结构的搭建、用户交互事件的响应以及数据的可视化呈现。View 层通过 @State@Link@Prop 等装饰器与 ViewModel 层建立响应式数据绑定,当 ViewModel 中的数据状态发生变化时,View 层会自动触发重新渲染,无需开发者手动操作 DOM。

View 层不包含任何业务逻辑代码,所有用户操作均通过事件回调委托给 ViewModel 层处理。这种设计使得 View 层可以专注于 UI 表现,当需要修改界面样式或布局时,开发者只需调整 View 层的组件代码,而不必关心业务逻辑的实现细节。

第二层:ViewModel 层(视图模型层)

ViewModel 层是 View 层与 Model 层之间的桥梁,负责封装与视图直接相关的业务逻辑和状态管理。每个页面通常对应一个 ViewModel 实例,例如笔记列表页对应 NoteListViewModel,编辑页对应 NoteEditorViewModel

ViewModel 持有从 Model 层获取的数据,并将其转换为 View 层可以直接消费的状态对象。当 ViewModel 中的数据发生变化时,通过 @Observed 装饰器或自定义的发布-订阅机制通知 View 层更新。ViewModel 同时负责处理用户操作的业务流程编排,例如点击保存按钮时需要依次执行数据校验、格式转换、数据库写入等操作。

第三层:Model 层(数据模型层)

Model 层是应用的"数据心脏",由实体定义(Entity)、数据仓库(Repository)和数据源(DataSource)三部分组成。Entity 定义数据的结构模型;Repository 封装数据操作的具体逻辑,屏蔽底层数据源的差异;DataSource 对接鸿蒙关系型数据库(RDB)和键值型数据库(KVStore),执行具体的 CRUD 操作。

Model 层向上层提供清晰的数据操作接口,ViewModel 层通过调用 Repository 的方法获取或操作数据,而不需要关心数据是来自本地数据库还是远程服务器。这种抽象使得替换或扩展数据源变得非常容易。

┌──────────────────────────────────────────────────────────────┐
│                      View Layer (ArkUI)                      │
│  ┌─────────────────┐  ┌────────────────┐  ┌──────────────┐  │
│  │  NoteListPage    │  │  NoteEditorPage │  │  SearchPage  │  │
│  │  @State notes[]  │  │  @State title   │  │  @State      │  │
│  │  @State loading  │  │  @State content │  │  query text  │  │
│  └────────┬────────┘  └───────┬────────┘  └──────┬───────┘  │
├───────────┼───────────────────┼──────────────────┼──────────┤
│           │      ViewModel Layer                          │
│  ┌────────┴────────┐  ┌───────┴────────┐  ┌──────┴───────┐│
│  │ NoteListVM      │  │ NoteEditorVM   │  │ SearchVM     ││
│  │ loadNotes()     │  │ saveNote()     │  │ search(key)  ││
│  │ deleteNote()    │  │ autoSave()     │  │ recentSearch ││
│  │ toggleFavorite()│  │ formatText()   │  │ clearHistory ││
│  └────────┬────────┘  └───────┬────────┘  └──────┬───────┘│
├───────────┼───────────────────┼──────────────────┼──────────┤
│           │      Model Layer                             │
│  ┌────────┴──────────────────────────────────────────────┐│
│  │                 NoteRepository                        ││
│  │  + getAll(): Promise<NoteEntity[]>                    ││
│  │  + getById(id): Promise<NoteEntity | null>            ││
│  │  + insert(note): Promise<number>                      ││
│  │  + update(note): Promise<number>                      ││
│  │  + delete(id): Promise<void>                          ││
│  │  + search(keyword): Promise<NoteEntity[]>             ││
│  └───────────────────────┬──────────────────────────────┘│
│                          │                                │
│  ┌───────────────────────┴──────────────────────────────┐│
│  │              DataSource (RDB / KVStore)              ││
│  │  relationalStore.getRdbStore()                       ││
│  │  kvStore.getKVStore()                                ││
│  └──────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────┘

2.2 关键技术选型详解

2.2.1 开发语言:ArkTS

ArkTS 是鸿蒙生态的原生应用开发语言,它在 TypeScript 的基础上进行了深度扩展,增加了用于声明式 UI 编程的语法特性。ArkTS 保留了 TypeScript 的类型系统,确保在编译阶段就能发现大部分类型相关的错误,同时通过 AOT(Ahead-of-Time)编译技术将代码编译为高效的机器码,在运行时获得接近原生语言的执行性能。

ArkTS 的静态类型检查机制是其最重要的质量保障手段之一。开发者在编码过程中,IDE 就能实时提示类型不匹配、空值引用、未定义属性等问题,大幅降低了运行时异常发生的概率。此外,ArkTS 对 nullundefined 的严格管控也有效规避了 JavaScript 中经典的空指针问题。

2.2.2 UI 框架:ArkUI

ArkUI 是鸿蒙原生的声明式 UI 框架,其核心理念是"数据驱动 UI"。开发者只需描述界面在不同状态下的呈现形式,框架自动跟踪数据变化并高效更新 UI。这种编程范式相比传统的命令式 UI 编程,代码量更少、逻辑更清晰、维护成本更低。

ArkUI 的响应式系统通过一组装饰器实现:

  • @State:标记组件内部的可变状态,当状态值发生变化时,依赖该状态的 UI 部分自动重新渲染。@State 是 ArkUI 响应式系统中最基础的装饰器,适用于组件内部的局部状态管理。
  • @Link:建立父子组件之间的双向数据绑定,父组件中的状态变化会自动同步到子组件,子组件中的修改也会反映到父组件。@Link 适用于需要跨组件共享状态的场景。
  • @Prop:实现父组件到子组件的单向数据传递,子组件可以读取和修改 Prop 的值,但修改不会影响父组件的原始数据。@Prop 适用于父子组件之间的一次性数据初始化。
  • @Observed 与 @ObjectLink:用于深度观察对象的属性变化。当被 @Observed 装饰的类实例中的属性发生变化时,所有使用 @ObjectLink 绑定该实例的组件都会收到通知并重新渲染。这对于复杂对象的深层属性变化监控非常有用。
  • @Provide 与 @Consume:实现跨多层组件的依赖注入,无需逐层传递 Props。@Provide 在祖先组件中提供数据,@Consume 在后代组件中注入数据,形成一个响应式的数据供应链。
2.2.3 数据持久化方案

数据持久化是本项目的核心模块之一,针对不同场景的数据存储需求,设计了差异化的存储方案:

存储类型 技术方案 适用场景
结构化数据 关系型数据库(RDB) 笔记记录的存储、查询、排序、全文搜索
配置数据 键值型数据库(KVStore) 应用设置、用户偏好、搜索历史
临时数据 AppStorage / LocalStorage 页面间状态共享、临时缓存

RDB 基于 SQLite 引擎提供完整的关系型数据库能力,支持事务操作、复杂查询、索引优化等特性。对于笔记类应用来说,RDB 的优势在于:支持通过 SQL 语句进行灵活的数据查询和聚合操作,可以利用索引加速搜索效率,能够通过事务机制保证数据操作的一致性。

KVStore 以键值对的形式存储数据,适用于轻量级的配置存储场景,例如用户选择的主题颜色、笔记列表的排序方式、是否开启自动保存、搜索历史记录等。KVStore 的读写速度极快,在存储小体积数据时性能优于 RDB。

2.2.4 状态管理与数据流

应用的状态管理采用了分层递进的策略:

  • 局部状态:使用 @State 管理组件内部的 UI 状态,如输入框的文本内容、弹窗的显示状态、列表的加载动画等。
  • 页面状态:使用 ViewModel 类持有页面的业务状态数据,ViewModel 中的属性通过 @Observed 装饰器实现响应式跟踪。
  • 全局状态:使用 AppStorage 管理应用级别的共享状态,如用户登录状态、全局主题设置等。AppStorage 是鸿蒙 UI 框架提供的单例存储对象,所有组件都可以直接访问。

数据流遵循单向流动原则:

  1. View 层触发事件(用户操作)
  2. ViewModel 层处理事件并调用 Repository
  3. Repository 操作 DataSource 并返回结果
  4. ViewModel 更新状态数据
  5. View 层自动响应状态变化,重新渲染 UI

这种单向数据流的设计使得数据变化的链条清晰可追溯,避免了双向绑定可能导致的级联更新和难以调试的数据混乱问题。

2.3 ArkTS 核心特性详析

1. 严格的类型系统

ArkTS 在 TypeScript 的基础上进一步收紧了类型规则,禁止使用 any 类型,要求所有变量和函数参数都必须具有明确的类型注解。这一特性看似增加了编码时的约束,但实际上极大地提升了代码的健壮性和可维护性。

interface NoteData {
  id: number;
  title: string;
  content: string;
  createdAt: number;
  updatedAt: number;
  isFavorite: boolean;
}

2. 声明式 UI 语法

ArkUI 的声明式 UI 语法允许开发者以函数式的方式描述界面结构。通过 @Builder 装饰器可以定义可复用的 UI 片段,通过 @Component 可以将一组 UI 组件封装为独立的自定义组件。结合 @State@Prop@Link 等装饰器,组件能够以极少的代码量实现复杂的响应式交互。

3. 并发编程模型

ArkTS 提供了 TaskPoolWorker 两种多线程并发方案:

  • TaskPool:适用于短时间、独立的任务,由系统管理的线程池自动调度和执行。TaskPool 的 API 简洁,使用 Promise 风格,适合执行数据库读写、文本解析等计算密集型操作,执行完毕后自动回收线程资源。
  • Worker:适用于长时间运行的后台任务,开发者需要手动创建和管理 Worker 线程的生命周期。Worker 适合处理持续性的计算任务,如实时音频处理、后台数据同步等。

在本项目中,数据库的写入操作和大文本的处理均通过 TaskPool 异步执行,确保耗时操作不会阻塞 UI 线程。

import { taskpool } from '@kit.ArkTS';

@Concurrent
async function saveNoteToDb(noteData: NoteData): Promise<number> {
  const store = await getRdbStore();
  const row = { title: noteData.title, content: noteData.content, content_text: noteData.content.replace(/<[^>]+>/g, ''), updated_at: Date.now() };
  return (await store.update(row, [noteData.id])).rowCount;
}

// 主线程调用
const task = new taskpool.Task(saveNoteToDb, note);
const affectedRows = await taskpool.execute(task) as number;

三、核心功能模块详细设计

3.1 笔记管理模块

笔记管理模块是整个应用的功能核心,覆盖了笔记从创建到删除的完整生命周期。

3.1.1 创建笔记

用户打开应用后,点击悬浮按钮或菜单中的"新建笔记"入口,即可进入编辑器页面创建新笔记。系统默认以当前时间作为笔记的初始标题(格式为"YYYY年MM月DD日的笔记"),用户可自行修改。编辑器自动获取焦点,方便用户立即开始记录。

新建笔记的完整流程如下:

  1. 用户在列表页点击"新建"按钮
  2. 路由跳转到编辑器页面,传递 mode=create 参数
  3. 编辑器页面初始化 ViewModel,创建一个空的 NoteEntity 实例
  4. ViewModel 调用 Repository.insert() 在数据库中创建一条空笔记记录,获得该笔记的唯一 ID
  5. 用户在编辑器中进行内容编辑
  6. 用户主动保存或触发自动保存机制,ViewModel 调用 Repository.update() 更新笔记内容
3.1.2 编辑笔记

编辑笔记支持完整的富文本编辑能力,编辑器组件由 ArkUI 的 TextArea 和自定义富文本工具栏组合而成。

编辑器的自动保存策略采用防抖(Debounce)机制:

class NoteEditorVM {
  private autoSaveTimer: number | null = null;
  private readonly AUTO_SAVE_DELAY_MS = 2000; // 2秒防抖

  onContentChanged(newContent: string): void {
    this.currentNote.content = newContent;

    // 清除上一次的定时器
    if (this.autoSaveTimer !== null) {
      clearTimeout(this.autoSaveTimer);
    }

    // 设置新的定时器
    this.autoSaveTimer = setTimeout(() => {
      this.saveToDatabase();
    }, this.AUTO_SAVE_DELAY_MS);
  }

  private async saveToDatabase(): Promise<void> {
    try {
      this.currentNote.updatedAt = Date.now();
      this.currentNote.contentText = this.stripHtml(this.currentNote.content);
      await NoteRepository.update(this.currentNote);
      console.info('自动保存成功');
    } catch (error) {
      console.error('自动保存失败:', error);
    }
  }

  private stripHtml(html: string): string {
    return html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
  }
}

防抖机制的核心思想是:在用户连续输入的过程中,不立即执行保存操作,而是等待用户停止输入一段时间后再执行。这样可以有效减少数据库写入次数,避免频繁的磁盘 I/O 操作,同时降低功耗。2 秒的延迟时间是经过实际测试后选择的平衡点,既不会让用户感觉到保存延迟,又能显著降低写入频率。

3.1.3 删除笔记

删除操作采用软删除策略,即不从数据库中物理删除记录,而是将 is_deleted 字段标记为 1。软删除的优势在于:

  • 误删恢复能力:用户删除的笔记可以放入"回收站",在一定期限内可以恢复
  • 数据安全:防止因程序异常导致的不可逆数据丢失
  • 操作可逆:为后续增加"最近删除"功能预留数据结构基础
async function softDeleteNote(noteId: number): Promise<void> {
  const store = await getRdbStore();
  const row = { is_deleted: 1, updated_at: Date.now() };
  const predicates = new relationalStore.RdbPredicates('note');
  predicates.equalTo('id', noteId);
  await store.update(row, predicates);
}

列表页默认查询 is_deleted = 0 的数据,构建主列表视图。回收站页面则展示 is_deleted = 1 的笔记,并提供恢复和永久删除两个操作。

3.1.4 笔记分类与排序

笔记列表支持多种维度的分类和排序方式:

排序方式 实现逻辑
按更新时间倒序(默认) ORDER BY updated_at DESC
按创建时间倒序 ORDER BY created_at DESC
按标题字母序 ORDER BY title ASC
仅显示收藏笔记 WHERE is_favorite = 1
按创建日期分组 根据 created_at 日期部分做 GROUP BY

日期分组功能在 UI 上表现为"今天"、“昨天”、“本周”、"更早"等分组标题,帮助用户快速定位到特定时间范围内的笔记。这一效果通过在后端查询时计算时间差并在前端进行分组渲染来实现。

3.1.5 笔记搜索

搜索功能是笔记管理模块的重要补充,支持基于标题和全文内容的实时搜索。

搜索功能的实现策略:

class SearchVM {
  private allNotes: NoteEntity[] = [];
  searchResults: NoteEntity[] = [];
  recentSearches: string[] = [];
  isLoading: boolean = false;

  async initialize(): Promise<void> {
    this.allNotes = await NoteRepository.getAll();
    this.recentSearches = await KVStoreUtil.getObject<string[]>('recent_searches', []);
  }

  async search(keyword: string): Promise<void> {
    if (keyword.trim().length === 0) { this.searchResults = []; return; }
    this.isLoading = true;
    const lowerKeyword = keyword.toLowerCase();
    this.searchResults = this.allNotes.filter(note =>
      note.title.toLowerCase().includes(lowerKeyword) ||
      note.contentText.toLowerCase().includes(lowerKeyword)
    );
    this.searchResults.sort((a, b) => {
      const aMatch = a.title.toLowerCase().includes(lowerKeyword);
      const bMatch = b.title.toLowerCase().includes(lowerKeyword);
      if (aMatch && !bMatch) return -1;
      if (!aMatch && bMatch) return 1;
      return b.updatedAt - a.updatedAt;
    });
    this.saveSearchHistory(keyword);
    this.isLoading = false;
  }

  private saveSearchHistory(keyword: string): void {
    this.recentSearches = this.recentSearches.filter(s => s !== keyword);
    this.recentSearches.unshift(keyword);
    if (this.recentSearches.length > 10) this.recentSearches.pop();
    KVStoreUtil.putObject('recent_searches', this.recentSearches);
  }
}

搜索结果的排序策略中,标题匹配的结果优先于内容匹配的结果,因为标题通常更能概括笔记的核心主题。同时,在内容匹配的结果中,更新时间最近的笔记排在前面,确保用户最可能需要的结果出现在列表顶部。

3.2 富文本编辑模块

富文本编辑是记事本应用的核心竞争力之一,直接影响用户的输入体验和笔记的表达能力。

3.2.1 富文本格式支持

本应用支持以下富文本格式:

  • 文字格式:加粗(Bold)、斜体(Italic)、下划线(Underline)、删除线(Strikethrough)
  • 段落格式:无序列表(Bullet List)、有序列表(Ordered List)、引用块(Blockquote)
  • 分隔元素:水平分隔线(Horizontal Rule)
  • 标题层级:H1、H2、H3 三级标题

富文本的底层存储格式采用自定义的简易标记语法,而非直接存储 HTML,以降低存储和解析的开销:

// 富文本标记语法示例
// *这是加粗的文字* → 加粗
// _这是斜体的文字_ → 斜体
// ~这是删除线的文字~ → 删除线
// - 列表项 → 无序列表
// 1. 列表项 → 有序列表
// ## 标题文字 → 二级标题

class RichTextParser {
  static parseToDisplay(text: string): RichTextSegment[] {
    const segments: RichTextSegment[] = [];
    const lines = text.split('\n');

    for (const line of lines) {
      if (line.startsWith('## ')) {
        segments.push({ type: 'heading', level: 2, text: line.substring(3) });
      } else if (line.startsWith('- ')) {
        segments.push({ type: 'bullet', text: line.substring(2) });
      } else if (/^\d+\.\s/.test(line)) {
        segments.push({ type: 'ordered', text: line.replace(/^\d+\.\s/, '') });
      } else if (line === '---') {
        segments.push({ type: 'divider' });
      } else {
        segments.push({ type: 'paragraph', text: line });
      }
    }
    return segments;
  }

  static parseInlineFormatting(text: string): InlineSegment[] {
    const inlineSegments: InlineSegment[] = [];
    // 正则匹配加粗、斜体、删除线等行内格式
    const regex = /(\*[^*]+\*)|(_[^_]+_)|(~[^~]+~)/g;
    let lastIndex = 0;
    let match: RegExpExecArray | null;

    while ((match = regex.exec(text)) !== null) {
      if (match.index > lastIndex) {
        inlineSegments.push({ type: 'plain', text: text.substring(lastIndex, match.index) });
      }

      const matchedText = match[0];
      if (matchedText.startsWith('*') && matchedText.endsWith('*') && !matchedText.startsWith('**')) {
        inlineSegments.push({ type: 'bold', text: matchedText.slice(1, -1) });
      } else if (matchedText.startsWith('_')) {
        inlineSegments.push({ type: 'italic', text: matchedText.slice(1, -1) });
      } else if (matchedText.startsWith('~')) {
        inlineSegments.push({ type: 'strikethrough', text: matchedText.slice(1, -1) });
      }
      lastIndex = regex.lastIndex;
    }

    if (lastIndex < text.length) {
      inlineSegments.push({ type: 'plain', text: text.substring(lastIndex) });
    }
    return inlineSegments;
  }
}
3.2.2 编辑器工具栏

编辑器底部的工具栏提供了格式切换按钮,用户可以通过点击按钮为选中的文本或当前段落应用格式。工具栏的设计遵循以下原则:

  • 上下文感知:根据当前光标所在位置的格式状态,自动高亮对应的格式按钮
  • 即时反馈:点击格式按钮后,编辑区立即反映格式变化,无视觉延迟
  • 触摸友好:按钮尺寸不小于 44×44 点,间距充足,避免误触

工具栏组件采用响应式布局,在横竖屏切换时自动调整按钮排列方式。当空间不足时,部分不常用的格式按钮会被折叠到"更多"菜单中。

3.2.3 字数统计

字数统计是笔记类应用的基础功能,实时显示当前笔记的字符数、字数和行数。ArkTS 中通过字符串的 length 属性获取字符数(中英文均按一个字符计算),通过 split('\n') 计算行数。字数统计在编辑器的标题栏区域以小号文字显示,不影响用户的编辑视线。

function getStats(text: string): { chars: number; lines: number } {
  return {
    chars: text.length,
    lines: text.split('\n').length
  };
}

3.3 数据持久化模块

3.3.1 数据库 Schema 设计

数据库是记事本应用的存储基石,表结构设计直接关系到应用的查询性能和数据管理能力。本应用采用单表设计,将所有笔记存储在同一张表中,通过字段标记来区分不同的数据状态。

-- 笔记数据表完整 DDL
CREATE TABLE IF NOT EXISTS note (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    title       TEXT    NOT NULL DEFAULT '',
    content     TEXT    DEFAULT '',
    content_text TEXT   DEFAULT '',
    created_at  INTEGER NOT NULL,
    updated_at  INTEGER NOT NULL,
    is_favorite INTEGER DEFAULT 0,
    is_deleted  INTEGER DEFAULT 0,
    color       INTEGER DEFAULT 0,
    tag         TEXT    DEFAULT ''
);

-- 索引设计:加速查询性能
CREATE INDEX IF NOT EXISTS idx_note_updated_at ON note(updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_note_created_at ON note(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_note_is_deleted ON note(is_deleted);
CREATE INDEX IF NOT EXISTS idx_note_is_favorite ON note(is_favorite);
CREATE INDEX IF NOT EXISTS idx_note_title ON note(title);

各字段设计说明:

  • id:自增主键,每条笔记的唯一标识符。AUTOINCREMENT 确保 ID 连续且不重复,即使记录被永久删除也不会复用已删除的 ID。
  • title:笔记标题,不允许为空。即使创建笔记时用户没有输入标题,系统也会自动生成一个基于时间的默认标题。
  • content:笔记正文,采用自定义的富文本标记语法存储(非 HTML),以降低存储空间和解析复杂度。
  • content_text:纯文本版本的正文内容,去除所有格式标记。这个字段专为全文搜索设计,避免在搜索时对每条记录的 content 字段进行实时解析。
  • created_at / updated_at:Unix 毫秒时间戳,分别记录笔记的创建时间和最后修改时间。
  • is_favorite:收藏标记,0 表示未收藏,1 表示已收藏。
  • is_deleted:软删除标记,0 表示正常,1 表示已删除。
  • color:笔记颜色标记,用于 UI 层面的视觉分类(预留字段,对应不同的颜色标签)。
  • tag:笔记标签,用于分类管理(预留字段)。
3.3.2 数据库初始化与管理

数据库的初始化在应用启动时完成,通过 relationalStore.getRdbStore() 接口获取或创建数据库实例:

import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';

class DatabaseHelper {
  private static readonly DATABASE_NAME = 'NoteApp.db';
  private static readonly DATABASE_VERSION = 1;
  private static store: relationalStore.RdbStore | null = null;

  static async getStore(): Promise<relationalStore.RdbStore> {
    if (this.store !== null) {
      return this.store;
    }

    const config: relationalStore.StoreConfig = {
      name: this.DATABASE_NAME,
      securityLevel: relationalStore.SecurityLevel.S1,
      encrypt: false
    };

    try {
      this.store = await relationalStore.getRdbStore(getContext(), config);
      await this.createTable();
      return this.store;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`数据库初始化失败: code=${error.code}, message=${error.message}`);
      throw error;
    }
  }

  private static async createTable(): Promise<void> {
    const createTableSql = `
      CREATE TABLE IF NOT EXISTS note (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        title       TEXT NOT NULL DEFAULT '',
        content     TEXT DEFAULT '',
        content_text TEXT DEFAULT '',
        created_at  INTEGER NOT NULL,
        updated_at  INTEGER NOT NULL,
        is_favorite INTEGER DEFAULT 0,
        is_deleted  INTEGER DEFAULT 0,
        color       INTEGER DEFAULT 0,
        tag         TEXT DEFAULT ''
      )
    `;

    const createIndexSql = `
      CREATE INDEX IF NOT EXISTS idx_note_updated_at ON note(updated_at DESC)
    `;

    try {
      const store = this.store!;
      await store.executeSql(createTableSql);
      await store.executeSql(createIndexSql);
      console.info('数据库表与索引创建成功');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`数据库表创建失败: ${error.message}`);
      throw error;
    }
  }
}

在数据库的安全性配置方面,securityLevel 参数设置为 SecurityLevel.S1(普通级别),适用于不包含敏感个人数据的应用场景。如果后续应用中增加了用户认证、加密笔记等功能,可以升级安全级别或启用数据库加密。

3.3.3 数据仓库 Repository 层

Repository 是 Model 层的核心组件,封装了所有数据操作逻辑。ViewModel 通过 Repository 的方法来读取和写入数据,不直接操作数据库。

class NoteRepository {
  static async getAll(includeDeleted: boolean = false): Promise<NoteEntity[]> {
    const store = await DatabaseHelper.getStore();
    const predicates = new relationalStore.RdbPredicates('note');

    if (!includeDeleted) {
      predicates.equalTo('is_deleted', 0);
    }

    predicates.orderByDesc('updated_at');

    try {
      const resultSet = await store.query(predicates, [
        'id', 'title', 'content', 'content_text',
        'created_at', 'updated_at', 'is_favorite', 'color', 'tag'
      ]);

      return this.parseResultSet(resultSet);
    } catch (err) {
      console.error('查询笔记失败:', err);
      return [];
    }
  }

  static async search(keyword: string): Promise<NoteEntity[]> {
    const store = await DatabaseHelper.getStore();
    const predicates = new relationalStore.RdbPredicates('note');

    predicates.equalTo('is_deleted', 0);
    predicates.and();
    predicates.beginWrap();
    predicates.like('title', `%${keyword}%`);
    predicates.or();
    predicates.like('content_text', `%${keyword}%`);
    predicates.endWrap();
    predicates.orderByDesc('updated_at');

    try {
      const resultSet = await store.query(predicates, [
        'id', 'title', 'content', 'content_text',
        'created_at', 'updated_at', 'is_favorite', 'color', 'tag'
      ]);
      return this.parseResultSet(resultSet);
    } catch (err) {
      console.error('搜索笔记失败:', err);
      return [];
    }
  }

  static async insert(note: Omit<NoteEntity, 'id'>): Promise<number> {
    const store = await DatabaseHelper.getStore();
    const row = {
      title: note.title,
      content: note.content,
      content_text: note.contentText,
      created_at: note.createdAt,
      updated_at: note.updatedAt,
      is_favorite: note.isFavorite ? 1 : 0,
      is_deleted: 0,
      color: note.color ?? 0,
      tag: note.tag ?? ''
    };

    try {
      const rowId = await store.insert('note', row);
      console.info(`笔记创建成功, id=${rowId}`);
      return rowId;
    } catch (err) {
      console.error('笔记创建失败:', err);
      throw err;
    }
  }

  static async update(note: NoteEntity): Promise<number> {
    const store = await DatabaseHelper.getStore();
    const row = {
      title: note.title,
      content: note.content,
      content_text: note.contentText,
      updated_at: note.updatedAt,
      is_favorite: note.isFavorite ? 1 : 0,
      color: note.color ?? 0,
      tag: note.tag ?? ''
    };
    const predicates = new relationalStore.RdbPredicates('note');
    predicates.equalTo('id', note.id);

    try {
      const affectedRows = await store.update(row, predicates);
      return affectedRows;
    } catch (err) {
      console.error('笔记更新失败:', err);
      throw err;
    }
  }

  static async softDelete(id: number): Promise<void> {
    const store = await DatabaseHelper.getStore();
    const predicates = new relationalStore.RdbPredicates('note');
    predicates.equalTo('id', id);

    try {
      await store.update({ is_deleted: 1, updated_at: Date.now() }, predicates);
    } catch (err) {
      console.error('笔记删除失败:', err);
      throw err;
    }
  }

  static async permanentlyDelete(id: number): Promise<void> {
    const store = await DatabaseHelper.getStore();
    const predicates = new relationalStore.RdbPredicates('note');
    predicates.equalTo('id', id);

    try {
      await store.delete(predicates);
    } catch (err) {
      console.error('笔记永久删除失败:', err);
      throw err;
    }
  }

  private static parseResultSet(resultSet: relationalStore.ResultSet): NoteEntity[] {
    const notes: NoteEntity[] = [];

    try {
      while (resultSet.goToNextRow()) {
        const note: NoteEntity = {
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          title: resultSet.getString(resultSet.getColumnIndex('title')),
          content: resultSet.getString(resultSet.getColumnIndex('content')),
          contentText: resultSet.getString(resultSet.getColumnIndex('content_text')),
          createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')),
          updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')),
          isFavorite: resultSet.getLong(resultSet.getColumnIndex('is_favorite')) === 1,
          color: resultSet.getLong(resultSet.getColumnIndex('color')),
          tag: resultSet.getString(resultSet.getColumnIndex('tag'))
        };
        notes.push(note);
      }
    } finally {
      resultSet.close(); // 务必在 finally 块中关闭结果集
    }

    return notes;
  }
}

注意 parseResultSet 方法中的资源管理:ResultSet 对象在使用完毕后必须关闭,否则会导致数据库连接泄漏,最终耗尽系统资源。将 resultSet.close() 放在 finally 块中是确保即使发生异常也能正确释放资源的关键实践。

3.3.4 KVStore 配置管理

对于应用配置和用户偏好设置,使用 KVStore(键值型数据库)进行管理:

import { preferences } from '@kit.ArkData';

class KVStoreUtil {
  private static store: preferences.Preferences | null = null;

  static async getStore(): Promise<preferences.Preferences> {
    if (this.store !== null) {
      return this.store;
    }

    try {
      this.store = await preferences.getPreferences(getContext(), 'note_app_settings');
      return this.store;
    } catch (err) {
      console.error('KVStore 初始化失败:', err);
      throw err;
    }
  }

  static async get(key: string, defaultValue: string): Promise<string> {
    const store = await this.getStore();
    return store.get(key, defaultValue);
  }

  static async put(key: string, value: string): Promise<void> {
    const store = await this.getStore();
    await store.put(key, value);
    await store.flush(); // 立即刷写到磁盘
  }

  static async delete(key: string): Promise<void> {
    const store = await this.getStore();
    await store.delete(key);
    await store.flush();
  }

  // 简便方法:直接操作 JSON 数据
  static async getObject<T>(key: string, defaultValue: T): Promise<T> {
    const jsonStr = await this.get(key, '');
    if (jsonStr === '') return defaultValue;
    try {
      return JSON.parse(jsonStr) as T;
    } catch {
      return defaultValue;
    }
  }

  static async putObject(key: string, value: object): Promise<void> {
    await this.put(key, JSON.stringify(value));
  }
}

KVStore 存储的典型配置项包括:

配置键 类型 默认值 说明
theme string “light” 主题模式(light/dark)
sort_by string “updated_at” 排序字段
sort_order string “desc” 排序方向
auto_save string “true” 是否启用自动保存
auto_save_interval string “2000” 自动保存间隔(毫秒)
recent_searches string(JSON) “[]” 搜索历史记录(最多10条)
font_size string “16” 编辑器字号

3.4 用户界面模块

3.4.1 首页(笔记列表页)

首页是用户打开应用后看到的主界面,其信息架构设计的核心原则是"让用户快速找到需要的笔记"。页面布局从上到下分为三个区域:

顶部状态栏与操作栏:显示应用名称和当前笔记总数,右侧提供"搜索"和"设置"入口按钮。操作栏采用半透明毛玻璃效果,在列表滚动时产生视觉深度层次感。

笔记列表区:采用 List 组件实现虚拟列表,通过 LazyForEach 懒加载数据源,仅渲染当前可视区域内的列表项,大幅降低长列表场景下的内存占用和渲染开销。每个列表项(NoteCard 组件)展示笔记标题、正文摘要(取前 50 个字符,去除格式标记)、更新时间,并根据笔记的 color 字段在左侧显示颜色标记条。

底部悬浮按钮:右下角的"+“悬浮按钮用于快速创建新笔记。按钮采用 FAB(Floating Action Button)设计模式,带有展开动画效果,长按可展开快捷操作菜单(如"新建纯文本笔记”、"新建待办清单"等)。

@Component
struct NoteListPage {
  @State notes: NoteEntity[] = [];
  @State isLoading: boolean = true;
  @State sortBy: string = 'updated_at';

  private vm: NoteListVM = new NoteListVM();

  aboutToAppear(): void {
    this.loadNotes();
  }

  private async loadNotes(): Promise<void> {
    this.isLoading = true;
    try {
      this.notes = await this.vm.loadNotes(this.sortBy);
    } catch (err) {
      console.error('加载笔记列表失败:', err);
    } finally {
      this.isLoading = false;
    }
  }

  build() {
    Stack() {
      Column() {
        // 顶部操作栏
        Row() {
          Text(`记事本 (${this.notes.length})`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
          Blank()
          Image($r('app.media.icon_search'))
            .width(24)
            .height(24)
            .onClick(() => {
              router.pushUrl({ url: 'pages/Search' });
            })
          Image($r('app.media.icon_settings'))
            .width(24)
            .height(24)
            .margin({ left: 16 })
            .onClick(() => {
              router.pushUrl({ url: 'pages/Settings' });
            })
        }
        .padding({ left: 20, right: 20, top: 16, bottom: 12 })
        .width('100%')

        // 笔记列表
        if (this.isLoading) {
          LoadingProgress()
            .width(32)
            .height(32)
            .margin({ top: 100 })
        } else if (this.notes.length === 0) {
          EmptyState({
            icon: $r('app.media.icon_empty'),
            message: '还没有笔记,点击右下角开始记录吧'
          })
        } else {
          List() {
            LazyForEach(this.notes, (note: NoteEntity) => {
              ListItem() {
                NoteCard({ note: note })
                  .margin({ left: 16, right: 16, bottom: 8 })
                  .onClick(() => {
                    router.pushUrl({
                      url: 'pages/Editor',
                      params: { noteId: note.id }
                    });
                  })
                  .gesture(
                    LongPressGesture()
                      .onAction(() => {
                        this.showNoteContextMenu(note.id);
                      })
                  )
              }
            }, (note: NoteEntity) => note.id.toString())
          }
          .width('100%')
          .layoutWeight(1)
        }
      }
      .width('100%')
      .height('100%')

      // 悬浮按钮
      Column() {
        Blank()
        Row() {
          Blank()
          Image($r('app.media.icon_add'))
            .width(28)
            .height(28)
        }
        .width(60)
        .height(60)
        .backgroundColor('#007AFF')
        .borderRadius(30)
        .shadow({ radius: 8, color: 'rgba(0,122,255,0.3)' })
        .margin({ right: 20, bottom: 30 })
        .onClick(() => {
          router.pushUrl({ url: 'pages/Editor', params: { mode: 'create' } });
        })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}
3.4.2 笔记编辑页

编辑页是用户使用频率最高的页面,其设计追求沉浸式的书写体验:

  • 标题栏:默认为折叠状态,用户向下滚动正文时自动展开显示"返回"和"更多操作"按钮
  • 标题输入区:大字号标题输入框,支持任意长度的文本输入
  • 正文编辑区:占满剩余空间的文本编辑区域,底部附带富文本格式工具栏
  • 底部信息栏:显示字数和行数统计,以及"最后保存时间"提示

编辑页进入时会自动判断是新建笔记还是编辑已有笔记,通过路由参数 modenoteId 来区分。如果是编辑已有笔记,编辑器会立即加载该笔记的内容数据;如果是新建笔记,编辑器会先创建一个空的数据库记录以获得笔记 ID,然后进入编辑状态。

3.4.3 搜索页

搜索页提供了比列表页更精细的笔记检索能力:

  • 搜索输入框:自动获得焦点,输入即搜索,无需要点击搜索按钮
  • 搜索结果列表:实时展示匹配结果,标题中的匹配关键词以高亮色显示
  • 搜索历史:在输入框为空时显示最近的搜索历史,支持点击快速搜索和手动清除
  • 无结果提示:当搜索无匹配结果时,显示友好的空状态提示
3.4.4 设置页

设置页提供应用级别的偏好配置选项:

  • 主题切换:支持浅色模式、深色模式、跟随系统(三种选项)
  • 排序设置:设置笔记列表的默认排序方式
  • 自动保存:开启/关闭自动保存功能,以及设置保存间隔时间
  • 关于应用:显示应用版本号、开发者信息、开源许可等

四、数据流与状态管理

4.1 完整数据流链路分析

以"用户创建新笔记并保存"为例,完整的数据流链路如下:

步骤1:用户点击首页的"+"悬浮按钮
       → Index.ets 触发 onClick 事件
       → 调用 router.pushUrl() 跳转到编辑器页面
       → 路由参数 { mode: 'create' }

步骤2:编辑器页面初始化(aboutToAppear 生命周期)
       → Editor.ets 创建 NoteEditorVM 实例
       → NoteEditorVM.createNewNote()
       → NoteRepository.insert({ title: "新建笔记", content: "", ... })
       → 数据库返回新笔记的 ID
       → NoteEditorVM.currentNote.id = 新 ID
       → UI 更新:编辑器进入可用状态

步骤3:用户输入笔记内容
       → TextArea 的 onChange 事件触发
       → NoteEditorVM.onContentChanged(text)
       → 更新 NoteEditorVM.currentNote.content
       → 设置防抖定时器(2秒后自动保存)

步骤4:自动保存触发
       → NoteEditorVM.autoSaveTimer 到期
       → NoteEditorVM.saveToDatabase()
       → NoteRepository.update(currentNote)
       → 数据库更新记录
       → UI 更新:显示"已保存"状态

这条完整的链路清晰地展示了 MVVM 架构中数据从 View 层到 Model 层再到数据库的完整流转过程。每个层级各司其职,变化可追溯、可测试。

4.2 异步操作的异常处理

所有涉及数据库的操作都采用异步方式执行,并配备完善的异常处理机制:

async function safeDatabaseOperation<T>(
  operation: () => Promise<T>,
  fallbackValue: T,
  errorMessage: string
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    console.error(`${errorMessage}:`, error);
    // 可在此处集成异常上报服务
    return fallbackValue;
  }
}

// 使用示例
const notes = await safeDatabaseOperation(
  () => NoteRepository.getAll(),
  [],
  '获取笔记列表失败'
);

4.3 状态同步机制

当用户在编辑器中修改笔记并返回列表页时,需要确保列表页能够刷新以反映最新的笔记内容。这一状态同步通过以下两种机制结合实现:

  1. 页面可见性回调:利用页面生命周期方法 onPageShow(),在页面重新变为可见时自动刷新数据
  2. 全局状态广播:通过 AppStorage 或 EventHub 发布"笔记已更新"事件,其他页面监听到事件后选择性刷新
// 方式一:页面生命周期刷新
@Entry
@Component
struct IndexPage {
  onPageShow(): void {
    // 页面重新获得焦点时刷新笔记列表
    this.loadNotes();
  }
}

// 方式二:事件总线通知
import { EventHub } from '@kit.ArkUI';

// 保存笔记后发布事件
EventHub.emit('note_updated', { noteId: currentNote.id });

// 列表页监听事件
aboutToAppear(): void {
  EventHub.on('note_updated', (data) => {
    // 选择性刷新:可以只刷新被修改的那条笔记
    this.refreshSingleNote(data.noteId);
  });
}

五、性能优化策略

5.1 列表性能优化

笔记列表是应用中数据量最大的场景,当笔记数量达到数百甚至上千条时,列表的渲染性能直接关系到用户的第一印象。

惰性加载(Lazy Loading):使用 LazyForEach 替代传统的 ForEach,仅渲染当前屏幕可视区域内的列表项。当用户滚动列表时,离开屏幕的列表项会被回收,新进入屏幕的列表项才会被创建。这种按需渲染的策略可以将列表的内存占用降低 90% 以上。

// 数据源需要实现 IDataSource 接口或使用 LazyForEach 支持的数组
List() {
  LazyForEach(this.notesDataSource, (item: NoteEntity) => {
    ListItem() {
      NoteCard({ note: item })
    }
  }, (item: NoteEntity) => item.id.toString())
}
.width('100%')

组件复用:列表项组件 NoteCard 在滑动过程中会被复用,而非不断地创建和销毁。ArkUI 的 ListItem 组件内置了复用机制,开发者无需额外配置即可受益。

条件渲染:在编辑器中,富文本格式工具栏等次要 UI 元素在不需要时不应渲染。通过 if 条件语句控制组件的渲染与销毁,减少不必要的布局计算。

减少状态更新范围:当列表中某条笔记的状态变化时(如切换收藏状态),只更新该条笔记对应的组件,而不是重新渲染整个列表。通过精细化的状态拆分实现:

// 错误做法:整个列表重新渲染
@State notes: NoteEntity[] = [];
toggleFavorite(noteId: number) {
  this.notes = this.notes.map(n => {
    if (n.id === noteId) return { ...n, isFavorite: !n.isFavorite };
    return n;
  });
}

// 推荐做法:只更新目标笔记的 ViewModel
// 每条笔记持有独立的 ViewModel,修改仅触发自身更新

5.2 数据库查询优化

索引优化:在 updated_atcreated_atis_deletedis_favoritetitle 等常用查询字段上建立索引,将查询速度提升数倍到数十倍。索引的设计需要平衡读写性能——过多的索引会降低写入速度(每次写入都需要更新索引),因此只对高频查询字段建立索引。

查询字段精简化:在查询列表时,只查询所需的字段,避免使用 SELECT *。例如列表页只需要 idtitlecontent_text(取前 50 字符即可)、updated_at,而 content(富文本完整内容)在列表页不需要加载,只有在进入编辑器时才完整读取。

// 列表页查询:只请求必要字段
predicates.select(['id', 'title', 'content_text', 'created_at', 'updated_at', 'is_favorite', 'color', 'tag']);

// 编辑页查询:请求完整数据(包含 content 字段)
predicates.select(['id', 'title', 'content', 'content_text', 'created_at', 'updated_at', 'is_favorite', 'color', 'tag']);

分页加载:当笔记数量超过 100 条时,采用分页策略,每次只加载 50 条,用户滚动到底部时自动加载下一页。这比一次性加载所有数据的方式更节省内存和启动时间。

static async getPage(page: number, pageSize: number = 50): Promise<NoteEntity[]> {
  const store = await DatabaseHelper.getStore();
  const predicates = new relationalStore.RdbPredicates('note');
  predicates.equalTo('is_deleted', 0);
  predicates.orderByDesc('updated_at');
  predicates.limit(pageSize, (page - 1) * pageSize); // limit + offset

  const resultSet = await store.query(predicates, [
    'id', 'title', 'content_text', 'created_at', 'updated_at',
    'is_favorite', 'color', 'tag'
  ]);
  return this.parseResultSet(resultSet);
}

5.3 内存优化

大文本处理:对于内容长度超过 10 万字符的笔记,在编辑器中使用分段加载策略,只加载和渲染当前可见区域的文本内容,而非一次性加载全文。

图片缓存:为后续扩展的图片附件功能预留 LRU(最近最少使用)内存缓存策略,限制图片缓存池大小为 50MB,超出时自动淘汰最久未使用的缓存项。

资源释放:在组件的生命周期方法 aboutToDisappear 中释放所有占用的资源,包括清除定时器、关闭数据库连接、释放图片资源等。

aboutToDisappear(): void {
  // 清除自动保存定时器
  if (this.autoSaveTimer !== null) {
    clearTimeout(this.autoSaveTimer);
    this.autoSaveTimer = null;
  }
  // 释放大对象引用,帮助垃圾回收
  this.currentNote = null;
}

5.4 启动速度优化

  • 冷启动优化:通过代码懒加载(动态 import)和资源按需加载,减少应用启动时加载的模块数量
  • 数据库预热:应用启动后,在主页渲染完成的同时,立即在后端线程中预加载最近 20 条笔记数据
  • 页面预加载:在用户浏览列表时,通过读取用户行为数据,预判用户可能点击的笔记并提前加载其内容

六、安全与隐私设计

6.1 数据安全

本地优先存储:所有笔记数据默认仅存储在用户设备本地,不强制上传到云端。应用不收集任何用户的个人信息、设备信息或使用行为数据。

数据库隔离:每个应用的数据库实例相互隔离,其他应用无法直接访问本应用的数据库文件。鸿蒙系统的安全沙箱机制为应用数据提供了系统级的隔离保护。

数据库加密(可选):对于有高安全需求的场景,可以开启 RDB 的加密功能。加密后的数据库文件即使被非法读取,也无法还原其中的数据内容。

// 启用数据库加密
const config: relationalStore.StoreConfig = {
  name: 'NoteApp.db',
  securityLevel: relationalStore.SecurityLevel.S3, // 较高安全级别
  encrypt: true,
  encryptConfig: {
    encryptionKey: 'user_defined_encryption_key' // 加密密钥
  }
};

6.2 隐私保护

  • 无网络权限申请:应用默认不申请网络权限,从根源上杜绝数据外传的可能性
  • 无账号系统:不要求用户注册或登录,无需提供手机号、邮箱等个人信息
  • 本地数据导出:提供数据导出功能,用户可以将笔记导出为文本文件,完全掌控自己的数据
  • 数据删除确认:永久删除笔记前弹出二次确认对话框,防止误操作导致的数据永久丢失

6.3 权限管理

应用遵循最小权限原则,只申请应用运行所必需的系统权限:

权限 用途 必要性
ohos.permission.STORAGE 数据导出到外部存储 仅导出功能需要
ohos.permission.VIBRATE 触觉反馈 可选,提升交互体验

所有权限在首次使用时动态申请,并在权限申请弹窗中明确告知用户权限的用途。用户也可以随时在系统设置中关闭已授予的权限。


七、测试方案

7.1 单元测试

针对 Repository 层和 ViewModel 层的核心业务方法编写单元测试,验证数据操作的正确性和异常处理的完备性。

// 单元测试示例(使用 HarmonyOS 测试框架)
describe('NoteRepository tests', () => {
  it('insert should return positive row id', async () => {
    const noteId = await NoteRepository.insert({
      title: '测试笔记',
      content: '测试内容',
      contentText: '测试内容',
      createdAt: Date.now(),
      updatedAt: Date.now(),
      isFavorite: false,
      color: 0,
      tag: ''
    });
    expect(noteId).toBeGreaterThan(0);
  });

  it('soft delete should not physically remove record', async () => {
    const noteId = await createTestNote();
    await NoteRepository.softDelete(noteId);
    const notes = await NoteRepository.getAll(false);
    expect(notes.find(n => n.id === noteId)).toBeUndefined();
    const allNotes = await NoteRepository.getAll(true);
    expect(allNotes.find(n => n.id === noteId)).toBeDefined();
  });

  it('search should find notes by title', async () => {
    await createTestNote('鸿蒙开发笔记', '内容');
    const results = await NoteRepository.search('鸿蒙');
    expect(results.length).toBeGreaterThan(0);
    expect(results[0].title).toContain('鸿蒙');
  });
});

7.2 UI 集成测试

使用鸿蒙 UI 测试框架编写集成测试,验证核心用户交互流程的正确性:

  • 创建笔记流程:点击"新建"按钮 → 进入编辑器 → 输入内容 → 返回列表页 → 验证列表中出现新笔记
  • 编辑笔记流程:点击已有笔记 → 进入编辑器 → 修改内容 → 返回列表页 → 验证列表展示更新后的内容
  • 删除笔记流程:长按笔记 → 选择删除 → 验证笔记从列表中消失 → 进入回收站 → 验证笔记存在于回收站中
  • 搜索功能测试:在搜索页输入关键词 → 验证搜索结果列表正确匹配 → 清除关键词 → 验证显示搜索历史

7.3 性能测试

  • 列表滚动性能:创建 500 条笔记,测试列表滚动帧率,目标帧率不低于 60fps
  • 启动性能:测试冷启动时间,目标控制在 2 秒以内
  • 数据库写入性能:测试连续写入 100 条笔记的总耗时,控制在 2 秒以内
  • 内存占用:在 500 条笔记的场景下,列表页内存占用控制在 150MB 以内

八、项目目录结构规范

NoteApp/
├── entry/                                # 应用主模块
│   └── src/
│       └── main/
│           ├── ets/                      # ArkTS 源码目录
│           │   ├── pages/                # 页面级组件
│           │   │   ├── Index.ets         # 首页(笔记列表)
│           │   │   ├── Editor.ets        # 笔记编辑器页面
│           │   │   ├── Search.ets        # 搜索页面
│           │   │   └── Settings.ets      # 设置页面
│           │   ├── components/           # 可复用 UI 组件
│           │   │   ├── NoteCard.ets      # 笔记卡片组件
│           │   │   ├── EmptyState.ets    # 空状态占位组件
│           │   │   ├── EditorToolbar.ets # 编辑器工具栏组件
│           │   │   └── ConfirmDialog.ets # 确认弹窗组件
│           │   ├── model/                # 数据模型与实体定义
│           │   │   └── NoteModel.ets     # NoteEntity 接口与类型定义
│           │   ├── viewmodel/            # ViewModel 层
│           │   │   ├── NoteListVM.ets    # 列表页 ViewModel
│           │   │   ├── NoteEditorVM.ets  # 编辑器 ViewModel
│           │   │   └── SearchVM.ets      # 搜索页 ViewModel
│           │   ├── repository/           # 数据仓库层
│           │   │   └── NoteRepository.ets
│           │   ├── db/                   # 数据库层
│           │   │   └── DatabaseHelper.ets
│           │   ├── utils/                # 工具函数
│           │   │   ├── TimeUtil.ets      # 时间格式化工具
│           │   │   ├── RichTextParser.ets # 富文本解析器
│           │   │   └── KVStoreUtil.ets   # KVStore 工具封装
│           │   └── constants/            # 常量定义
│           │       └── AppConstants.ets  # 应用级常量
│           ├── resources/                # 资源文件
│           │   ├── base/
│           │   │   ├── media/            # 图片资源
│           │   │   ├── element/          # 字符串、颜色等基础资源
│           │   │   └── profile/          # 配置资源
│           │   ├── en_US/                # 英文语言包
│           │   └── zh_CN/                # 中文语言包
│           └── module.json5              # 模块配置
├── oh-package.json5                      # ohpm 包依赖配置
├── build-profile.json5                   # Hvigor 构建配置
└── hvigorfile.ts                         # Hvigor 构建脚本

目录结构设计说明

  • 按功能模块分目录:pages、components、model、viewmodel、repository、db、utils 各司其职,新加入项目的开发者可以快速定位代码位置
  • 关注点分离:UI 代码(pages + components)与业务逻辑代码(viewmodel + repository + db)严格分离,修改 UI 不影响业务逻辑,反之亦然
  • 实体与类型集中管理:所有数据模型的接口定义集中在 model 目录,一处定义多处引用,避免类型定义分散导致的维护困难
  • 工具函数单一职责:每个 util 文件只聚焦一个能力领域,文件体积控制在 200 行以内

九、开发环境与构建部署

9.1 开发环境要求

工具 版本要求 说明
DevEco Studio 5.0.0 及以上 鸿蒙官方集成开发环境,基于 IntelliJ IDEA
HarmonyOS SDK API 12 及以上 目标平台 SDK,包含系统 API 和工具链
Node.js 18.x LTS ohpm 包管理器依赖
Hvigor 3.x 鸿蒙项目构建工具,兼容 Gradle 构建脚本
ohpm 最新版 鸿蒙包管理器,用于管理第三方依赖
Git 2.x 版本控制

9.2 构建配置

// build-profile.json5
{
  "app": {
    "signingConfigs": [
      {
        "name": "default",
        "type": "HarmonyOS"
      }
    ],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compileSdkVersion": "12",
        "compatibleSdkVersion": "12",
        "targetSdkVersion": "12"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

9.3 应用打包与发布

  • 调试包(Debug):使用自动签名的调试证书,安装到开发者设备进行调试
  • Release 包:使用华为开发者联盟签发的正式证书签名,发布到华为应用市场
  • HAP 包:鸿蒙应用的标准打包格式,安装包体积目标控制在 5MB 以内

9.4 ohpm 依赖配置

// oh-package.json5
{
  "name": "note-app",
  "version": "1.0.0",
  "description": "基于鸿蒙原生技术的记事本应用",
  "main": "index.ets",
  "dependencies": {
    "@kit.ArkUI": "12.x",
    "@kit.ArkData": "12.x"
  },
  "devDependencies": {
    "@ohos/hypium": "1.2.x",
    "@ohos/hamock": "1.0.x"
  }
}

十、项目价值

10.1 技术价值

价值维度 详细说明
技术栈深度融合 项目完整展示了 ArkTS 语言、ArkUI 框架、RDB 关系型数据库、KVStore 键值存储、TaskPool 并发编程等多种鸿蒙原生技术的融合使用方案,为开发者提供了一份从理论到实践的完整参考
MVVM 架构落地 提供了 MVVM 架构在鸿蒙应用中的具体实现指导,包括各层之间的职责划分、数据绑定方式、异步处理模式等,帮助开发者理解并应用这一主流架构模式
组件化开发范式 展示了如何通过 @Component 装饰器封装可复用的 UI 组件,如何利用 @Prop、@Link、@Builder 等机制实现组件的灵活配置和高效复用
性能优化方法论 总结了列表虚拟化、防抖保存、查询优化、内存管理等针对鸿蒙平台的具体优化策略,可为其他类似应用提供直接的性能优化参考
开发规范体系 建立了涵盖项目目录结构、命名规范、代码风格、错误处理模式等方面的开发规范,有助于团队协作开发时的代码一致性维护

10.2 业务价值

  • 精准满足用户刚需:记事本是移动场景下最基础的工具类应用之一,覆盖工作笔记、学习笔记、生活备忘、创意灵感、日记随笔等多样化使用场景,具有极高的用户粘性和使用频率。
  • 数据自主可控:区别于依赖云服务的笔记应用,本应用的所有数据存储于用户设备本地,不要求用户注册账号,不收集用户个人信息,从根本上保障了用户的数据主权和隐私安全。
  • 完全离线可用:不依赖网络连接,用户在飞行模式或无网络环境下仍然可以正常创建、编辑和查阅笔记,契合地铁、飞机、山区等无网络场景的需求。
  • 轻量化设计:应用安装包目标体积在 5MB 以内,启动速度快,运行时内存占用低,即使在低端鸿蒙设备上也能流畅运行。
  • 高度可扩展:模块化和分层架构设计为后续功能扩展预留了清晰的空间。未来可无缝添加云同步(基于华为云)、语音笔记(基于 ASR 语音识别)、图片附件、Markdown 渲染、标签分类、笔记分享等功能,使应用从一个基础记事本演进为功能完备的知识管理工具。

10.3 生态价值

  • 丰富鸿蒙原生应用生态:在当前鸿蒙原生应用数量相对有限的大背景下,贡献一款高质量的、完全基于原生技术栈的工具类应用,有助于充实鸿蒙应用市场的基础品类。
  • 开发者社区技术贡献:项目的架构设计、代码实现、性能优化策略等内容,可以作为鸿蒙开发者的学习参考资料,降低 ArkTS/ArkUI 的学习门槛,加速鸿蒙开发人才的培养。
  • 验证鸿蒙技术成熟度:通过在真实应用场景中深度使用鸿蒙 API 和框架能力,检验 ArkTS 语言的开发效率、ArkUI 框架的渲染性能、RDB 数据库的稳定性等关键指标,为鸿蒙平台的持续改进提供来自开发者的真实反馈。
  • 探索多设备协同:基于鸿蒙分布式技术架构,本应用具备扩展为多设备协同应用的天然基础。未来可以在手机、平板、智慧屏等设备间实现笔记数据的无缝流转和协同编辑,探索鸿蒙"一次开发,多端部署"的应用开发范式。

十一、未来展望

11.1 短期规划(1-2 个版本)

  • Markdown 语法支持:增加完整的 Markdown 编辑和实时预览功能,满足技术用户的笔记需求
  • 标签分类系统:支持为笔记打标签,并基于标签进行检索和筛选
  • 回收站功能:在 UI 层面增加"最近删除"入口,支持批量恢复和自动清理过期删除记录
  • 笔记排序增强:增加拖拽排序、置顶等功能

11.2 中期规划(3-6 个月)

  • 云同步服务:基于华为云服务(CloudService)实现笔记的跨设备同步,端到端加密确保数据安全
  • 图片附件支持:在笔记中插入图片,并实现图片的缩略图预览和全屏查看
  • 手写笔记:利用鸿蒙手写 API,支持在笔记中插入手写内容
  • 语音笔记:集成鸿蒙语音识别服务,实现语音转文字功能

11.3 长期规划(6 个月以上)

  • 多端协同:基于分布式数据管理能力,实现手机、平板、PC 三端笔记数据实时同步和接力编辑
  • 智能助手集成:接入鸿蒙智慧能力,提供笔记内容摘要生成、关键信息提取、智能标签推荐等 AI 能力
  • 开放生态:提供笔记数据的导入导出标准格式(如 Markdown、JSON),支持与其他笔记应用的数据互操作

十二、总结

本记事本鸿蒙应用项目,从技术选型到架构设计,从功能实现到性能优化,从安全保障到生态价值,进行了系统性的规划和实践。项目基于 HarmonyOS 原生技术栈,采用 ArkTS 语言与 ArkUI 框架,通过 MVVM 分层架构实现了清晰的责任划分和良好的可维护性。

在功能层面,项目覆盖了笔记创建、编辑、搜索、分类等完整的笔记生命周期管理,并通过富文本编辑、自动保存、软删除等设计细节提升用户的使用体验。在技术层面,项目充分运用了鸿蒙 RDB、KVStore、TaskPool、AppStorage 等系统能力,并针对列表渲染、数据库查询、内存管理等场景制定了针对性的性能优化策略。

项目不仅是一款满足用户日常记事需求的功能型应用,更是一份面向鸿蒙开发者社区的技术参考资料,内容涵盖了从项目初始化、架构搭建、功能开发到性能优化的完整工作流。通过这一项目的实践和分享,我们期待能够为鸿蒙原生应用生态的繁荣发展贡献一份力量。

在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐