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次:数据模型设计与实现

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

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

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

相关推荐
深海呐12 小时前
鸿蒙 Video组件的基本使用(HarmonyOS-VideoView)
华为·harmonyos·harmonyos video·harmonyos 视频播放·harmonyos 播放器·harmonyos 播放视频
是稻香啊14 小时前
HarmonyOS6 ArkUI 无障碍悬停事件(onAccessibilityHover)全面解析与实战演示
华为·harmonyos·harmonyos6
前端不太难14 小时前
从小项目到大型鸿蒙 App 的架构变化
架构·状态模式·harmonyos
aqi0014 小时前
【送书活动】《鸿蒙HarmonyOS 6:应用开发从零基础到App上线》迎新送书啦
android·华为·harmonyos·鸿蒙
Oyster3828916 小时前
告别“盲调”与“重编”!我写了一个鸿蒙 ArkUI 纯端侧的可视化调试神器,正式开源!
harmonyos·arkui
盐焗西兰花16 小时前
鸿蒙学习实战之路-Share Kit系列(7/17)-自定义分享面板操作区
linux·学习·harmonyos
xym18 小时前
Taskpool简单使用2
harmonyos
不爱吃糖的程序媛19 小时前
鸿蒙 Flutter 多引擎场景开发指导
flutter·华为·harmonyos
小雨青年20 小时前
鸿蒙 HarmonyOS 6 | 多媒体(05)全局播控 AVSession 接入与后台控制
华为·harmonyos