HarmonyOS APP<玩转React>开源教程十一:组件化开发概述

第12次:教程数据服务层

服务层是应用架构中的关键一环,负责封装业务逻辑和数据访问。本次课程将学习如何设计和实现 TutorialService,为应用提供统一的数据访问接口。


学习目标

  • 理解服务层的设计模式
  • 掌握静态类的使用方法
  • 学会数据缓存策略
  • 实现服务初始化流程
  • 完成 TutorialService 的完整开发

12.1 服务层设计模式

什么是服务层?

服务层(Service Layer)是一种架构模式,位于 UI 层和数据层之间:

复制代码
┌─────────────────────────────────────┐
│           UI 层 (Pages)             │
├─────────────────────────────────────┤
│         服务层 (Services)           │  ← 本次课程重点
├─────────────────────────────────────┤
│          数据层 (Data)              │
└─────────────────────────────────────┘

服务层的职责

职责 说明
数据访问封装 统一数据获取接口
业务逻辑处理 实现业务规则
数据转换 格式化和处理数据
缓存管理 提高数据访问效率
错误处理 统一异常处理

为什么需要服务层?

  1. 解耦:UI 层不直接依赖数据层
  2. 复用:多个页面可共享同一服务
  3. 测试:便于单元测试
  4. 维护:修改数据源只需改服务层

12.2 静态类设计

静态类 vs 实例类

在 ArkTS 中,服务层通常使用静态类:

typescript 复制代码
// 静态类:通过类名直接调用
export class TutorialService {
  static getAllModules(): LearningModule[] {
    // ...
  }
}

// 使用
const modules = TutorialService.getAllModules();
typescript 复制代码
// 实例类:需要创建实例
export class TutorialService {
  getAllModules(): LearningModule[] {
    // ...
  }
}

// 使用
const service = new TutorialService();
const modules = service.getAllModules();

静态类的优势

  1. 无需实例化:直接调用,使用简单
  2. 全局单例:天然的单例模式
  3. 内存效率:不创建多余对象

静态属性与方法

typescript 复制代码
export class TutorialService {
  // 静态属性
  private static initialized: boolean = false;
  private static cachedModules: LearningModule[] = [];

  // 静态方法
  static init(): void {
    // 初始化逻辑
  }

  static getAllModules(): LearningModule[] {
    // 获取数据逻辑
  }
}

12.3 数据缓存策略

为什么需要缓存?

每次调用 TutorialData.getAllModules() 都会重新构建数据,影响性能。通过缓存可以:

  1. 减少重复计算
  2. 提高响应速度
  3. 降低内存分配

缓存实现

typescript 复制代码
export class TutorialService {
  private static initialized: boolean = false;
  private static cachedModules: LearningModule[] = [];

  static getAllModules(): LearningModule[] {
    // 如果未初始化,先初始化
    if (!TutorialService.initialized) {
      TutorialService.init();
    }
    // 返回缓存数据
    return TutorialService.cachedModules;
  }

  static init(): void {
    if (TutorialService.initialized) {
      return;  // 避免重复初始化
    }

    // 加载数据到缓存
    TutorialService.cachedModules = TutorialData.getAllModules();
    TutorialService.initialized = true;
  }
}

缓存失效策略

在某些场景下需要刷新缓存:

typescript 复制代码
static refreshCache(): void {
  TutorialService.cachedModules = TutorialData.getAllModules();
  console.info('[TutorialService] Cache refreshed');
}

12.4 服务初始化流程

初始化时机

服务初始化通常在应用启动时进行:

typescript 复制代码
// Index.ets
aboutToAppear(): void {
  // 初始化主题
  initTheme(getContext(this));
  
  // 初始化教程服务
  TutorialService.init();
  
  // 加载数据
  this.loadData();
}

初始化依赖

TutorialService 初始化时会级联初始化其他服务:

typescript 复制代码
static init(): void {
  if (TutorialService.initialized) {
    return;
  }

  // 1. 加载模块数据
  TutorialService.cachedModules = TutorialData.getAllModules();

  // 2. 初始化搜索服务(依赖模块数据)
  SearchService.init(TutorialService.cachedModules);

  // 3. 初始化测验服务
  QuizService.init(TutorialData.getAllQuizzes());

  // 4. 初始化徽章服务
  BadgeService.init(TutorialService.cachedModules);

  TutorialService.initialized = true;
  console.info('[TutorialService] Initialized');
}

初始化顺序

复制代码
TutorialService.init()
    │
    ├── 加载 TutorialData
    │
    ├── SearchService.init()
    │
    ├── QuizService.init()
    │
    └── BadgeService.init()

12.5 数据访问方法

获取所有模块

typescript 复制代码
static getAllModules(): LearningModule[] {
  if (!TutorialService.initialized) {
    TutorialService.init();
  }
  return TutorialService.cachedModules;
}

根据 ID 获取模块

typescript 复制代码
static getModuleById(id: string): LearningModule | undefined {
  return TutorialService.getAllModules().find(m => m.id === id);
}

根据 ID 获取课程

typescript 复制代码
static getLessonById(moduleId: string, lessonId: string): Lesson | undefined {
  const module = TutorialService.getModuleById(moduleId);
  return module?.lessons.find(l => l.id === lessonId);
}

按难度筛选模块

typescript 复制代码
static getModulesByDifficulty(difficulty: DifficultyType): LearningModule[] {
  return TutorialService.getAllModules().filter(m => m.difficulty === difficulty);
}

统计方法

typescript 复制代码
// 获取总课程数
static getTotalLessonCount(): number {
  return TutorialService.getAllModules().reduce(
    (total, module) => total + module.lessons.length,
    0
  );
}

// 获取总模块数
static getTotalModuleCount(): number {
  return TutorialService.getAllModules().length;
}

// 按难度统计模块数
static getModuleCountByDifficulty(): Map<DifficultyType, number> {
  const counts = new Map<DifficultyType, number>();
  const difficulties: DifficultyType[] = ['beginner', 'basic', 'intermediate', 'advanced', 'ecosystem'];

  for (const diff of difficulties) {
    counts.set(diff, TutorialService.getModulesByDifficulty(diff).length);
  }

  return counts;
}

12.6 实操:实现 TutorialService.ets

步骤一:创建服务文件

entry/src/main/ets/services/ 目录下创建 TutorialService.ets 文件。

步骤二:编写完整服务代码

typescript 复制代码
/**
 * 教程数据服务
 * 封装 TutorialData 的访问方法
 */
import { TutorialData } from '../data/TutorialData';
import { LearningModule, Lesson, Quiz, DailyQuestion, DifficultyType } from '../models/Models';
import { SearchService } from './SearchService';
import { QuizService } from './QuizService';
import { BadgeService } from './BadgeService';

/**
 * 教程服务类
 */
export class TutorialService {
  // 初始化标志
  private static initialized: boolean = false;
  
  // 缓存的模块数据
  private static cachedModules: LearningModule[] = [];

  /**
   * 初始化服务
   * 加载数据并初始化依赖服务
   */
  static init(): void {
    if (TutorialService.initialized) {
      return;
    }

    // 加载所有模块到缓存
    TutorialService.cachedModules = TutorialData.getAllModules();

    // 初始化搜索服务
    SearchService.init(TutorialService.cachedModules);

    // 初始化测验服务
    QuizService.init(TutorialData.getAllQuizzes());

    // 初始化徽章服务
    BadgeService.init(TutorialService.cachedModules);

    TutorialService.initialized = true;
    console.info('[TutorialService] Initialized with', TutorialService.cachedModules.length, 'modules');
  }

  /**
   * 获取所有模块
   * @returns 所有学习模块数组
   */
  static getAllModules(): LearningModule[] {
    if (!TutorialService.initialized) {
      TutorialService.init();
    }
    return TutorialService.cachedModules;
  }

  /**
   * 根据 ID 获取模块
   * @param id 模块 ID
   * @returns 模块对象或 undefined
   */
  static getModuleById(id: string): LearningModule | undefined {
    return TutorialService.getAllModules().find(m => m.id === id);
  }

  /**
   * 根据 ID 获取课程
   * @param moduleId 模块 ID
   * @param lessonId 课程 ID
   * @returns 课程对象或 undefined
   */
  static getLessonById(moduleId: string, lessonId: string): Lesson | undefined {
    const module = TutorialService.getModuleById(moduleId);
    return module?.lessons.find(l => l.id === lessonId);
  }

  /**
   * 按难度获取模块
   * @param difficulty 难度等级
   * @returns 指定难度的模块数组
   */
  static getModulesByDifficulty(difficulty: DifficultyType): LearningModule[] {
    return TutorialService.getAllModules().filter(m => m.difficulty === difficulty);
  }

  /**
   * 获取每日一题
   * @returns 每日一题对象
   */
  static getDailyQuestion(): DailyQuestion {
    return TutorialData.getDailyQuestion();
  }

  /**
   * 获取课程测验
   * @param lessonId 课程 ID
   * @returns 测验对象或 undefined
   */
  static getQuizForLesson(lessonId: string): Quiz | undefined {
    return TutorialData.getQuizForLesson(lessonId);
  }

  /**
   * 获取所有测验
   * @returns 所有测验数组
   */
  static getAllQuizzes(): Quiz[] {
    return TutorialData.getAllQuizzes();
  }

  /**
   * 获取总课程数
   * @returns 所有模块的课程总数
   */
  static getTotalLessonCount(): number {
    return TutorialService.getAllModules().reduce(
      (total, module) => total + module.lessons.length,
      0
    );
  }

  /**
   * 获取总模块数
   * @returns 模块总数
   */
  static getTotalModuleCount(): number {
    return TutorialService.getAllModules().length;
  }

  /**
   * 获取各难度级别的模块数
   * @returns 难度到模块数的映射
   */
  static getModuleCountByDifficulty(): Map<DifficultyType, number> {
    const counts = new Map<DifficultyType, number>();
    const difficulties: DifficultyType[] = ['beginner', 'basic', 'intermediate', 'advanced', 'ecosystem'];

    for (const diff of difficulties) {
      counts.set(diff, TutorialService.getModulesByDifficulty(diff).length);
    }

    return counts;
  }
}

步骤三:在页面中使用服务

更新 Index.ets,使用 TutorialService:

typescript 复制代码
import { TutorialService } from '../services/TutorialService';
import { LearningModule } from '../models/Models';

@Entry
@Component
struct Index {
  @State modules: LearningModule[] = [];
  @State isLoading: boolean = true;

  aboutToAppear(): void {
    this.initAndLoadData();
  }

  private async initAndLoadData(): Promise<void> {
    try {
      // 初始化存储
      await StorageUtil.init(getContext(this));
      
      // 初始化主题
      initTheme(getContext(this));
      
      // 初始化教程服务并获取数据
      TutorialService.init();
      this.modules = TutorialService.getAllModules();
      
      console.info('[Index] Loaded', this.modules.length, 'modules');
    } catch (error) {
      console.error('[Index] Failed to load data:', error);
    } finally {
      this.isLoading = false;
    }
  }

  build() {
    // UI 代码...
  }
}

步骤四:使用服务获取数据

typescript 复制代码
// 获取所有模块
const allModules = TutorialService.getAllModules();

// 获取入门模块
const beginnerModules = TutorialService.getModulesByDifficulty('beginner');

// 获取特定模块
const module = TutorialService.getModuleById('beginner-01');

// 获取特定课程
const lesson = TutorialService.getLessonById('beginner-01', 'b01-l01');

// 获取统计数据
const totalLessons = TutorialService.getTotalLessonCount();
const totalModules = TutorialService.getTotalModuleCount();

// 获取每日一题
const dailyQuestion = TutorialService.getDailyQuestion();

服务层最佳实践

1. 单一职责

每个服务只负责一个领域:

typescript 复制代码
TutorialService  // 教程数据
ProgressService  // 学习进度
QuizService      // 测验功能
BookmarkService  // 收藏功能

2. 错误处理

在服务层统一处理错误:

typescript 复制代码
static getModuleById(id: string): LearningModule | undefined {
  try {
    return TutorialService.getAllModules().find(m => m.id === id);
  } catch (error) {
    console.error('[TutorialService] getModuleById error:', error);
    return undefined;
  }
}

3. 日志记录

添加适当的日志便于调试:

typescript 复制代码
static init(): void {
  console.info('[TutorialService] Initializing...');
  // ...
  console.info('[TutorialService] Initialized with', count, 'modules');
}

4. 类型安全

使用 TypeScript 类型确保类型安全:

typescript 复制代码
static getModulesByDifficulty(difficulty: DifficultyType): LearningModule[] {
  // DifficultyType 限制了参数只能是有效的难度值
}

本次课程小结

通过本次课程,你已经:

✅ 理解了服务层的设计模式

✅ 掌握了静态类的使用方法

✅ 学会了数据缓存策略

✅ 实现了服务初始化流程

✅ 完成了 TutorialService 的完整开发


课后练习

  1. 添加搜索方法:实现根据关键词搜索模块和课程

  2. 添加排序方法:实现按难度、课时数等排序

  3. 添加分页方法:实现模块列表分页获取


下次预告

第13次:教程数据内容编写

我们将学习如何组织教程内容数据:

  • 入门模块内容设计
  • 基础模块内容设计
  • 进阶模块内容设计
  • 数据结构最佳实践

开始填充真实的教程内容!

相关推荐
Flutter笔记2 小时前
独立开发了一个睡眠记录 App:SleepDiary / 睡眠声音日记
前端
YimWu2 小时前
面试官:能聊聊 oh-my-opencode 这个插件都有啥内容吗?
前端·agent·ai编程
前端付豪2 小时前
AI Tutor v4:学习路径推荐(Learning Path)
前端·python·llm
爱吃的小肥羊2 小时前
等了整整一年,Midjourney V8今天终于开放测试了!
前端
玉米Yvmi2 小时前
给 JS穿上铠甲:TypeScript 基础核心概念详解(类型/接口/泛型)
前端·javascript·typescript
搞个锤子哟2 小时前
js并发请求,且限制并发请求数量实现方案
前端
米丘2 小时前
vue-router v5.x createRouter 是创建路由实例?
前端·vue.js
阿蒙Amon2 小时前
C#常用类库-详解JetBrains.Annotations
前端·数据库·c#
lichenyang4532 小时前
Next.js 初学者核心知识点
前端