HarmonyOS APP 开源教程五:项目架构设计
本文介绍 HarmonyOS 应用的分层架构设计原则,包括表现层、组件层、服务层等职责划分。详细规划了项目目录结构,规范了命名规则。通过 Constants.ets 实现常量集中管理,使用 StorageUtil 处理持久化存储,并搭建服务层基础框架(TutorialService, ProgressService)。最后完成页面路由配置与项目骨架验证,为后续开发奠定基础。

本文介绍 HarmonyOS 应用的分层架构设计原则,包括表现层、组件层、服务层等职责划分。详细规划了项目目录结构,规范了命名规则。通过 Constants.ets 实现常量集中管理,使用 StorageUtil 处理持久化存储,并搭建服务层基础框架(TutorialService, ProgressService)。最后完成页面路由配置与项目骨架验证,为后续开发奠定基础。

良好的项目架构是应用可维护性和可扩展性的基础。本次课程将学习分层架构设计原则,规划项目目录结构,并搭建完整的项目骨架。
随着项目规模增长,如果所有代码都写在一起:
┌─────────────────────────────────────┐
│ 表现层 (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
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 |
在 entry/src/main/ets/common/ 下创建 Constants.ets:
/**
* 应用常量定义
* 集中管理所有常量,便于维护和修改
*/
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;
}
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');
现在,让我们创建项目的完整目录结构和基础文件。
在 entry/src/main/ets/ 下创建以下目录:
在 common/ 下创建存储工具:
/**
* 持久化存储工具类
*/
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);
}
}
}
/**
* 主题工具类
*/
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;
}
创建 services/TutorialService.ets:
/**
* 教程数据服务
*/
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:
/**
* 进度管理服务
*/
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;
}
}
更新 entry/src/main/resources/base/profile/main_pages.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 ✓
通过本次课程,你已经:
✅ 理解了分层架构设计原则 ✅ 掌握了项目目录结构规划 ✅ 学会了常量和配置的集中管理 ✅ 创建了存储工具类 ✅ 搭建了服务层基础框架 ✅ 完成了项目骨架搭建
第 6 次:数据模型设计与实现
我们将深入设计应用的数据模型:
完善的数据模型是应用功能的基础!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online