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

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 # 应用入口 

命名规范

类型命名规则示例
页面PascalCaseIndex.ets, ModuleDetail.ets
组件PascalCaseModuleCard.ets, CodeBlock.ets
服务PascalCase + ServiceTutorialService.ets
工具PascalCase + UtilStorageUtil.ets
常量UPPER_SNAKE_CASEAPP_NAME, PRIMARY_COLOR
接口PascalCaseLearningModule, UserProgress

5.3 常量管理:Constants.ets

创建常量文件

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

/** * 应用常量定义 * 集中管理所有常量,便于维护和修改 *//** * 应用基本信息 */exportclassAppConstants{staticreadonlyAPP_NAME:string='React 学习教程';staticreadonlyAPP_VERSION:string='1.0.0';staticreadonlyPREFERENCES_NAME:string='react_tutorial_prefs';}/** * React 品牌色 */exportclassReactColors{staticreadonlyPRIMARY:string='#61DAFB';staticreadonlyPRIMARY_DARK:string='#20232a';staticreadonlySECONDARY_DARK:string='#282c34';staticreadonlyGRADIENT_START:string='#61DAFB';staticreadonlyGRADIENT_END:string='#21a0c4';}/** * 浅色主题颜色 */exportclassLightThemeColors{staticreadonlyBACKGROUND:string='#f8f9fa';staticreadonlyCARD_BACKGROUND:string='#ffffff';staticreadonlyTEXT_PRIMARY:string='#1a1a2e';staticreadonlyTEXT_SECONDARY:string='#495057';staticreadonlyDIVIDER:string='#e9ecef';}/** * 深色主题颜色 */exportclassDarkThemeColors{staticreadonlyBACKGROUND:string='#1a1a2e';staticreadonlyCARD_BACKGROUND:string='#282c34';staticreadonlyTEXT_PRIMARY:string='#ffffff';staticreadonlyTEXT_SECONDARY:string='#d1d5db';staticreadonlyDIVIDER:string='#3d3d5c';}/** * 难度等级颜色 */exportclassDifficultyColors{staticreadonlyBEGINNER:string='#51cf66';staticreadonlyBASIC:string='#339af0';staticreadonlyINTERMEDIATE:string='#ff922b';staticreadonlyADVANCED:string='#ff6b6b';staticreadonlyECOSYSTEM:string='#9775fa';}/** * 存储键名 */exportclassStorageKeys{staticreadonlyUSER_PROGRESS:string='user_progress';staticreadonlyBOOKMARKS:string='bookmarks';staticreadonlyTHEME_MODE:string='theme_mode';staticreadonlyQUIZ_HISTORY:string='quiz_history';staticreadonlyWRONG_ANSWERS:string='wrong_answers';staticreadonlyQUIZ_STATISTICS:string='quiz_statistics';}/** * 路由路径 */exportclassRoutePaths{staticreadonlyINDEX:string='pages/Index';staticreadonlyMODULE_DETAIL:string='pages/ModuleDetail';staticreadonlyLESSON_DETAIL:string='pages/LessonDetail';staticreadonlyQUIZ_PAGE:string='pages/QuizPage';staticreadonlyQUIZ_BANK:string='pages/QuizBankPage';staticreadonlyCODE_PLAYGROUND:string='pages/CodePlayground';staticreadonlySEARCH:string='pages/SearchPage';staticreadonlyBOOKMARK:string='pages/BookmarkPage';}/** * 难度等级类型 */exporttypeDifficultyLevel='beginner'|'basic'|'intermediate'|'advanced'|'ecosystem';/** * 难度等级显示名称 */exportconst DifficultyNames: Record<DifficultyLevel,string>={'beginner':'入门','basic':'基础','intermediate':'进阶','advanced':'高级','ecosystem':'生态'};/** * 获取难度等级颜色 */exportfunctiongetDifficultyColor(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;}/** * 应用配置 */exportclassAppConfig{staticreadonlyQUIZ_PASSING_SCORE:number=60;staticreadonlySTREAK_BADGE_DAYS:number=7;staticreadonlyMAX_WRONG_ANSWERS:number=100;staticreadonlySEARCH_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

common/ 下创建存储工具:

/** * 持久化存储工具类 */import{ preferences }from'@kit.ArkData';import{ AppConstants }from'./Constants';exportclassStorageUtil{privatestatic preferencesInstance: preferences.Preferences |null=null;/** * 初始化 */staticasyncinit(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 实例 */privatestaticgetPreferences(): preferences.Preferences {if(!StorageUtil.preferencesInstance){thrownewError('StorageUtil not initialized');}return StorageUtil.preferencesInstance;}/** * 获取字符串 */staticasyncgetString(key:string, defaultValue:string=''):Promise<string>{try{const prefs = StorageUtil.getPreferences();returnawait prefs.get(key, defaultValue)asstring;}catch(error){console.error(`[StorageUtil] getString failed for ${key}:`, error);return defaultValue;}}/** * 设置字符串 */staticasyncsetString(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);}}/** * 获取对象 */staticasyncgetObject<T>(key:string, defaultValue:T):Promise<T>{try{const prefs = StorageUtil.getPreferences();const jsonStr =await prefs.get(key,'')asstring;if(!jsonStr)return defaultValue;returnJSON.parse(jsonStr)asT;}catch(error){console.error(`[StorageUtil] getObject failed for ${key}:`, error);return defaultValue;}}/** * 设置对象 */staticasyncsetObject<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);}}/** * 删除键 */staticasyncremove(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);}}/** * 清空所有数据 */staticasyncclear():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';exportenum ThemeMode {AUTO='auto',LIGHT='light',DARK='dark'}exportinterfaceThemeColors{ background:string; cardBackground:string; textPrimary:string; textSecondary:string; divider:string;}exportconst LightTheme: ThemeColors ={ background: LightThemeColors.BACKGROUND, cardBackground: LightThemeColors.CARD_BACKGROUND, textPrimary: LightThemeColors.TEXT_PRIMARY, textSecondary: LightThemeColors.TEXT_SECONDARY, divider: LightThemeColors.DIVIDER};exportconst DarkTheme: ThemeColors ={ background: DarkThemeColors.BACKGROUND, cardBackground: DarkThemeColors.CARD_BACKGROUND, textPrimary: DarkThemeColors.TEXT_PRIMARY, textSecondary: DarkThemeColors.TEXT_SECONDARY, divider: DarkThemeColors.DIVIDER};exportfunctioninitTheme(context: Context):void{ AppStorage.setOrCreate('isDarkMode',false); AppStorage.setOrCreate('themeMode', ThemeMode.LIGHT);}exportfunctiontoggleTheme():void{const isDark = AppStorage.get<boolean>('isDarkMode')??false; AppStorage.set('isDarkMode',!isDark); AppStorage.set('themeMode',!isDark ? ThemeMode.DARK: ThemeMode.LIGHT);}exportfunctiongetThemeColors(isDarkMode:boolean): ThemeColors {return isDarkMode ? DarkTheme : LightTheme;}

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

创建 services/TutorialService.ets

/** * 教程数据服务 */import{ LearningModule, Lesson, DifficultyType }from'../models/Models';exportclassTutorialService{privatestatic initialized:boolean=false;privatestatic modules: LearningModule[]=[];/** * 初始化服务 */staticinit():void{if(TutorialService.initialized)return;// 后续会加载实际数据 TutorialService.initialized =true;console.info('[TutorialService] Initialized');}/** * 获取所有模块 */staticgetAllModules(): LearningModule[]{return TutorialService.modules;}/** * 根据 ID 获取模块 */staticgetModuleById(id:string): LearningModule |undefined{return TutorialService.modules.find(m => m.id === id);}/** * 根据难度获取模块 */staticgetModulesByDifficulty(difficulty: DifficultyType): LearningModule[]{return TutorialService.modules.filter(m => m.difficulty === difficulty);}/** * 获取总课程数 */staticgetTotalLessonCount():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';exportclassProgressService{privatestatic cachedProgress: UserProgress |null=null;/** * 加载用户进度 */staticasyncloadProgress():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);returnDEFAULT_USER_PROGRESS;}}/** * 保存用户进度 */staticasyncsaveProgress(progress: UserProgress):Promise<void>{try{await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress); ProgressService.cachedProgress = progress;}catch(error){console.error('[ProgressService] Save failed:', error);}}/** * 标记课程完成 */staticasyncmarkLessonComplete(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);}}/** * 计算模块完成百分比 */staticgetCompletionPercentage(module: LearningModule, progress: UserProgress):number{if(module.lessons.length ===0)return0;const completed = module.lessons.filter( l => progress.completedLessons.includes(l.id)).length;return Math.round((completed / module.lessons.length)*100);}/** * 更新连续学习天数 */staticasyncupdateStreak():Promise<number>{const progress =await ProgressService.loadProgress();const today =newDate().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((newDate(today).getTime()-newDate(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 ✓ 

本次课程小结

通过本次课程,你已经:

✅ 理解了分层架构设计原则
✅ 掌握了项目目录结构规划
✅ 学会了常量和配置的集中管理
✅ 创建了存储工具类
✅ 搭建了服务层基础框架
✅ 完成了项目骨架搭建


课后练习

  1. 添加日志工具:创建 LogUtil.ets,封装日志输出方法
  2. 扩展常量:添加更多应用配置常量
  3. 创建更多服务:按照模板创建 BookmarkService.ets

下次预告

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

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

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

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

Read more

使用 Bright Data Web Scraper API + Python 高效抓取 Glassdoor 数据:从配置到结构化输出全流程实战

使用 Bright Data Web Scraper API + Python 高效抓取 Glassdoor 数据:从配置到结构化输出全流程实战

使用 Bright Data Web Scraper API + Python 高效抓取 Glassdoor 数据:从配置到结构化输出全流程实战 摘要 本文详细介绍了如何使用 Bright Data 的 Web Scraper API 搭配 Python,实现对 Glassdoor 平台信息的高效抓取。通过 API 请求构建器、反爬机制集成与结构化数据输出,开发者可轻松获取高质量网页数据,适用于招聘分析、AI 训练与商业情报等场景,同时介绍了 Bright Data 的 Deep Lookup 功能,通过自然语言指令实现深度数据挖掘,进一步拓展数据采集的智能化能力。 前言 数字化商业时代,网页数据蕴含着市场洞察的宝藏,从 AI 模型训练的高质量素材,到商业分析、市场调研与竞争情报的核心依据,结构化网页数据成为开发者的

By Ne0inhk
一文了解Blob文件格式,前端必备技能之一

一文了解Blob文件格式,前端必备技能之一

文章目录 * 前言 * 一、什么是Blob? * 二、Blob的基本特性 * 三、Blob的构造函数 * 四、常见使用场景 * 1. 文件下载 * 2. 图片预览 * 3. 大文件分片上传 * 四、Blob与其他API的关系 * 1. File API * 2. FileReader * 3. URL.createObjectURL() * 4. Response * 五、性能与内存管理 * 六、实际案例:导出Word文档 * 七、浏览器兼容性 * 八、总结 前言 最近在项目中需要导出文档时,我首次接触到了 Blob 文件格式。作为一个前端开发者,虽然经常听到 "Blob" 这个术语,但对其具体原理和应用场景并不十分了解。经过一番研究和实践,

By Ne0inhk
Flutter 组件 inappwebview_cookie_manager 适配 鸿蒙Harmony 实战 - 驾驭核心大 Web 容器缓存隧道、构建金融级政企应用绝对防串号跨域大隔离基座

Flutter 组件 inappwebview_cookie_manager 适配 鸿蒙Harmony 实战 - 驾驭核心大 Web 容器缓存隧道、构建金融级政企应用绝对防串号跨域大隔离基座

Flutter 组件 inappwebview_cookie_manager 适配鸿蒙 HarmonyOS 实战:构建金融级政企应用的绝对防串号、跨域隔离基座 前言 在鸿蒙(OpenHarmony)生态全面爆发的元年,尤其是在涉及极高密级的政务信创办公系统,或是动辄千万流水、每日亿级请求的金融级应用中,一个核心的安全问题浮出水面:“如何在原生系统底层、Flutter 视图层,以及那些杂乱不可控的第三方或历史遗留的 Web/H5 容器之间,实现身份Cookie或核心Token的绝对安全、单向透传,并具备强力的清理能力?” 这个问题一旦处理不当,哪怕只是露出一丝缝隙,都可能在极短时间内引发全应用的恶性串号、账目混乱,甚至导致严重的数据越权泄露,成为整个系统的“核爆级”架构黑洞。 如果你的前端团队仍然只是粗糙地打开一个毫无防护的 WebView,并天真地指望业务层每次都能主动、无遗漏地手动清理缓存和密码,那么你的应用在断网重连、异地登录或多并发场景下,极易因 Session 未能彻底清除而发生严重的“串绑撞车”事故。更可怕的是,由于缺乏统一管控,各类敏感

By Ne0inhk
【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦

【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦

目录 【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦 一、为什么要做全局错误处理? 1、将业务逻辑与错误处理解耦 2、为监控和埋点提供统一入口 二、Vue 中的基础全局错误处理方式 1、Vue 中全局错误处理写法 2、它会捕获哪些错误? 3、它不会捕获哪些错误? 4、errorHandler 的参数含义 三、全局错误处理的进阶设计 1、定义“可识别的业务错误” 2、在 errorHandler 中做真正的“分类处理” 3、补齐 Promise reject 的捕获能力 4、错误处理的策略化封装 四、结语         作者:watermelo37         ZEEKLOG优质创作者、华为云云享专家、阿里云专家博主、腾讯云“

By Ne0inhk