跳到主要内容HarmonyOS APP 开源教程五:项目架构设计 | 极客日志TypeScript大前端
HarmonyOS APP 开源教程五:项目架构设计
综述由AI生成介绍 HarmonyOS 应用的分层架构设计原则,包括表现层、组件层、服务层等职责划分。详细规划了项目目录结构,规范了命名规则。通过 Constants.ets 实现常量集中管理,使用 StorageUtil 处理持久化存储,并搭建服务层基础框架(TutorialService, ProgressService)。最后完成页面路由配置与项目骨架验证,为后续开发奠定基础。
忘忧28 浏览 第 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:
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';
}
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');
5.4 实操:搭建完整项目骨架
步骤 1:创建目录结构
在 entry/src/main/ets/ 下创建以下目录:
- common/
- components/
- data/
- models/
- services/
步骤 2:创建 StorageUtil.ets
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);
}
}
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
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:
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;
}
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;
}
}
步骤 5:更新页面路由配置
更新 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 ✓
本次课程小结
✅ 理解了分层架构设计原则
✅ 掌握了项目目录结构规划
✅ 学会了常量和配置的集中管理
✅ 创建了存储工具类
✅ 搭建了服务层基础框架
✅ 完成了项目骨架搭建
课后练习
- 添加日志工具:创建 LogUtil.ets,封装日志输出方法
- 扩展常量:添加更多应用配置常量
- 创建更多服务:按照模板创建 BookmarkService.ets
下次预告
- 学习模块数据结构
- 课程内容数据结构
- 用户进度数据结构
- 测验相关数据结构
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online