第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 ✓
本次课程小结
通过本次课程,你已经:
✅ 理解了分层架构设计原则
✅ 掌握了项目目录结构规划
✅ 学会了常量和配置的集中管理
✅ 创建了存储工具类
✅ 搭建了服务层基础框架
✅ 完成了项目骨架搭建
课后练习
-
添加日志工具:创建 LogUtil.ets,封装日志输出方法
-
扩展常量:添加更多应用配置常量
-
创建更多服务:按照模板创建 BookmarkService.ets
下次预告
第6次:数据模型设计与实现
我们将深入设计应用的数据模型:
- 学习模块数据结构
- 课程内容数据结构
- 用户进度数据结构
- 测验相关数据结构
完善的数据模型是应用功能的基础!