- 教育
- 后端
- 前端
【免费下载链接】oppia
A free, online learning platform to make quality education accessible for all.
本指南以 Oppia 仓库中 core/templates/pages/exploration-player-page/services/README.md 为核心骨架,结合该目录下 39 个 Angular 服务实现与配套测试,系统讲解共享服务层在“现有 Lesson Player”与“新 Lesson Player”双实现共存架构中的定位、职责划分、编码规范与测试方法。读完本文,你将掌握该目录内每个核心服务的功能边界、它们之间的调用关系,以及如何安全地新增或修改一个被多页面共享的 Angular 服务。
一、背景:为什么需要一个“共享服务层”
在 Oppia 中,学习者与“探索(Exploration)”交互的页面统称为 Lesson Player。目前仓库中存在两套并行的播放器实现:
- current-lesson-player:现有播放器,按其目录内 README.md 的说明,该实现已被标记为deprecated,待新播放器完全上线后将整体移除;
- new-lesson-player:新播放器,目标是更好的用户体验与可访问性、更强的移动端响应式表现,以及更清晰的模块化代码架构,其推进状态跟踪见 README.md。
两套播放器虽然 UI 与组件树不同,但底层有一大批业务逻辑是完全一致的:答案判定、卡片流转、提示/解答管理、进度统计、音频图片预加载……如果各自复制一份,必然导致行为漂移与维护灾难。因此 exploration-player-page 目录 下的 services 目录专门存放被两套播放器共享的服务文件,从源码结构看,它是整个播放器模块的“公共业务内核”,也是原 README 定义本文主题的出发点。
从目录级 README(exploration-player-page/README.md)可以确认这一分层决策:current-lesson-player与new-lesson-player各管各的组件,/services则承载两者共用的逻辑。
二、服务清单:39 个共享服务一图览
目录下共有 39 个*.service.ts文件,且每个服务都配有同名*.spec.ts单元测试(共 39 个),形成“一服务一测试”的强约束。按职责可划分为如下几组:
1. 引擎与模式(播放器的“大脑”)
| 服务 | 文件 | 核心职责 |
|---|---|---|
ExplorationEngineService | exploration-engine.service.ts | 探索(Exploration)的核心引擎:初始化探索、维护当前状态、解析答案、驱动卡片与反馈。从源码看它聚合了AnswerClassificationService、ImagePreloaderService、PlayerTranscriptService等十余个服务,是全目录依赖面最广的枢纽 |
QuestionPlayerEngineService | question-player-engine.service.ts | 练习题会话与学前测(pretest)模式的问题引擎,跟踪每题作答、提示使用与解答查看情况 |
DiagnosticTestPlayerEngineService | diagnostic-test-player-engine.service.ts | 诊断测试播放器引擎,配合DiagnosticTestTopicTrackerModel按主题/技能动态出题 |
CurrentEngineService | current-engine.service.ts | 三个引擎之上的动态委托器,通过setExplorationEngineService()/setQuestionPlayerEngineService()/setDiagnosticTestPlayerEngineService()切换当前活跃引擎,让上层组件无需耦合具体实现 |
ExplorationModeService | exploration-mode.service.ts | 通过 URL 与页面上下文判定播放模式,定义EXPLORATION_MODE枚举(EXPLORATION、EDITOR_PREVIEW、QUESTION_PLAYER、PRETEST、STORY_CHAPTER、LESSON_PLAYER、DIAGNOSTIC_TEST_PLAYER),并在切模式时同步切换CurrentEngineService |
2. 会话状态与流转(卡片、转录、位置)
| 服务 | 文件 | 核心职责 |
|---|---|---|
ConversationFlowService | conversation-flow.service.ts | 对话流总控,负责“提交答案→得到反馈→推进下一张卡”的主循环,聚合了转录、提示、统计、引擎等全部关键服务(约 1900 行,是全目录最复杂的服务) |
PlayerTranscriptService | player-transcript.service.ts | 维护学习过程的转录日志(看过哪些卡片、提交过哪些答案),支持restore/restoreImmutably恢复、addNewCard/addPreviousCard/addNewInput追加记录 |
PlayerPositionService | player-position.service.ts | 管理当前卡片位置、卡片序号与展示状态,是转录与视图之间的桥梁 |
CardAnimationService | card-animation.service.ts | 卡片切换动画控制 |
CurrentInteractionService | current-interaction.service.ts | 当前交互组件的运行时适配(提交答案、更新交互实例等) |
ChapterProgressService | chapter-progress.service.ts | 章节进度百分比计算与进度指示 |
3. 检查点(Checkpoint)与进度持久化
| 服务 | 文件 | 核心职责 |
|---|---|---|
CheckpointProgressService | checkpoint-progress.service.ts | 基于探索状态图的深度(depth graph)计算检查点列表,跟踪最近到达的检查点,支持跨会话进度恢复 |
CheckpointCelebrationUtilityService | checkpoint-celebration-utility.service.ts | 到达检查点时的庆祝弹窗等交互效用 |
ProgressUrlService | progress-url.service.ts | 生成携带进度参数的 URL,用于检查点跳转与恢复 |
NumberAttemptsService | number-attempts.service.ts | 以静态字段统计单卡内作答次数,供“学习者真的卡住”判定使用 |
4. 内容辅助:提示、解答、概念卡、模态框
| 服务 | 文件 | 核心职责 |
|---|---|---|
HintsAndSolutionManagerService | hints-and-solution-manager.service.ts | 提示与解答的释放节奏控制:定义了ACCELERATED_HINT_WAIT_TIME_MSEC = 10000、WAIT_FOR_TOOLTIP_TO_BE_SHOWN_MSEC = 500等关键常量,通过BehaviorSubject暴露已释放提示数 |
HintAndSolutionModalService | hint-and-solution-modal.service.ts | 提示/解答模态框的显示控制 |
ConceptCardManagerService | concept-card-manager.service.ts | 概念卡片的加载与展示管理 |
RefresherExplorationConfirmationModalService | refresher-exploration-confirmation-modal.service.ts | “推荐复习探索”确认模态框 |
FatigueDetectionService | fatigue-detection.service.ts | 学习者疲劳检测(连续作答异常时的提示) |
5. 资源预加载:图片、音频、翻译
| 服务 | 文件 | 核心职责 |
|---|---|---|
ImagePreloaderService | image-preloader.service.ts | 以 BFS 顺序按状态图预取图片到缓存,跟踪下载中/失败/待下载文件名,并暴露getImageUrl供组件按需取图 |
AudioPreloaderService | audio-preloader.service.ts | 音频文件的预加载缓存 |
AudioTranslationManagerService | audio-translation-manager.service.ts | 音频翻译(voiceover)的加载与播放管理 |
VoiceoverPlayerService | voiceover-player.service.ts | 配音播放器状态控制 |
ExtractImageFilenamesFromModelService | extract-image-filenames-from-model.service.ts | 从状态模型/HTML 中抽取图片文件名,供预加载器使用 |
ContentTranslationManagerService | content-translation-manager.service.ts | 内容多语言翻译的获取与切换 |
ContentTranslationLanguageService | content-translation-language.service.ts | 当前内容翻译语言状态管理 |
6. 答案与学习行为数据
| 服务 | 文件 | 核心职责 |
|---|---|---|
AnswerClassificationService | answer-classification.service.ts | 答案判定核心:按顺序匹配各答案组(AnswerGroup)内的规则,命中即返回AnswerClassificationResult;全部未命中则回退默认结果(DEFAULT_OUTCOME_CLASSIFICATION),无默认结果时告警并抛错。同时导出InteractionRulesService规则函数接口类型 |
LearnerAnswerInfoService | learner-answer-info.service.ts | 记录学习者的答案信息,供后续“继续练习/复习”分析 |
LearnerParamsService | learner-params.service.ts | 学习者参数(ExplorationParams)的计算与更新 |
LearnerViewRatingService | learner-view-rating.service.ts | 学习者评分逻辑(如探索结束后是否允许评分) |
StatsReportingService | stats-reporting.service.ts | 统计上报中枢:聚合各状态的作答数、有效反馈数、命中数、解答查看次数等,每 300000ms(5 分钟)通过StatsReportingBackendApiService批量上报后端,并集成SiteAnalyticsService与PlaythroughService |
7. 后端 API 封装与杂项
| 服务 | 文件 | 核心职责 |
|---|---|---|
FeedbackPopupBackendApiService | feedback-popup-backend-api.service.ts | 反馈弹窗的后端 API 封装 |
LearnerLocalNavBackendApiService | learner-local-nav-backend-api.service.ts | 学习者本地导航相关 API |
LearnerViewInfoBackendApiService | learner-view-info-backend-api.service.ts | 学习者视图信息 API |
LearnerViewRatingBackendApiService | learner-view-rating-backend-api.service.ts | 评分提交/获取 API |
ExplorationRecommendationsService | exploration-recommendations.service.ts | 完成探索后的推荐探索加载 |
MobileMenuService | mobile-menu.service.ts | 移动端菜单开关状态 |
8. 初始化
| 服务 | 文件 | 核心职责 |
|---|---|---|
ExplorationInitializationService | exploration-initialization.service.ts | 播放器的统一初始化入口。从其文件头注释可知,它是为了避免ExplorationEngineService与ExplorationModeService之间的循环依赖而引入的临时服务,负责按模式(编辑预览/正式播放)加载探索数据、特性开关、学前测问题并完成引擎初始化 |
从依赖方向上看,目录内存在清晰的分层结构:底层是AnswerClassificationService、PlayerTranscriptService、NumberAttemptsService等无副作用的基础服务;中间是ExplorationEngineService、QuestionPlayerEngineService、DiagnosticTestPlayerEngineService三个引擎;顶层是ConversationFlowService、ExplorationInitializationService与CurrentEngineService/ExplorationModeService组成的调度层。新播放器可以复用除顶层 UI 状态之外的几乎所有服务。
三、编码规范:README 的两条硬性要求
原 README 在 Guidelines 一节给出了两条必须遵守的开发准则,结合源码可以进一步明确其执行标准:
1. 为所有新增服务/新方法编写规范的 JsDoc
查看目录内的服务实现,可以看到每一处公开方法都有标准的 JsDoc 注释,例如 answer-classification.service.ts 中的classifyAnswer注释清晰说明了参数类型与返回值;image-preloader.service.ts 对kickOffImagePreloader(sourceStateName)的参数含义也做了逐行说明。
实践要点:
- 每个服务文件头部必须有
@fileoverview注释,一句话概括该服务的职责(本目录所有文件均遵守); - 每个公开方法写清参数类型、返回值类型与副作用;
- 如果属性初始化依赖 Angular 生命周期或
init方法,需要使用非空断言(!),并在注释中说明原因——这是 exploration-engine.service.ts 等文件的既有模式。
2. 服务被多组件、多页面共享,改动必须谨慎
从服务构造函数可以直观看到共享程度:例如 conversation-flow.service.ts 注入了 30 余个依赖,exploration-engine.service.ts 同样注入十余个服务。任何一个服务的签名或行为变化,都可能沿着依赖图波及其余数十个服务与组件。README 要求:
- 谨慎改动:优先保持方法签名兼容,新增行为而不是修改既有行为;
- 彻底测试:按下一节的流程执行完整验证。
四、测试策略:三层验证流程
原 README 在 Testing Recommendations 中给出了三层测试流程,以下是结合仓库实际的展开:
第一层:单元测试(Unit Tests)
每个服务都必须有配套的*.spec.ts,覆盖预期行为、边界情况与错误处理。目录内 39 个服务与 39 个测试文件一一对应。
以 answer-classification.service.spec.ts 为例,测试应至少覆盖:
- 正常命中:答案命中某个答案组的规则时,返回正确的
AnswerClassificationResult(含分组索引、规则索引); - 回退行为:所有规则均未命中时,返回默认结果;
- 错误处理:
defaultOutcome缺失时抛出异常,并触发AlertsService警告; - 边界情况:空答案组列表、空规则列表、特殊类型答案等。
对于有内部状态的服务(如 player-transcript.service.ts),还需覆盖状态恢复、重复添加卡片、首卡回退抛错等场景。
第二层:依赖组件测试(Dependent Component Tests)
由于服务被多个组件共享,单测通过不代表集成安全。需要运行所有依赖该服务的组件测试,检查是否产生非预期的副作用。在仓库中,播放器相关组件测试位于:
- exploration-player-page.component.spec.ts(现有播放器)
- lesson-player-page.component.spec.ts(新播放器)
修改任何共享服务后,建议同时跑通这两个播放器的组件测试,以及所有依赖注入链上的服务测试。
第三层:手动验证(Manual Verification)
自动化测试无法覆盖真实交互体验,README 特别强调手动走查应用,确认所有使用该服务的功能按预期工作。这对“新旧播放器共享”的服务尤其重要。建议手动验证的路径包括:
- 以
/explore/:id进入现有播放器完成一次完整探索(作答→反馈→提示→解答→下一卡); - 开启新播放器特性开关后以
/lesson/:id重复上述流程(开关方式见 new-lesson-player/README.md); - 验证编辑器预览模式(
/create/:id)、练习题会话与诊断测试模式下的服务行为; - 验证弱网/慢加载场景下图片与音频预加载的降级表现。
注:新播放器通过
new_lesson_player特性开关控制,开启后所有/explore/:id链接会自动重定向至/lesson/:id。因此双实现验证可以高效合并:开关开启时走新播放器,关闭时走旧播放器,手动确认两套 UI 下共享服务行为一致。
五、新增共享服务的完整实践路径
结合 README 规范与仓库既有实现,新增一个共享服务的推荐流程如下:
- 先评估共享必要性:确认该逻辑确实会被两套播放器共用;若仅属于某一套播放器的 UI 表现,应放入对应播放器目录(
current-lesson-player/或new-lesson-player/)而非 services 目录; - 创建服务文件:参照 number-attempts.service.ts 的最小实现范式——文件头版权声明与
@fileoverview、@Injectable({providedIn: 'root'})、公开方法带完整 JsDoc; - 编写单元测试:创建同名
.spec.ts,覆盖预期行为、边界与错误分支; - 接入依赖注入:在需要处注入服务;若服务间有循环依赖风险,参照
ExplorationInitializationService的“临时服务解耦”思路拆分; - 跑依赖测试:执行依赖该服务的组件测试(新旧播放器两侧);
- 手动走查:在真实页面中验证新旧播放器下的行为一致性。
六、总结
exploration-player-page/services目录是 Oppia 播放器前端的“公共内核”:39 个共享服务以 Angular 依赖注入组织成“基础服务 → 三大引擎 → 调度层”的清晰层次,支撑着现有与新两代 Lesson Player。其 README 强调的两条准则——规范 JsDoc、谨慎改共享代码——以及“单元测试 → 依赖组件测试 → 手动验证”的三层流程,正是保障这套被多页面共用的服务层长期稳定演进的工程基石。对于计划向播放器新增能力的开发者,本文的服务清单、调用链分析与测试路径可作为直接的手册与查漏工具。
- 教育
- 后端
- 前端
【免费下载链接】oppia
A free, online learning platform to make quality education accessible for all.
相关推荐
SoundSwitch Services 层架构指南:面向服务集成的设计边界与源码实现剖析
SoundSwitch Services 层架构指南:面向服务集成的设计边界与源码实现剖析 SoundSwitch 是一个用 C / WinForms 编写的默
桌面应用Vuetify 共享组合式函数(Shared Composables)开发规范与源码级实践指南
Vuetify 共享组合式函数(Shared Composables)开发规范与源码级实践指南 导读 Vuetify 的核心组件库由数百个共享组合式函数(com
前端UI组件Cherry Studio 跨进程共享层(@shared)架构指南:五目录封闭集合、双不变量与放置决策
Cherry Studio 跨进程共享层(@shared)架构指南:五目录封闭集合、双不变量与放置决策 Cherry Studio 是一个基于 Electron
人工智能大模型AI 应用交互助手本地部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考