最近在指导学弟学妹做毕业设计时,发现一个挺普遍的现象:很多同学用 ArkTS 开发的应用,功能虽然实现了,但代码结构一团乱麻,所有逻辑都堆在 UI 页面里,稍微改点需求就牵一发而动全身,更别提后续维护和扩展了。这其实就是典型的“Demo 思维”,只求功能跑通,忽略了软件工程中至关重要的高内聚、低耦合原则。今天,我就以构建一个“校园服务应用”(整合课表查询与通知推送)为例,分享一下如何用 ArkTS 从零开始,搭建一个结构清晰、易于维护的毕业设计项目。
1. 背景痛点:为什么你的毕业设计只是“玩具”?
很多同学的项目都存在以下几个通病,导致其离一个“合格”的毕业设计相去甚远:
- 逻辑混杂,难以维护:所有网络请求、数据处理、业务逻辑和 UI 渲染代码都写在
@Component装饰的页面文件中。一个文件动辄几百行,查找和修改bug如同大海捞针。 - 缺乏状态管理,数据流混乱:页面间传递数据靠层层
prop透传,或者滥用AppStorage和LocalStorage,导致数据更新来源不明,状态同步困难。 - 网络请求裸奔,错误处理缺失:直接在
aboutToAppear或按钮事件里写http.createHttp().request,没有统一的封装。错误处理(如网络异常、服务器错误)要么没有,要么每个页面重复写一遍。 - 无视性能,体验卡顿:大量同步操作阻塞主线程,图片不经处理直接加载,列表滚动卡顿,应用冷启动缓慢,这些都会让答辩老师对你的技术能力产生质疑。
- 毫无测试,稳定性存疑:整个项目没有单元测试或集成测试,功能是否正常全靠手动点击。一旦修改代码,根本无法快速验证是否引入了新问题。
2. 技术选型:为什么是 ArkTS?
对于鸿蒙生态下的毕业设计,技术栈选择很关键。我们对比一下:
- 传统 Web 前端 (Vue/React) + 小程序:虽然生态成熟,但无法直接利用鸿蒙系统的原生能力(如分布式软总线、原子化服务),性能有上限,且作为毕业设计,技术新颖性不足。
- 纯 Java/JS 开发:早期的鸿蒙开发方式,在声明式 UI 和开发效率上已不占优势,且 ArkTS 正是它们的演进和整合。
- ArkTS:它是鸿蒙生态的“亲儿子”语言,基于 TypeScript,提供了声明式 UI 范式。其优势在于:
- 性能更优:通过方舟编译器生成高效机器码,UI 渲染性能接近原生。
- 开发高效:声明式语法让 UI 描述更直观,状态管理(
@State,@Prop,@Link)与 UI 自动绑定,减少胶水代码。 - 能力全面:直接、便捷地调用所有鸿蒙系统 API,为应用添加分布式、原子化等特色功能铺平了道路。
- 类型安全:TypeScript 的强类型系统能在编码阶段发现大量潜在错误,这对大型项目或团队协作至关重要。
因此,选择 ArkTS 不仅能做出功能完备的应用,更能体现你对前沿技术栈的掌握,为毕业设计增加含金量。
3. 核心实现:构建清晰的分层架构
我们的目标是构建一个“课表查询+通知推送”的应用。核心是设计一个分层架构,通常可以划分为:数据模型层(Model)、服务/逻辑层(Service/ViewModel)、视图层(View)。
3.1 项目结构与分层
src/main/ets/ ├── entryability ├── pages │ └── Index.ets // 视图层-主页面 ├── model │ ├── Course.ts // 数据模型-课程 │ └── Notification.ts // 数据模型-通知 ├── service │ ├── ApiService.ts // 网络请求封装 │ ├── CourseService.ts // 课程业务逻辑 │ └── NotificationService.ts // 通知业务逻辑 ├── utils │ ├── http.ts // 网络请求底层封装 │ ├── storage.ts // 数据持久化工具 │ └── logger.ts // 日志工具 └── viewmodel ├── CourseViewModel.ts // 课程视图模型(状态管理) └── NotificationViewModel.ts // 通知视图模型3.2 网络模块封装 (utils/http.ts)
这是基础中的基础。我们不应该在每个需要网络请求的地方都创建HttpClient实例。
// utils/http.ts import http from '@ohos.net.http'; import { logger } from './logger'; // 自定义日志工具 class HttpRequest { private client: http.HttpClient; constructor() { this.client = http.createHttp(); } // 统一的请求方法 async request<T>(url: string, options: http.HttpRequestOptions): Promise<T> { try { const response = await this.client.request(url, options); const result = JSON.parse(response.result as string); // 假设后端统一返回格式 { code: 0, data: T, message: 'success' } if (result.code === 0) { return result.data as T; } else { // 业务逻辑错误 logger.error(`API Error: ${url}`, result.message); throw new Error(result.message || 'Request failed'); } } catch (error) { // 网络或解析错误 logger.error(`Network Error: ${url}`, error); throw new Error('Network request failed'); } finally { // 重要:释放资源,避免内存泄漏 this.client.destroy(); } } // 封装 GET 请求 get<T>(url: string, params?: Record<string, string>): Promise<T> { const fullUrl = params ? `${url}?${new URLSearchParams(params).toString()}` : url; return this.request(fullUrl, { method: http.RequestMethod.GET }); } // 封装 POST 请求 post<T>(url: string, data: object): Promise<T> { return this.request(url, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json' }, extraData: JSON.stringify(data) }); } } export const httpRequest = new HttpRequest();3.3 视图模型与状态管理 (viewmodel/CourseViewModel.ts)
这是连接视图和数据的桥梁,负责管理页面状态和业务逻辑。
// viewmodel/CourseViewModel.ts import { Course } from '../model/Course'; import { CourseService } from '../service/CourseService'; export class CourseViewModel { // 使用 @State 装饰器管理状态,当状态变更时,依赖它的UI会自动更新 @State courseList: Array<Course> = []; @State isLoading: boolean = false; @State errorMessage: string = ''; private courseService: CourseService; constructor() { this.courseService = new CourseService(); } // 加载课程数据 async loadCourses(week: number): Promise<void> { // 防止重复加载 if (this.isLoading) { return; } this.isLoading = true; this.errorMessage = ''; try { const data = await this.courseService.fetchCourses(week); this.courseList = data; } catch (error) { this.errorMessage = `加载失败: ${error.message}`; logger.error('Failed to load courses', error); } finally { this.isLoading = false; } } // 其他业务方法,如添加、删除课程等 addCourse(course: Course): void { // 先更新本地状态,提供即时反馈 this.courseList = [...this.courseList, course]; // 再异步同步到服务器 this.courseService.syncAddCourse(course).catch(e => { // 如果同步失败,可以回滚或提示用户 logger.error('Sync add course failed', e); }); } }3.4 视图层 (pages/Index.ets)
视图层只关心如何展示数据和接收用户交互,将复杂的逻辑委托给 ViewModel。
// pages/Index.ets import { CourseViewModel } from '../viewmodel/CourseViewModel'; @Entry @Component struct Index { // 在组件中实例化或通过依赖注入获取 ViewModel private courseVM: CourseViewModel = new CourseViewModel(); build() { Column({ space: 20 }) { // 标题 Text('我的课表').fontSize(30).fontWeight(FontWeight.Bold) // 加载状态提示 if (this.courseVM.isLoading) { LoadingProgress().width(50).height(50) Text('加载中...') } // 错误信息提示 if (this.courseVM.errorMessage) { Text(this.courseVM.errorMessage) .fontColor(Color.Red) .fontSize(14) } // 课程列表 List({ space: 10 }) { ForEach(this.courseVM.courseList, (item: Course) => { ListItem() { CourseItem({ course: item }) // 使用子组件,进一步解耦 } }, (item: Course) => item.id.toString()) } .layoutWeight(1) // 占据剩余空间 .width('100%') // 刷新按钮 Button('刷新课表') .onClick(() => { // 视图层只触发动作,具体逻辑在ViewModel中 this.courseVM.loadCourses(1); }) .width('80%') } .padding(20) .width('100%') .height('100%') .onPageShow(() => { // 页面显示时加载数据 this.courseVM.loadCourses(1); }) } } // 独立的课程项子组件 @Component struct CourseItem { @Prop course: Course; // 通过Prop接收数据 build() { Row({ space: 10 }) { Column({ space: 5 }) { Text(this.course.name).fontSize(18).fontWeight(FontWeight.Medium) Text(`${this.course.time} @ ${this.course.location}`).fontSize(14).fontColor(Color.Gray) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) } .padding(15) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 5, color: '#00000010', offsetX: 0, offsetY: 2 }) .width('100%') } }4. 性能与安全:不可忽视的环节
4.1 启动耗时优化
- 延迟加载与按需加载:对于非首屏必需的模块(如某些复杂工具库、子页面),使用动态导入(
import())或路由懒加载。 - 资源优化:图片使用合适的格式(WebP)和尺寸,避免大图直接渲染。可以使用
Image组件的alt和objectFit属性。 - 避免主线程阻塞:将耗时的同步计算(如复杂数据转换)放入
TaskPool(任务池)中异步执行。 - 合理使用
aboutToAppear:不要在这里执行大量同步操作或网络请求。数据加载可以稍晚一点,优先保证页面框架渲染出来。
4.2 数据隐私与安全
- 敏感信息存储:用户的登录令牌、个人信息等,必须使用安全的持久化方案,如
@ohos.security.huks(硬件密钥库)或经过加密后存入Preferences。切勿明文存储在AppStorage或文件中。 - 网络传输安全:确保所有 API 请求都使用 HTTPS。对敏感请求参数可以考虑进行非对称加密。
- 权限最小化:在
module.json5中只申请应用必需的最小权限,并在使用前动态检查 (abilityAccessCtrl) 和向用户解释用途。
5. 生产避坑指南
- 资源释放:像
HttpClient、文件句柄、订阅的事件监听器,必须在组件销毁(aboutToDisappear)或使用完毕后及时释放/取消订阅,否则会导致内存泄漏。 - 异步竞态处理:快速连续点击按钮触发多次网络请求时,可能导致数据显示错乱。解决方法:在 ViewModel 中设置
isLoading锁,或使用AbortController取消之前的请求。 - 设备兼容性:鸿蒙设备屏幕尺寸和形态多样。UI 布局应多使用弹性布局(
Flex、百分比、layoutWeight),避免固定尺寸。使用媒体查询(@ohos.mediaquery) 来适配不同设备。 - 列表渲染优化:
ForEach渲染长列表时,务必提供稳定的、唯一的key生成函数,帮助 ArkUI 高效复用组件节点。对于超长列表,考虑实现分页或虚拟滚动。 - 状态管理陷阱:
@State用于组件内部状态,@Prop用于父子组件单向同步,@Link用于双向同步,@Provide和@Consume用于跨组件层级传递。理解其区别,避免滥用AppStorage造成全局状态污染。 - 调试与日志:开发阶段善用
console和logger,但上线前务必移除或关闭无关的console.log。可以封装一个日志工具,根据编译环境开关日志级别。
总结与动手建议
通过以上步骤,我们构建的不仅仅是一个功能性的“校园服务应用”,更是一个具备良好架构、易于测试和扩展的工程化项目。这种分层设计使得:
- 数据层变更不影响UI。
- 业务逻辑可以独立进行单元测试。
- UI组件可以高度复用。
你的毕业设计完全可以参照这个模式进行重构。试着分析你当前的项目:
- 能否将混杂在页面里的网络请求抽离成独立的
Service? - 能否将页面状态和逻辑提取到
ViewModel中? - 是否有可以复用的UI组件能被抽象出来?
- 有哪些性能瓶颈可以优化(如图片、列表)?
最后,思考一下扩展性:如果未来要增加“成绩查询”、“校园卡充值”甚至“分布式设备协同”功能,按照现在的架构,你需要修改多少地方?一个优秀的毕业设计,应该能让答辩老师清晰地看到你对软件设计原则的理解和应用,而 ArkTS 为你提供了实现这些原则的优秀工具。动手重构吧,你会发现代码质量提升后,开发和维护的心情都会变得愉悦。