HarmonyOS APP<玩转React>开源教程五:项目架构设计

第5次:项目架构设计

良好的项目架构是应用可维护性和可扩展性的基础。本次课程将学习分层架构设计原则,规划项目目录结构,并搭建完整的项目骨架。


学习目标

  • 理解分层架构设计原则
  • 掌握项目目录结构规划
  • 学会常量和配置管理
  • 完成项目骨架搭建

5.1 分层架构设计原则

为什么需要分层?

随着项目规模增长,如果所有代码都写在一起:

  • 代码难以维护
  • 功能难以复用
  • 团队协作困难
  • 测试难以进行

分层架构的优势

复制代码
┌─────────────────────────────────────┐
│           表现层 (Pages)             │  ← 用户界面
├─────────────────────────────────────┤
│          组件层 (Components)         │  ← 可复用 UI
├─────────────────────────────────────┤
│          服务层 (Services)           │  ← 业务逻辑
├─────────────────────────────────────┤
│           数据层 (Data)              │  ← 数据管理
├─────────────────────────────────────┤
│          模型层 (Models)             │  ← 类型定义
└─────────────────────────────────────┘

各层职责

层级 职责 示例
Pages 页面组件,处理路由 Index.ets, LessonDetail.ets
Components 可复用 UI 组件 ModuleCard.ets, CodeBlock.ets
Services 业务逻辑封装 TutorialService.ets, ProgressService.ets
Data 数据定义和管理 TutorialData.ets, SourceCodeData.ets
Models 类型和接口定义 Models.ets
Common 公共工具和常量 Constants.ets, StorageUtil.ets

依赖原则

复制代码
Pages → Components → Services → Data → Models
  ↓         ↓           ↓         ↓       ↓
  └─────────┴───────────┴─────────┴───────┘
                    Common
  • 上层可以依赖下层
  • 下层不能依赖上层
  • 同层之间尽量避免依赖

5.2 目录结构规划

完整目录结构

复制代码
entry/src/main/ets/
├── common/                     # 公共模块
│   ├── Constants.ets          # 常量定义
│   ├── StorageUtil.ets        # 存储工具
│   └── ThemeUtil.ets          # 主题工具
│
├── components/                 # 可复用组件
│   ├── CodeBlock.ets          # 代码块组件
│   ├── FloatingButton.ets     # 浮动按钮
│   ├── HeroBanner.ets         # 首页横幅
│   ├── KnowledgeCard.ets      # 知识卡片
│   ├── LessonItem.ets         # 课程项
│   ├── ModuleCard.ets         # 模块卡片
│   ├── ProgressRing.ets       # 进度环
│   ├── QuizOptionItem.ets     # 测验选项
│   ├── QuizQuestion.ets       # 测验题目
│   ├── QuizResultCard.ets     # 测验结果
│   └── SkillTreeNode.ets      # 技能树节点
│
├── data/                       # 数据定义
│   ├── TutorialData.ets       # 教程数据
│   ├── SourceCodeData.ets     # 源码数据
│   ├── OpenSourceProjectData.ets  # 开源项目数据
│   ├── InterviewQuizData.ets  # 面试题数据
│   └── DemoProjectData.ets    # 示例项目数据
│
├── models/                     # 数据模型
│   └── Models.ets             # 类型定义
│
├── pages/                      # 页面组件
│   ├── Index.ets              # 首页
│   ├── ModuleDetail.ets       # 模块详情
│   ├── LessonDetail.ets       # 课程详情
│   ├── QuizPage.ets           # 测验页
│   ├── QuizBankPage.ets       # 题库页
│   ├── CodePlayground.ets     # 代码调试
│   ├── SearchPage.ets         # 搜索页
│   ├── BookmarkPage.ets       # 收藏页
│   ├── SourceCodePage.ets     # 源码学习
│   ├── OpenSourcePage.ets     # 开源项目
│   └── WrongAnswerBookPage.ets # 错题本
│
├── services/                   # 业务服务
│   ├── TutorialService.ets    # 教程服务
│   ├── ProgressService.ets    # 进度服务
│   ├── QuizService.ets        # 测验服务
│   ├── BookmarkService.ets    # 收藏服务
│   ├── SearchService.ets      # 搜索服务
│   ├── BadgeService.ets       # 徽章服务
│   └── WrongAnswerService.ets # 错题服务
│
└── entryability/
    └── EntryAbility.ets       # 应用入口

命名规范

类型 命名规则 示例
页面 PascalCase Index.ets, ModuleDetail.ets
组件 PascalCase ModuleCard.ets, CodeBlock.ets
服务 PascalCase + Service TutorialService.ets
工具 PascalCase + Util StorageUtil.ets
常量 UPPER_SNAKE_CASE APP_NAME, PRIMARY_COLOR
接口 PascalCase LearningModule, UserProgress

5.3 常量管理:Constants.ets

创建常量文件

entry/src/main/ets/common/ 下创建 Constants.ets

typescript 复制代码
/**
 * 应用常量定义
 * 集中管理所有常量,便于维护和修改
 */

/**
 * 应用基本信息
 */
export class AppConstants {
  static readonly APP_NAME: string = 'React 学习教程';
  static readonly APP_VERSION: string = '1.0.0';
  static readonly PREFERENCES_NAME: string = 'react_tutorial_prefs';
}

/**
 * React 品牌色
 */
export class ReactColors {
  static readonly PRIMARY: string = '#61DAFB';
  static readonly PRIMARY_DARK: string = '#20232a';
  static readonly SECONDARY_DARK: string = '#282c34';
  static readonly GRADIENT_START: string = '#61DAFB';
  static readonly GRADIENT_END: string = '#21a0c4';
}

/**
 * 浅色主题颜色
 */
export class LightThemeColors {
  static readonly BACKGROUND: string = '#f8f9fa';
  static readonly CARD_BACKGROUND: string = '#ffffff';
  static readonly TEXT_PRIMARY: string = '#1a1a2e';
  static readonly TEXT_SECONDARY: string = '#495057';
  static readonly DIVIDER: string = '#e9ecef';
}

/**
 * 深色主题颜色
 */
export class DarkThemeColors {
  static readonly BACKGROUND: string = '#1a1a2e';
  static readonly CARD_BACKGROUND: string = '#282c34';
  static readonly TEXT_PRIMARY: string = '#ffffff';
  static readonly TEXT_SECONDARY: string = '#d1d5db';
  static readonly DIVIDER: string = '#3d3d5c';
}

/**
 * 难度等级颜色
 */
export class DifficultyColors {
  static readonly BEGINNER: string = '#51cf66';
  static readonly BASIC: string = '#339af0';
  static readonly INTERMEDIATE: string = '#ff922b';
  static readonly ADVANCED: string = '#ff6b6b';
  static readonly ECOSYSTEM: string = '#9775fa';
}

/**
 * 存储键名
 */
export class StorageKeys {
  static readonly USER_PROGRESS: string = 'user_progress';
  static readonly BOOKMARKS: string = 'bookmarks';
  static readonly THEME_MODE: string = 'theme_mode';
  static readonly QUIZ_HISTORY: string = 'quiz_history';
  static readonly WRONG_ANSWERS: string = 'wrong_answers';
  static readonly QUIZ_STATISTICS: string = 'quiz_statistics';
}

/**
 * 路由路径
 */
export class RoutePaths {
  static readonly INDEX: string = 'pages/Index';
  static readonly MODULE_DETAIL: string = 'pages/ModuleDetail';
  static readonly LESSON_DETAIL: string = 'pages/LessonDetail';
  static readonly QUIZ_PAGE: string = 'pages/QuizPage';
  static readonly QUIZ_BANK: string = 'pages/QuizBankPage';
  static readonly CODE_PLAYGROUND: string = 'pages/CodePlayground';
  static readonly SEARCH: string = 'pages/SearchPage';
  static readonly BOOKMARK: string = 'pages/BookmarkPage';
}

/**
 * 难度等级类型
 */
export type DifficultyLevel = 'beginner' | 'basic' | 'intermediate' | 'advanced' | 'ecosystem';

/**
 * 难度等级显示名称
 */
export const DifficultyNames: Record<DifficultyLevel, string> = {
  'beginner': '入门',
  'basic': '基础',
  'intermediate': '进阶',
  'advanced': '高级',
  'ecosystem': '生态'
};

/**
 * 获取难度等级颜色
 */
export function getDifficultyColor(difficulty: DifficultyLevel): string {
  const colors: Record<DifficultyLevel, string> = {
    'beginner': DifficultyColors.BEGINNER,
    'basic': DifficultyColors.BASIC,
    'intermediate': DifficultyColors.INTERMEDIATE,
    'advanced': DifficultyColors.ADVANCED,
    'ecosystem': DifficultyColors.ECOSYSTEM
  };
  return colors[difficulty] ?? DifficultyColors.BEGINNER;
}

/**
 * 应用配置
 */
export class AppConfig {
  static readonly QUIZ_PASSING_SCORE: number = 60;
  static readonly STREAK_BADGE_DAYS: number = 7;
  static readonly MAX_WRONG_ANSWERS: number = 100;
  static readonly SEARCH_DEBOUNCE_MS: number = 300;
}

使用常量

typescript 复制代码
import { AppConstants, ReactColors, StorageKeys, getDifficultyColor } from '../common/Constants';

// 使用应用信息
Text(AppConstants.APP_NAME)

// 使用颜色
.backgroundColor(ReactColors.PRIMARY)

// 使用存储键
StorageUtil.getObject(StorageKeys.USER_PROGRESS, defaultValue)

// 使用函数
let color = getDifficultyColor('intermediate');

5.4 实操:搭建完整项目骨架

现在,让我们创建项目的完整目录结构和基础文件。

步骤 1:创建目录结构

entry/src/main/ets/ 下创建以下目录:

  • common/
  • components/
  • data/
  • models/
  • services/

步骤 2:创建 StorageUtil.ets

common/ 下创建存储工具:

typescript 复制代码
/**
 * 持久化存储工具类
 */
import { preferences } from '@kit.ArkData';
import { AppConstants } from './Constants';

export class StorageUtil {
  private static preferencesInstance: preferences.Preferences | null = null;

  /**
   * 初始化
   */
  static async init(context: Context): Promise<void> {
    try {
      StorageUtil.preferencesInstance = await preferences.getPreferences(
        context,
        AppConstants.PREFERENCES_NAME
      );
      console.info('[StorageUtil] Initialized');
    } catch (error) {
      console.error('[StorageUtil] Init failed:', error);
    }
  }

  /**
   * 获取 Preferences 实例
   */
  private static getPreferences(): preferences.Preferences {
    if (!StorageUtil.preferencesInstance) {
      throw new Error('StorageUtil not initialized');
    }
    return StorageUtil.preferencesInstance;
  }

  /**
   * 获取字符串
   */
  static async getString(key: string, defaultValue: string = ''): Promise<string> {
    try {
      const prefs = StorageUtil.getPreferences();
      return await prefs.get(key, defaultValue) as string;
    } catch (error) {
      console.error(`[StorageUtil] getString failed for ${key}:`, error);
      return defaultValue;
    }
  }

  /**
   * 设置字符串
   */
  static async setString(key: string, value: string): Promise<void> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.put(key, value);
      await prefs.flush();
    } catch (error) {
      console.error(`[StorageUtil] setString failed for ${key}:`, error);
    }
  }

  /**
   * 获取对象
   */
  static async getObject<T>(key: string, defaultValue: T): Promise<T> {
    try {
      const prefs = StorageUtil.getPreferences();
      const jsonStr = await prefs.get(key, '') as string;
      if (!jsonStr) return defaultValue;
      return JSON.parse(jsonStr) as T;
    } catch (error) {
      console.error(`[StorageUtil] getObject failed for ${key}:`, error);
      return defaultValue;
    }
  }

  /**
   * 设置对象
   */
  static async setObject<T>(key: string, value: T): Promise<void> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.put(key, JSON.stringify(value));
      await prefs.flush();
    } catch (error) {
      console.error(`[StorageUtil] setObject failed for ${key}:`, error);
    }
  }

  /**
   * 删除键
   */
  static async remove(key: string): Promise<void> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.delete(key);
      await prefs.flush();
    } catch (error) {
      console.error(`[StorageUtil] remove failed for ${key}:`, error);
    }
  }

  /**
   * 清空所有数据
   */
  static async clear(): Promise<void> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.clear();
      await prefs.flush();
    } catch (error) {
      console.error('[StorageUtil] clear failed:', error);
    }
  }
}

步骤 3:更新 ThemeUtil.ets

typescript 复制代码
/**
 * 主题工具类
 */
import { LightThemeColors, DarkThemeColors } from './Constants';

export enum ThemeMode {
  AUTO = 'auto',
  LIGHT = 'light',
  DARK = 'dark'
}

export interface ThemeColors {
  background: string;
  cardBackground: string;
  textPrimary: string;
  textSecondary: string;
  divider: string;
}

export const LightTheme: ThemeColors = {
  background: LightThemeColors.BACKGROUND,
  cardBackground: LightThemeColors.CARD_BACKGROUND,
  textPrimary: LightThemeColors.TEXT_PRIMARY,
  textSecondary: LightThemeColors.TEXT_SECONDARY,
  divider: LightThemeColors.DIVIDER
};

export const DarkTheme: ThemeColors = {
  background: DarkThemeColors.BACKGROUND,
  cardBackground: DarkThemeColors.CARD_BACKGROUND,
  textPrimary: DarkThemeColors.TEXT_PRIMARY,
  textSecondary: DarkThemeColors.TEXT_SECONDARY,
  divider: DarkThemeColors.DIVIDER
};

export function initTheme(context: Context): void {
  AppStorage.setOrCreate('isDarkMode', false);
  AppStorage.setOrCreate('themeMode', ThemeMode.LIGHT);
}

export function toggleTheme(): void {
  const isDark = AppStorage.get<boolean>('isDarkMode') ?? false;
  AppStorage.set('isDarkMode', !isDark);
  AppStorage.set('themeMode', !isDark ? ThemeMode.DARK : ThemeMode.LIGHT);
}

export function getThemeColors(isDarkMode: boolean): ThemeColors {
  return isDarkMode ? DarkTheme : LightTheme;
}

步骤 4:创建服务层基础文件

创建 services/TutorialService.ets

typescript 复制代码
/**
 * 教程数据服务
 */
import { LearningModule, Lesson, DifficultyType } from '../models/Models';

export class TutorialService {
  private static initialized: boolean = false;
  private static modules: LearningModule[] = [];

  /**
   * 初始化服务
   */
  static init(): void {
    if (TutorialService.initialized) return;
    // 后续会加载实际数据
    TutorialService.initialized = true;
    console.info('[TutorialService] Initialized');
  }

  /**
   * 获取所有模块
   */
  static getAllModules(): LearningModule[] {
    return TutorialService.modules;
  }

  /**
   * 根据 ID 获取模块
   */
  static getModuleById(id: string): LearningModule | undefined {
    return TutorialService.modules.find(m => m.id === id);
  }

  /**
   * 根据难度获取模块
   */
  static getModulesByDifficulty(difficulty: DifficultyType): LearningModule[] {
    return TutorialService.modules.filter(m => m.difficulty === difficulty);
  }

  /**
   * 获取总课程数
   */
  static getTotalLessonCount(): number {
    return TutorialService.modules.reduce((sum, m) => sum + m.lessons.length, 0);
  }
}

创建 services/ProgressService.ets

typescript 复制代码
/**
 * 进度管理服务
 */
import { StorageUtil } from '../common/StorageUtil';
import { StorageKeys } from '../common/Constants';
import { UserProgress, DEFAULT_USER_PROGRESS, LearningModule } from '../models/Models';

export class ProgressService {
  private static cachedProgress: UserProgress | null = null;

  /**
   * 加载用户进度
   */
  static async loadProgress(): Promise<UserProgress> {
    try {
      const progress = await StorageUtil.getObject<UserProgress>(
        StorageKeys.USER_PROGRESS,
        DEFAULT_USER_PROGRESS
      );
      ProgressService.cachedProgress = progress;
      return progress;
    } catch (error) {
      console.error('[ProgressService] Load failed:', error);
      return DEFAULT_USER_PROGRESS;
    }
  }

  /**
   * 保存用户进度
   */
  static async saveProgress(progress: UserProgress): Promise<void> {
    try {
      await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress);
      ProgressService.cachedProgress = progress;
    } catch (error) {
      console.error('[ProgressService] Save failed:', error);
    }
  }

  /**
   * 标记课程完成
   */
  static async markLessonComplete(lessonId: string): Promise<void> {
    const progress = await ProgressService.loadProgress();
    if (!progress.completedLessons.includes(lessonId)) {
      progress.completedLessons.push(lessonId);
      progress.currentLesson = lessonId;
      await ProgressService.saveProgress(progress);
    }
  }

  /**
   * 计算模块完成百分比
   */
  static getCompletionPercentage(module: LearningModule, progress: UserProgress): number {
    if (module.lessons.length === 0) return 0;
    const completed = module.lessons.filter(
      l => progress.completedLessons.includes(l.id)
    ).length;
    return Math.round((completed / module.lessons.length) * 100);
  }

  /**
   * 更新连续学习天数
   */
  static async updateStreak(): Promise<number> {
    const progress = await ProgressService.loadProgress();
    const today = new Date().toISOString().split('T')[0];

    if (progress.lastStudyDate === today) {
      return progress.learningStreak;
    }

    const lastDate = progress.lastStudyDate;
    if (!lastDate) {
      progress.learningStreak = 1;
    } else {
      const diff = Math.floor(
        (new Date(today).getTime() - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24)
      );
      progress.learningStreak = diff === 1 ? progress.learningStreak + 1 : 1;
    }

    progress.lastStudyDate = today;
    await ProgressService.saveProgress(progress);
    return progress.learningStreak;
  }
}

步骤 5:更新页面路由配置

更新 entry/src/main/resources/base/profile/main_pages.json

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/ModuleDetail",
    "pages/LessonDetail",
    "pages/QuizPage",
    "pages/QuizBankPage",
    "pages/CodePlayground",
    "pages/SearchPage",
    "pages/BookmarkPage",
    "pages/SourceCodePage",
    "pages/OpenSourcePage"
  ]
}

项目结构验证

完成后,你的项目结构应该如下:

复制代码
entry/src/main/ets/
├── common/
│   ├── Constants.ets      ✓
│   ├── StorageUtil.ets    ✓
│   └── ThemeUtil.ets      ✓
├── components/            (待创建)
├── data/                  (待创建)
├── models/
│   └── Models.ets         ✓
├── pages/
│   └── Index.ets          ✓
├── services/
│   ├── TutorialService.ets  ✓
│   └── ProgressService.ets  ✓
└── entryability/
    └── EntryAbility.ets   ✓

本次课程小结

通过本次课程,你已经:

✅ 理解了分层架构设计原则

✅ 掌握了项目目录结构规划

✅ 学会了常量和配置的集中管理

✅ 创建了存储工具类

✅ 搭建了服务层基础框架

✅ 完成了项目骨架搭建


课后练习

  1. 添加日志工具:创建 LogUtil.ets,封装日志输出方法

  2. 扩展常量:添加更多应用配置常量

  3. 创建更多服务:按照模板创建 BookmarkService.ets


下次预告

第6次:数据模型设计与实现

我们将深入设计应用的数据模型:

  • 学习模块数据结构
  • 课程内容数据结构
  • 用户进度数据结构
  • 测验相关数据结构

完善的数据模型是应用功能的基础!

相关推荐
代码飞天3 小时前
harmonyOS开发基础之标题栏(HdsNavigation)
华为·harmonyos
2501_920627616 小时前
Flutter 框架跨平台鸿蒙开发 - 派对策划助手应用
flutter·华为·harmonyos
沙雕不是雕又菜又爱玩7 小时前
基于HarmonyOS的笔记管理应用
harmonyos
@不误正业7 小时前
AI Agent多轮对话管理:3大架构源码级实现与性能对比(附鸿蒙实战)
人工智能·架构·harmonyos
里欧跑得慢7 小时前
Flutter 组件 powersync_core 的适配 鸿蒙Harmony 实战 - 驾驭极致离线优先架构、实现鸿蒙端高性能 SQL 增量同步与数据安全治理方案
flutter·harmonyos·鸿蒙·openharmony·powersync_core
轻口味8 小时前
HarmonyOS 6 自定义人脸识别模型9:基于tflite的人脸识别模型转换
华为·harmonyos
芙莉莲教你写代码8 小时前
Flutter 框架跨平台鸿蒙开发 - 网络安全学习应用
学习·web安全·flutter·华为·harmonyos
互联网散修9 小时前
零基础鸿蒙应用开发第二十五节:接口的行为契约能力
华为·harmonyos
@不误正业9 小时前
AI Agent工具调用深度实战-从Function-Calling到鸿蒙设备控制全链路
人工智能·华为·harmonyos
云和数据.ChenGuang11 小时前
鸿蒙应用对接扣子智能体:从 0 到 1 落地 AI 智能体集成
人工智能·华为·harmonyos