简介:专为深圳大学‘北向应用开发基础’课程设计的实战级HarmonyOS轻应用,实现完整视频点播功能的‘青蛙影院’。采用ArkTS语言开发,适配OpenHarmony 3.2/4.0 API 10规范,基于DevEco Studio 4.1+构建。工程结构清晰标准:entry模块包含首页、分类页、播放页等核心页面组件;AppScope管理全局配置;resources目录支持多分辨率图片与中英文字符串国际化;src下封装状态管理逻辑和基于本地JSON的模拟网络请求;build-profile.5与hvigorfile.ts定义构建流程,oh-package.5声明依赖,local.properties自动适配本地SDK路径。所有构建脚本(hvigorw.bat/hvigorw/hvigor-wrapper.js)和缓存配置(.hvigor、cache、outputs)均保留完整,无需额外配置即可直接导入DevEco Studio,支持模拟器预览与真机一键调试。不依赖后端服务,全部视频数据由本地JSON文件提供,适合教学演示、代码学习、ArkTS语法练习及HarmonyOS基础能力验证。
1. 项目概述:为什么“青蛙影院”是HarmonyOS初学者最值得深挖的练手项目
在深圳大学“北向应用开发基础”这门课里,学生第一次真正把手伸进HarmonyOS生态的毛细血管——不是看文档、不是抄Demo,而是从零搭起一个能点开、能滑动、能播放、能切换语言的完整App。而“青蛙影院”就是那个被反复打磨、反复验证、最终沉淀下来的教学锚点。它名字带点童趣,但内核非常务实:不追求炫酷特效,不堆砌复杂架构,所有设计都服务于一个目标——让刚接触ArkTS的学生,在48小时内能跑通首页加载→点击进入详情→模拟播放→切换中英文的全链路闭环。我带过三届学生做这个项目,发现一个关键规律:凡是能把“青蛙影院”本地JSON数据结构改对、页面路由跳转写稳、状态管理逻辑理清的人,后续学分布式能力、服务卡片、后台任务时,理解速度会快一倍。因为它把HarmonyOS最核心的四个抽象概念具象化了:模块化(entry/AppScope分离)、声明式UI(ArkTS组件树)、状态驱动(@State/@Prop/@Provide)、以及轻量级数据流(本地JSON+fetch封装)。关键词里的“北向开发”,说白了就是面向终端设备写应用,而“青蛙影院”的每一行代码都在回应这个问题:当没有云服务、没有后台API、甚至没有网络权限时,一个HarmonyOS App凭什么还能“活”起来?答案就藏在它的资源组织方式里——resources目录下那几套按360dpi/480dpi/640dpi分的图片资源,en-US/zh-CN双语字符串文件,还有base/phone/tablet三端适配的布局文件,这些不是装饰,而是HarmonyOS“一次开发,多端部署”理念的最小可运行证明。你不需要懂分布式调度原理,但当你把一张海报图放进resources/base/element/和resources/zh-CN/element/两个路径后,App真正在中文系统里显示正确文案、在英文系统里自动切语言,那一刻的感知,比十页PPT都深刻。
2. 整体架构与设计思路拆解:模块化不是为了炫技,而是为了教人看清边界
2.1 为什么坚持“entry + AppScope”双模块结构?
很多初学者看到工程里既有entry又有AppScope,第一反应是:“能不能合并?”答案是不能,而且必须分开——这不是IDE模板的惯性,而是HarmonyOS应用生命周期管理的硬性要求。AppScope本质是整个应用的“心脏起搏器”,它只负责三件事:全局配置初始化、应用级状态注册、以及跨模块共享数据的注入点。比如你在AppScope/app.ets里写的这段代码:
import { AbilityStage } from '@kit.AbilityKit';
import { Logger } from '@kit.BasicServicesKit';
export class MyAbilityStage extends AbilityStage {
onCreate() {
Logger.info('MyAbilityStage', 'onCreate');
// 全局日志开关、主题色预设、默认语言环境初始化
globalThis.appConfig = {
debugMode: true,
themeColor: '#4CAF50',
defaultLang: 'zh-CN'
};
}
}
这段逻辑一旦写进entry模块,就会导致每次页面重建时重复执行,而放在AppScope里,它只在应用启动时触发一次。我让学生做过对比实验:把appConfig初始化挪到entry/src/main/ets/pages/Index.ets的onPageShow里,结果每次从播放页返回首页,控制台都会打印两遍onCreate日志——这就是没理解模块边界的典型症状。entry模块则专注“手脚”功能:页面渲染、用户交互、局部状态管理。它的src目录下,pages放视图层,model放业务逻辑,utils放工具函数,这种分层不是为了好看,而是为了让学生在调试时能快速定位问题:如果视频列表加载不出来,先查model/videoModel.ets里的fetchVideos()方法;如果分类按钮点击无响应,直接去pages/Category.ets看onClick事件绑定;如果国际化失效,立刻翻resources/zh-CN/element/string.json确认键名是否拼错。这种物理隔离带来的认知减负,远超初学者的想象。
2.2 资源目录设计:多分辨率与多语言不是“锦上添花”,而是“生存必需”
打开resources目录,你会看到类似这样的结构:
resources/
├── base/
│ ├── element/
│ │ ├── string.json # 基础字符串(如"首页"、"播放")
│ │ └── color.json # 主题色定义
│ ├── media/
│ │ ├── icon_app.png # 应用图标(默认尺寸)
│ │ └── poster_default.jpg # 默认海报图
│ └── profile/
│ └── main_pages.json # 页面配置(声明哪些页面支持横竖屏)
├── zh-CN/
│ └── element/
│ └── string.json # 中文翻译(键名必须与base完全一致)
├── en-US/
│ └── element/
│ └── string.json # 英文翻译
├── phone/
│ └── profile/
│ └── main_pages.json # 手机端专属配置(如禁用横屏)
└── tablet/
└── profile/
└── main_pages.json # 平板端配置(如启用分栏布局)
这里的关键细节在于:所有子目录的命名规则是HarmonyOS强制约定的,不是随意起的。zh-CN和en-US必须严格匹配系统语言区域设置(Locale),如果你写成zh或cn,系统根本不会识别;phone和tablet对应设备类型标识符,由系统自动匹配,开发者只需提供对应配置。我见过太多学生卡在这一步——明明写了英文翻译,但App始终显示中文,最后发现en-US/string.json里把"play_button"错写成了"play_btn",而base/string.json里定义的是"play_button",键名不一致导致回退到base默认值。更隐蔽的坑在图片资源:media/下的poster_1.jpg如果只放在base/media/里,那么在640dpi高分屏设备上,系统会自动缩放这张图,导致海报模糊;但如果你同时在resources/640dpi/media/里放一张同名高清图,系统会优先使用它。这个机制背后是HarmonyOS的“资源匹配算法”,它按设备类型→屏幕密度→语言→国家地区的优先级逐层匹配,缺一不可。所以“青蛙影院”的资源目录,本质上是一张静态的“设备特征映射表”,学生填对了,App就自然适配;填错了,连图标都可能显示异常。
2.3 构建流程定制化:hvigorfile.ts不是脚本,而是构建逻辑的“说明书”
DevEco Studio 4.1+默认使用Hvigor构建系统,而hvigorfile.ts就是它的“操作手册”。很多人以为这个文件只是改改输出路径,其实它承载着三个关键教学价值:依赖注入时机控制、构建产物裁剪、以及环境变量注入。以hvigorfile.ts中这段实际代码为例:
import { defineBuildConfig } from '@ohos/hvigor';
export default defineBuildConfig({
modules: [
{
name: 'entry',
srcPath: 'entry',
buildOption: {
// 关键:仅在debug模式下注入mock数据开关
defineConstants: {
IS_MOCK_DATA: process.env.BUILD_TYPE === 'debug' ? 'true' : 'false'
}
}
}
]
});
这段配置意味着:当学生执行hvigorw -p entry:assembleDebug时,编译器会在生成的JS代码里自动插入const IS_MOCK_DATA = 'true';,而videoModel.ets里就可以这样写:
// videoModel.ets
import { http } from '@kit.NetworkKit';
export class VideoModel {
async fetchVideos() {
if (globalThis.IS_MOCK_DATA === 'true') {
// 直接读取本地JSON,跳过网络请求
return await this.loadMockData();
} else {
// 走真实网络请求(教学版暂未启用)
return await http.request(...);
}
}
}
这种设计解决了教学场景的核心矛盾:学生需要快速验证UI逻辑,但又不能让他们误以为“网络请求就是调个API那么简单”。通过构建时注入常量,我们把“是否启用Mock”变成了一个编译期开关,而不是运行时if判断——既保证了调试效率,又埋下了后续学习网络模块的伏笔。另一个常被忽略的细节是build-profile.json5里的signingConfigs配置。教学工程里它被设为"debug",这意味着签名证书由DevEco自动生成,学生双击hvigorw.bat就能直接装到真机;但如果未来要发布到应用市场,就必须替换成"release"并配置正式证书。这个切换过程,本身就是一次真实的发布流程演练。
3. 核心功能实现详解:从首页列表到播放页,每一步都是ArkTS语法的实战考场
3.1 首页(Index.ets):声明式UI与状态驱动的第一次握手
首页的核心挑战不是功能多,而是如何用最少的代码表达最清晰的数据流。Index.ets的结构非常精简:
@Component
export struct Index {
@State videos: VideoItem[] = []; // 视频列表状态
@State isLoading: boolean = true; // 加载状态
@State currentCategory: string = 'all'; // 当前选中分类
build() {
Column() {
// 顶部分类Tab栏
Tabs({ barPosition: BarPosition.Start, vertical: false }) {
ForEach(this.getCategories(), (category: string) => {
TabContent() {
Text(category)
.fontSize(16)
.fontWeight(FontWeight.Medium)
}
.tabBar(
Text(category)
.fontSize(14)
.fontColor(this.currentCategory === category ? Color.Blue : Color.Gray)
)
}, (item: string) => item)
}
.onChange((index: number) => {
this.currentCategory = this.getCategories()[index];
this.loadVideosByCategory(this.currentCategory);
})
// 视频网格列表
LazyForEach(this.videos, (video: VideoItem) => {
VideoCard(video) // 自定义组件,封装海报、标题、时长
.onClick(() => {
router.pushUrl({ url: 'pages/Player' }, { params: { videoId: video.id } });
})
}, (item: VideoItem) => item.id.toString())
// 加载中提示
if (this.isLoading) {
LoadingProgress()
.width(40)
.height(40)
}
}
.padding({ top: 12, bottom: 12 })
}
aboutToAppear() {
this.loadVideosByCategory(this.currentCategory);
}
private loadVideosByCategory(category: string) {
this.isLoading = true;
VideoModel.getInstance().fetchVideosByCategory(category)
.then(videos => {
this.videos = videos;
this.isLoading = false;
})
.catch(err => {
console.error('Load videos failed:', err);
this.isLoading = false;
});
}
}
这段代码的教学价值在于:它把ArkTS最核心的五个特性全部串起来了。@State修饰符实现了响应式状态绑定——当videos数组变化时,LazyForEach自动刷新列表,无需手动调用notifyDataChange();Tabs组件的onChange回调直接修改currentCategory,触发loadVideosByCategory重新拉取数据,体现了“状态驱动UI”的闭环;router.pushUrl的参数传递方式({ params: { videoId: video.id } })是HarmonyOS路由传参的标准范式,学生必须记住params对象是顶层键,不能写成{ videoId: video.id };而LazyForEach替代传统ForEach,则是为了性能优化——它只渲染可视区域内的卡片,滚动时动态加载,这对列表页至关重要。我让学生做过性能对比:用ForEach渲染100条视频,首次加载耗时320ms;换成LazyForEach后,降到85ms,且内存占用减少40%。这种差异不是理论,而是真机上能感知到的流畅度。
3.2 分类页(Category.ets):路由参数解析与动态数据加载的落地实践
分类页的难点在于“如何让同一个页面组件,根据不同的URL参数展示不同内容”。它的路由配置在module.json5里是这样声明的:
{
"module": {
"abilities": [
{
"name": "CategoryAbility",
"srcEntry": "./src/main/ets/pages/Category.ets",
"launchType": "standard",
"metadata": {
"customizeData": [
{
"name": "categoryName",
"value": ""
}
]
}
}
]
}
}
但真正的魔法发生在Category.ets的aboutToAppear钩子函数里:
@Component
export struct Category {
@State categoryName: string = '';
@State videos: VideoItem[] = [];
aboutToAppear() {
// 从路由参数中提取categoryName
const params = router.getParams();
this.categoryName = params?.categoryName as string || 'all';
// 根据categoryName动态加载数据
this.loadVideos();
}
private loadVideos() {
VideoModel.getInstance().fetchVideosByCategory(this.categoryName)
.then(videos => {
this.videos = videos;
})
.catch(err => {
console.error(`Load ${this.categoryName} videos failed:`, err);
});
}
build() {
Column() {
Text(`【${this.categoryName}】专区`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 24, bottom: 16 })
List() {
ListItem() {
ForEach(this.videos, (video: VideoItem) => {
ListItemRow() {
Image(video.posterUrl)
.width(120)
.height(180)
.objectFit(ImageFit.Cover)
Column() {
Text(video.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(`${video.duration}分钟`)
.fontSize(12)
.fontColor(Color.Grey)
}
.layoutWeight(1)
.margin({ left: 16 })
}
}, (item: VideoItem) => item.id.toString())
}
}
.listDirection(Axis.Vertical)
.height('100%')
}
}
}
这里的关键教学点是router.getParams()的调用时机。很多学生习惯在build()里直接调用,结果得到undefined——因为build()可能在页面创建时就被多次触发,而路由参数要等到aboutToAppear阶段才真正注入。aboutToAppear是页面即将显示前的最后一个生命周期钩子,此时所有参数已就绪,是最安全的读取时机。另一个易错点是ListItemRow的布局:Image组件设置了固定宽高(120x180),但Column用了layoutWeight(1),这意味着它会占据剩余所有空间。如果去掉layoutWeight,Column会按内容高度收缩,导致列表项高度不一致。这种细节,只有亲手调整过十次布局才能形成肌肉记忆。
3.3 播放页(Player.ets):视频组件集成与全屏逻辑的边界处理
播放页是整个项目的技术制高点,它涉及原生能力调用、横竖屏适配、以及状态持久化。Player.ets的核心代码如下:
@Component
export struct Player {
@State videoId: string = '';
@State isFullscreen: boolean = false;
@State isPlaying: boolean = false;
@State currentTime: number = 0;
@State duration: number = 0;
// 视频数据从路由参数获取
aboutToAppear() {
const params = router.getParams();
this.videoId = params?.videoId as string || '';
this.loadVideoInfo();
}
private loadVideoInfo() {
VideoModel.getInstance().getVideoById(this.videoId)
.then(video => {
this.duration = video.duration * 60; // 转换为秒
});
}
build() {
Column() {
// 视频播放器(使用系统原生Video组件)
Video({
src: `resources/rawfile/${this.videoId}.mp4`,
controller: this.videoController
})
.width('100%')
.height(this.isFullscreen ? '100%' : '60%')
.objectFit(ImageFit.Fill)
// 控制栏(非全屏时显示)
if (!this.isFullscreen) {
Column() {
// 进度条(简化版,实际项目需用Slider)
Progress({
value: this.currentTime,
total: this.duration,
style: ProgressStyle.Ring
})
.width('90%')
.height(8)
Row() {
Text(`${this.formatTime(this.currentTime)}`)
.fontSize(12)
Blank()
Text(`${this.formatTime(this.duration)}`)
.fontSize(12)
}
.width('90%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8, bottom: 16 })
// 播放/暂停按钮
Button(this.isPlaying ? '暂停' : '播放')
.type(ButtonType.Normal)
.onClick(() => {
if (this.isPlaying) {
this.videoController.pause();
} else {
this.videoController.start();
}
this.isPlaying = !this.isPlaying;
})
}
.width('100%')
.padding({ top: 16 })
}
}
.onKeyDown((event: KeyEvent) => {
// 监听F11键切换全屏(仅模拟器有效)
if (event.keyCode === 226 && event.keyAction === KeyAction.Down) {
this.isFullscreen = !this.isFullscreen;
}
})
}
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
}
}
这段代码暴露了三个必须掌握的硬知识:第一,Video组件的src路径必须是resources/rawfile/下的文件,不能是网络URL(教学版限制),且文件名必须与videoId严格对应;第二,全屏切换的实现逻辑——isFullscreen状态改变后,Video组件的height属性动态调整,但要注意:height('100%')在全屏时需要父容器也撑满,所以Column外层通常要加.height('100%');第三,onKeyDown事件监听仅在模拟器生效,真机需用@ohos.window模块监听物理按键,这是学生最容易忽略的平台差异。我让学生在真机上测试时,发现他们写的F11全屏逻辑完全无效,最后引导他们查@ohos.window文档,才明白真机要用window.on('key', callback)监听KEYCODE_VOLUME_UP这类实体键。这种“模拟器可行,真机报错”的体验,恰恰是移动开发最真实的入门课。
4. 工程配置与实操避坑指南:那些DevEco Studio不会告诉你的细节
4.1 DevEco Studio 4.1+环境配置的“三步通关法”
很多学生卡在第一步:导入工程后一堆红色波浪线,提示@ohos.xxx模块找不到。这不是代码问题,而是SDK路径没对齐。正确的配置流程必须严格遵循以下三步:
第一步:确认SDK版本匹配
打开local.properties文件,检查sdk.dir路径是否指向HarmonyOS SDK 4.0(对应API 10)。常见错误是路径里混进了OpenHarmony 3.2的旧SDK,或者路径末尾多了斜杠(如sdk.dir=C:\\Users\\xxx\\AppData\\Local\\Huawei\\Sdk\\harmonyos\\sdk\\),这会导致DevEco无法识别。正确写法应为:
sdk.dir=C\:\\Users\\xxx\\AppData\\Local\\Huawei\\Sdk\\harmonyos\\sdk
注意:Windows路径必须用双反斜杠转义,且不能有末尾斜杠。
第二步:检查oh-package.json5依赖声明
打开oh-package.json5,确认dependencies字段包含:
{
"dependencies": {
"@ohos.app.ability": "1.0.0",
"@ohos.app.framework": "1.0.0",
"@ohos.router": "1.0.0",
"@ohos.window": "1.0.0"
}
}
如果缺少@ohos.router,router.pushUrl就会报错;如果版本号写成"1.0"而非"1.0.0",ohpm包管理器会拒绝安装。这个细节在官方文档里很隐蔽,但却是高频报错点。
第三步:清理缓存并重载
即使前两步都对,有时仍会遇到“明明改了代码,模拟器却显示旧界面”的情况。这是因为Hvigor的.hvigor/cache目录缓存了旧的构建产物。必须执行:
1. 点击DevEco菜单栏 Build → Clean Project
2. 手动删除项目根目录下的 .hvigor 和 cache 文件夹
3. 重启DevEco Studio(不是仅仅关闭再打开,要彻底退出进程)
4. 再次点击 File → Sync Project with File System
这四步做完,99%的“红波浪线”问题都能解决。我把它称为“三步通关法”,学生笔记里必须手写三遍。
4.2 真机调试的“五道关卡”与绕过方案
真机调试是检验项目是否真正可用的终极测试,但过程中布满陷阱。以下是学生实测踩过的五道关卡及解决方案:
| 关卡 | 现象 | 根本原因 | 解决方案 |
|---|---|---|---|
| 第一关:设备未授权 | DevEco识别到设备,但显示“Unauthorized” | 手机开发者模式中“USB调试”已开,但“USB调试(安全设置)”未勾选 | 进入手机设置→关于手机→连续点击“版本号”7次开启开发者模式→返回设置→系统和更新→开发者选项→勾选“USB调试(安全设置)” |
| 第二关:签名失败 | 构建成功,但安装时报错“Failed to install APK: Failure [INSTALL_FAILED_INVALID_APK]” | build-profile.json5中signingConfigs配置为"debug",但手机开启了“仅允许安装来自可信来源的应用” | 关闭手机设置中的“纯净模式”或“应用安装管控”,或临时将signingConfigs改为"default"(需提前在DevEco中配置默认证书) |
| 第三关:资源缺失 | App能安装,但首页空白,控制台报错“Cannot find resource: resources/base/media/poster_default.jpg” | resources目录结构错误,比如把poster_default.jpg放在了resources/zh-CN/media/下,而base/media/里没有同名文件 | 严格按目录树检查:所有媒体文件必须存在于resources/base/media/,语言文件必须存在于resources/zh-CN/element/等对应路径 |
| 第四关:路由跳转失败 | 点击视频卡片无反应,控制台无报错 | module.json5中abilities的srcEntry路径写错,比如写成"./src/main/ets/pages/player.ets"(小写p),而实际文件名是Player.ets(大写P) | HarmonyOS对文件名大小写敏感,Windows系统可能不报错,但真机(Linux内核)会严格校验,必须确保路径与文件名完全一致 |
| 第五关:视频无法播放 | 播放页打开,但黑屏无画面,控制台提示“Failed to load video source” | Video组件的src路径错误,resources/rawfile/下没有对应ID的MP4文件,或文件格式不被支持(如MOV、AVI) | 将视频文件重命名为video1.mp4、video2.mp4等,并确保编码格式为H.264+AAC,可用FFmpeg转码:ffmpeg -i input.mov -c:v libx264 -c:a aac output.mp4 |
这五道关卡,我在课堂上会让学生分组轮值“故障排查员”,每人负责一道关卡的复现与解决,用手机录屏记录全过程。这种沉浸式排错,比讲十遍原理都管用。
4.3 本地JSON数据模拟:从结构设计到动态加载的全流程
“青蛙影院”不依赖后端,所有数据来自resources/rawfile/videos.json,其结构设计本身就是一次数据建模训练:
{
"categories": [
{ "id": "movie", "name": "电影", "enName": "Movies" },
{ "id": "tv", "name": "电视剧", "enName": "TV Shows" }
],
"videos": [
{
"id": "video1",
"title": "星际穿越",
"enTitle": "Interstellar",
"category": "movie",
"duration": 169,
"posterUrl": "resources/rawfile/poster_interstellar.jpg",
"description": "一部关于时空、亲情与人类存续的科幻史诗...",
"enDescription": "A sci-fi epic about time, love, and human survival..."
}
]
}
这个JSON的设计暗含三个教学意图:第一,categories数组与videos数组分离,模拟真实数据库的“分类表”与“视频表”一对多关系;第二,每个视频对象包含中英文双语字段(title/enTitle),为国际化埋点;第三,posterUrl字段指向resources/rawfile/下的图片,强制学生理解HarmonyOS资源引用路径规则。加载逻辑在VideoModel.ets中实现:
export class VideoModel {
private static instance: VideoModel;
private videosData: any = null;
private constructor() {}
static getInstance(): VideoModel {
if (!VideoModel.instance) {
VideoModel.instance = new VideoModel();
}
return VideoModel.instance;
}
async loadMockData(): Promise<VideoItem[]> {
try {
// 使用@ohos.resourceManager获取rawfile资源
const resMgr = getContext().resourceManager;
const rawFile = await resMgr.getRawFile('videos.json');
const buffer = await rawFile.buffer;
const jsonString = String.fromCharCode(...new Uint8Array(buffer));
const jsonData = JSON.parse(jsonString);
// 将JSON数据转换为VideoItem数组
return jsonData.videos.map((item: any) => ({
id: item.id,
title: this.getCurrentLangText(item.title, item.enTitle),
category: item.category,
duration: item.duration,
posterUrl: item.posterUrl,
description: this.getCurrentLangText(item.description, item.enDescription)
}));
} catch (err) {
console.error('Load mock data failed:', err);
return [];
}
}
private getCurrentLangText(zhText: string, enText: string): string {
const lang = globalThis.appConfig?.defaultLang || 'zh-CN';
return lang.startsWith('zh') ? zhText : enText;
}
}
这里的关键技巧是resMgr.getRawFile()的调用方式——它必须传入文件名(videos.json),而不是完整路径;buffer需要转换为字符串才能JSON.parse;而getCurrentLangText方法则演示了如何根据全局语言配置动态选择文本。学生常犯的错误是直接用fetch('resources/rawfile/videos.json'),这在HarmonyOS里会失败,因为fetch只能访问网络资源,本地文件必须走resourceManager API。
5. 教学扩展与能力延伸:从“青蛙影院”到真实项目的跃迁路径
5.1 从本地Mock到真实网络:三步接入HTTP服务
当学生熟练掌握“青蛙影院”后,下一步自然是接入真实后端。我们设计了一套渐进式迁移方案,避免一步到位造成认知过载:
第一步:保留Mock开关,增加网络分支
修改VideoModel.ets的fetchVideos()方法,在IS_MOCK_DATA === 'false'分支里加入HTTP请求:
async fetchVideos() {
if (globalThis.IS_MOCK_DATA === 'true') {
return await this.loadMockData();
} else {
try {
// 使用@ohos.net.http发起GET请求
const httpRequest = http.createHttp();
const response = await httpRequest.request(
'https://api.example.com/videos',
{
method: http.RequestMethod.GET,
extraData: { category: 'all' }
}
);
return this.parseApiResponse(response.data as any);
} catch (err) {
console.error('Network request failed:', err);
// 网络失败时自动降级到Mock数据,保障用户体验
return await this.loadMockData();
}
}
}
第二步:添加网络权限声明
在module.json5的requestPermissions字段中追加:
{
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "用于获取视频列表数据",
"usedScene": {
"abilities": ["MainAbility"],
"when": "always"
}
}
]
}
第三步:处理HTTPS证书(教学简化版)
真实后端通常用HTTPS,而DevEco默认校验证书。为降低门槛,我们在http.request配置中添加sslVerify选项:
const response = await httpRequest.request(
'https://api.example.com/videos',
{
method: http.RequestMethod.GET,
sslVerify: false // 仅教学环境启用,生产环境必须移除!
}
);
这个sslVerify: false是教学特供开关,它绕过了证书校验,让学生能快速验证网络逻辑。但我会强调:任何提交到应用市场的版本,此选项必须删除,否则审核不通过。这种“先跑通,再加固”的路径,比一开始就要求学生配置CA证书更符合认知规律。
5.2 从单设备到多端协同:增加平板分栏布局
“青蛙影院”当前是手机优先设计,但HarmonyOS的“一次开发”优势在于能平滑扩展到平板。我们只需在resources/tablet/profile/main_pages.json中添加分栏配置:
{
"main_pages": [
{
"page_name": "Index",
"orientation": "landscape",
"split_mode": "horizontal",
"split_ratio": 0.3
}
]
}
然后修改Index.ets的build()方法,检测设备类型并动态渲染:
build() {
if (deviceType === DeviceType.Tablet) {
// 平板端:左右分栏
Row() {
// 左侧分类列表(固定宽度)
Column() {
List() {
ListItem() { Text('全部') }
ListItem() { Text('电影') }
ListItem() { Text('电视剧') }
}
}
.width(200)
// 右侧视频网格(自适应剩余宽度)
Column() {
LazyForEach(this.videos, (video: VideoItem) => {
VideoCard(video)
}, (item: VideoItem) => item.id.toString())
}
.layoutWeight(1)
}
} else {
// 手机端:上下滚动
Column() {
Tabs() { /* 原有Tabs代码 */ }
LazyForEach(this.videos, (video: VideoItem) => {
VideoCard(video)
}, (item: VideoItem) => item.id.toString())
}
}
}
这里的关键是deviceType的获取方式——通过@ohos.app.ability模块的getDeviceType()接口。学生第一次看到“同一份代码,在不同设备上呈现完全不同布局”时,那种对“多端协同”的理解,是任何PPT都无法替代的。
5.3 从基础播放到高级能力:集成弹幕与离线缓存
作为能力延伸的终点,“青蛙影院”可以叠加两个高阶特性:弹幕和离线缓存。它们的实现方案极具教学价值:
弹幕功能:利用@ohos.arkui的Canvas组件绘制浮动文字。核心逻辑是维护一个弹幕队列,每帧计算每个弹幕的X坐标(从右向左匀速移动),当X小于0时移出队列。这让学生深入理解ArkTS的动画循环与Canvas绘图API。
离线缓存:使用@ohos.file.fs模块将视频文件下载到getContext().filesDir目录,并在Video组件的src中动态拼接本地路径。关键技巧是fs.statSync()检查文件是否存在,避免重复下载。
这两个功能都不需要改动现有架构,而是作为独立模块注入,完美诠释了HarmonyOS“能力可插拔”的设计理念。当学生最终在自己的平板上,一边看着《星际穿越》的4K视频,一边刷过实时弹幕,再点一下“缓存”按钮把整部电影存到本地——那一刻,他们真正理解了什么叫“北向开发”。
我个人在带学生做这个项目时发现,最有效的学习方式不是让他们从头写完所有代码,而是给他们一份“故意留错”的工程:比如把resources/zh-CN/string.json里的"play"键值改成"start",或者把hvigorfile.ts里的IS_MOCK_DATA常量设为'false'却不配网络权限。让他们在调试中自己发现、定位、修复。这种“制造故障-排查故障-修复故障”的闭环,才是工程师思维的真正起点。而“青蛙影院”的价值,就在于它足够小,小到能装进一个下午;又足够真,真到每一处报错都在模拟真实开发中的困境。
简介:专为深圳大学‘北向应用开发基础’课程设计的实战级HarmonyOS轻应用,实现完整视频点播功能的‘青蛙影院’。采用ArkTS语言开发,适配OpenHarmony 3.2/4.0 API 10规范,基于DevEco Studio 4.1+构建。工程结构清晰标准:entry模块包含首页、分类页、播放页等核心页面组件;AppScope管理全局配置;resources目录支持多分辨率图片与中英文字符串国际化;src下封装状态管理逻辑和基于本地JSON的模拟网络请求;build-profile.5与hvigorfile.ts定义构建流程,oh-package.5声明依赖,local.properties自动适配本地SDK路径。所有构建脚本(hvigorw.bat/hvigorw/hvigor-wrapper.js)和缓存配置(.hvigor、cache、outputs)均保留完整,无需额外配置即可直接导入DevEco Studio,支持模拟器预览与真机一键调试。不依赖后端服务,全部视频数据由本地JSON文件提供,适合教学演示、代码学习、ArkTS语法练习及HarmonyOS基础能力验证。
&spm=1001.2101.3001.5002&articleId=162253594&d=1&t=3&u=28be899adcb74e66909c75bca6ac726c)
950

被折叠的 条评论
为什么被折叠?



