前言现在网上大部分鸿蒙教程都是单页面 Demo代码全部堆在 entry 里没有分层、没有封装稍微做大一点就出现循环依赖、内存泄漏、UI 样式混乱、主包体积过大等问题。 本文基于 API23 搭建一套企业级四层架构脚手架整合 HAR 静态复用 HSP 动态分包封装网络、数据库、路由、弹窗、权限、文件、日志、全局状态、双主题全套底层工具附带通用列表、页面状态组件完整源码所有代码可直接复制运行适合课程设计、毕业设计、商用项目初始化。一、整体四层架构设计依赖流向单向不可逆页面 / HSP → 业务 HAR → har_base 底层库entry应用入口、常驻页面、全局初始化、HSP 加载调度har_base无业务底层工具、全局常量、通用 UI 组件全项目共享har_biz业务静态 HAR存放业务实体、数据库操作、业务卡片hsp_large重型低频页面动态分包按需加载用完卸载优化内存与包体积架构核心优势解耦分层新增业务只新增模块不改动原有代码HSP 动态分包大幅缩小主 HAP 安装包提升冷启动速度全套底层工具一次封装全项目复用无需重复写原生 API 模板统一深浅双主题无硬编码色值、尺寸全局一键换肤完善生命周期资源回收机制杜绝弹窗残留、请求残留、内存泄漏二、底层公共工具完整源码har_base2.1 全局主题工具 theme.etsetsimport AppStorage from ohos.arkui.state.AppStorage // 全局尺寸常量全项目统一规范 export const SizeConstant { gapXs: 4, gapSm: 8, gapMd: 12, gapLg: 16, radiusSm: 8, radiusMd: 12, radiusFull: 999, fontTip: 12, fontAux: 14, fontMain: 16, fontTitle: 22, btnNormal: 44 } // 浅色主题配色 const LightTheme { primary: #007DFF, danger: #F53F3F, success: #00B42A, pageBg: #F5F5F5, cardBg: #FFFFFF, textPrimary: #1D2129, textSecondary: #86909C, divider: #E5E6EB } // 深色主题配色 const DarkTheme { primary: #36A3FF, danger: #FF5C5C, success: #37D05B, pageBg: #121212, cardBg: #1E1E1E, textPrimary: #F2F3F5, textSecondary: #86909C, divider: #333333 } export function getThemeColor() { const isDark AppStorage.Getboolean(darkMode) ?? false return isDark ? DarkTheme : LightTheme } export default { getColor: getThemeColor, getSize: SizeConstant }2.2 全局持久化状态管理 store.etsetsimport AppStorage from ohos.arkui.state.AppStorage import PersistentStorage from ohos.arkui.state.PersistentStorage import common from ohos.app.ability.common import LogUtil from ./log_util class GlobalStore { private ctx: common.UIAbilityContext | null null setContext(context: common.UIAbilityContext) { this.ctx context } // 应用启动一次性绑定持久化字段 initPersistent() { PersistentStorage.link(darkMode, false) PersistentStorage.link(isLogin, false) PersistentStorage.link(token, ) LogUtil.info(GlobalStore, 持久化状态绑定完成) } setT(key: string, value: T) { AppStorage.SetOrCreate(key, value) } getT(key: string): T { return AppStorage.GetT(key) } // 登录批量赋值 login(token: string) { this.set(isLogin, true) this.set(token, token) } // 退出登录清空登录状态 logout() { this.set(isLogin, false) this.set(token, ) } // 切换深色模式 toggleDark() { const val this.getboolean(darkMode) this.set(darkMode, !val) } } export default new GlobalStore()2.3 分级脱敏日志工具 log_util.etsetsimport FileUtil from ./file_util import common from ohos.app.ability.common export enum LogLevel { DEBUG 0, INFO 1, WARN 2, ERROR 3 } class LogUtil { private ctx: common.UIAbilityContext | null null private level: LogLevel LogLevel.DEBUG private logPath: string private readonly MAX_FILE_SIZE 2 * 1024 * 1024 setContext(ctx: common.UIAbilityContext) { this.ctx ctx this.logPath ${FileUtil.getFilesRoot()}/app.log } setLogLevel(lv: LogLevel) { this.level lv } // 敏感信息脱敏 private desensitize(msg: string): string { msg msg.replace(/token[:]\s*\w/gi, token******) msg msg.replace(/1[3-9]\d{9}/g, 1*********) return msg } private getTime(): string { return new Date().toLocaleString() } private async checkFileSize() { const size await FileUtil.getDirSize(this.logPath) if (size this.MAX_FILE_SIZE) { await FileUtil.deleteFile(this.logPath) } } private async writeFile(tag: string, levelStr: string, content: string) { if (!this.ctx) return await this.checkFileSize() const oldText await FileUtil.readText(this.logPath) const line ${this.getTime()} ${levelStr} [${tag}] ${content}\n await FileUtil.writeText(this.logPath, oldText line) } private print(tag: string, lv: LogLevel, ...args: any[]) { if (lv this.level) return let content args.map(item typeof item object ? JSON.stringify(item) : String(item)).join( ) content this.desensitize(content) let levelStr switch (lv) { case LogLevel.DEBUG: levelStr [DEBUG] console.debug(levelStr, tag, content) break case LogLevel.INFO: levelStr [INFO] console.info(levelStr, tag, content) break case LogLevel.WARN: levelStr [WARN] console.warn(levelStr, tag, content) break case LogLevel.ERROR: levelStr [ERROR] console.error(levelStr, tag, content) break } this.writeFile(tag, levelStr, content) } debug(tag: string, ...args: any[]) { this.print(tag, LogLevel.DEBUG, ...args) } info(tag: string, ...args: any[]) { this.print(tag, LogLevel.INFO, ...args) } warn(tag: string, ...args: any[]) { this.print(tag, LogLevel.WARN, ...args) } error(tag: string, ...args: any[]) { this.print(tag, LogLevel.ERROR, ...args) } } export default new LogUtil()2.4 沙盒文件工具 file_util.etsetsimport fs from ohos.file.fs import fileio from ohos.fileio import common from ohos.app.ability.common class FileUtil { private ctx: common.UIAbilityContext | null null setContext(ctx: common.UIAbilityContext) { this.ctx ctx } getFilesDir(): string { return this.ctx?.filesDir ?? } getCacheDir(): string { return this.ctx?.cacheDir ?? } // 递归创建文件夹 async mkdir(path: string): Promiseboolean { try { const exist await fs.access(path) if (!exist) await fs.mkdir(path, true) return true } catch (e) { return false } } // 写入文本 async writeText(path: string, text: string): Promiseboolean { try { const parent path.substring(0, path.lastIndexOf(/)) await this.mkdir(parent) const fd await fileio.open(path, fileio.OpenMode.CREATE | fileio.OpenMode.WRITE_ONLY) await fileio.write(fd, text) await fileio.close(fd) return true } catch { return false } } // 读取文本 async readText(path: string): Promisestring { try { const exist await fs.access(path) if (!exist) return const fd await fileio.open(path, fileio.OpenMode.READ_ONLY) const stat await fileio.fstat(fd) const buf new ArrayBuffer(stat.size) await fileio.read(fd, buf) await fileio.close(fd) return String.fromCharCode(...new Uint8Array(buf)) } catch { return } } // 删除文件 async deleteFile(path: string): Promiseboolean { try { const exist await fs.access(path) if (!exist) return true await fs.unlink(path) return true } catch { return false } } // 递归删除文件夹 async deleteDir(path: string): Promiseboolean { try { const exist await fs.access(path) if (!exist) return true await fs.rmdir(path, { recursive: true }) return true } catch { return false } } // 计算文件夹总大小 async getDirSize(path: string): Promisenumber { let total 0 try { const exist await fs.access(path) if (!exist) return 0 const dir await fs.opendir(path) let entry while ((entry await dir.read()) ! null) { const full ${path}/${entry.name} if (entry.isFile()) { const stat await fs.stat(full) total stat.size } else if (entry.isDirectory()) { total await this.getDirSize(full) } } await dir.close() } catch {} return total } // 格式化字节单位 formatSize(byte: number): string { if (byte 1024) return ${byte} B if (byte 1024 * 1024) return ${(byte / 1024).toFixed(2)} KB return ${(byte / (1024 * 1024)).toFixed(2)} MB } // 清空缓存目录 async clearCache() { const cache this.getCacheDir() return await this.deleteDir(cache) } } export default new FileUtil()三、全局通用 UI 组件源码3.1 StateView 页面状态兜底组件etsimport ThemeUtil from ../utils/theme import animateTo from ohos.arkui.animateTo export enum PageState { LOADING, EMPTY, ERROR, CONTENT } Component export struct StateView { Param state: PageState PageState.LOADING Param onRetry: () void () {} BuilderParam content: () void State rotate: number 0 aboutToAppear() { animateTo({ duration: 1200, iterations: Infinity, curve: Curve.Linear }, () { this.rotate 360 }) } aboutToDisappear() { this.rotate 0 } Builder Loading() { const color ThemeUtil.getColor() const size ThemeUtil.getSize() Column({ space: size.gapMd }) { Text(⟳) .fontSize(42) .rotate({ angle: this.rotate }) Text(数据加载中...) .fontSize(size.fontAux) .fontColor(color.textSecondary) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } Builder Empty() { const color ThemeUtil.getColor() const size ThemeUtil.getSize() Column({ space: size.gapMd }) { Image($r(sys.media.ohos_ic_public_empty)) .width(80) .fillColor(color.textSecondary) Text(暂无数据) .fontSize(size.fontAux) .fontColor(color.textSecondary) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } Builder Error() { const color ThemeUtil.getColor() const size ThemeUtil.getSize() Column({ space: size.gapLg }) { Image($r(sys.media.ohos_ic_public_fail)) .width(80) .fillColor(color.textSecondary) Text(加载失败请点击重试) .fontSize(size.fontAux) .fontColor(color.textSecondary) Button(重试) .width(140) .height(size.btnNormal) .backgroundColor(color.primary) .borderRadius(size.radiusSm) .onClick(this.onRetry) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } build() { Column().layoutWeight(1) { if (this.state PageState.LOADING) { this.Loading() } else if (this.state PageState.EMPTY) { this.Empty() } else if (this.state PageState.ERROR) { this.Error() } else { this.content() } } } }3.2 RefreshListView 下拉刷新 上拉分页列表etsimport ThemeUtil from ../utils/theme export interface PageInfo { pageIndex: number pageSize: number isNoMore: boolean } Component export struct RefreshListViewT { Param list: T[] [] Param page: PageInfo Param onRefresh: () Promisevoid Param onLoadMore: () Promisevoid BuilderParam itemBuilder: (item: T, index: number) void State pullOffset: number 0 State isRefreshing: boolean false State isLoadMore: boolean false private readonly TRIGGER_HEIGHT 60 private readonly MAX_PULL 80 Builder PullHeader() { if (this.pullOffset 0 || this.isRefreshing) { Row() .width(100%) .height(this.pullOffset) .justifyContent(FlexAlign.Center) { Text(⟳) Text(this.isRefreshing ? 刷新中... : 下拉刷新) } } } Builder Footer() { const color ThemeUtil.getColor() const size ThemeUtil.getSize() Row() .width(100%) .height(50) .justifyContent(FlexAlign.Center) { if (this.isLoadMore) { Text(加载更多...).fontColor(color.textSecondary).fontSize(size.fontAux) } else if (this.page.isNoMore) { Text(———— 没有更多数据 ————).fontColor(color.divider).fontSize(size.fontAux) } else { Text(上拉加载更多).fontColor(color.textSecondary).fontSize(size.fontAux) } } } // 拖拽移动 handleDrag(deltaY: number) { if (this.isRefreshing) return let val this.pullOffset deltaY if (val 0) val 0 if (val this.MAX_PULL) val this.MAX_PULL this.pullOffset val } // 松手执行刷新 async handleRelease() { if (this.isRefreshing) return if (this.pullOffset this.TRIGGER_HEIGHT) { animateTo({ duration: 200 }, () this.pullOffset 0) return } this.isRefreshing true try { await this.onRefresh() } finally { animateTo({ duration: 200 }, () this.pullOffset 0, () { this.isRefreshing false }) } } // 滚动触底加载 handleScroll(info: ListScrollInfo) { if (this.isRefreshing || this.isLoadMore || this.page.isNoMore) return const threshold 150 if (info.scrollOffset info.scrollHeight info.totalHeight - threshold) { this.isLoadMore true this.onLoadMore().finally(() this.isLoadMore false) } } build() { Column() { this.PullHeader() List({ space: ThemeUtil.getSize().gapMd }) { ForEach(this.list, (item: T, idx: number) { ListItem() { this.itemBuilder(item, idx) } }) ListFooter() { this.Footer() } } .layoutWeight(1) .edgeEffect(EdgeEffect.None) .onScroll((info) this.handleScroll(info)) .gesture( PanGesture({ direction: PanDirection.Vertical, distance: 5 }) .onChange(e { if (e.offsetY 0) this.handleDrag(e.deltaY) }) .onActionEnd(() this.handleRelease()) ) } .width(100%) } }四、页面调用示例设置页深色模式 缓存清理etsimport GlobalStore from ohos:har_base/utils/store import FileUtil from ohos:har_base/utils/file_util import ThemeUtil from ohos:har_base/utils/theme import LogUtil from ohos:har_base/utils/log_util Entry Component struct SettingPage { State darkMode: boolean false State cacheText: string 计算中... aboutToAppear() { this.darkMode GlobalStore.getboolean(darkMode) this.calcCacheSize() } async calcCacheSize() { const byte await FileUtil.getDirSize(FileUtil.getCacheDir()) this.cacheText FileUtil.formatSize(byte) } async clearCache() { await FileUtil.clearCache() LogUtil.info(SettingPage, 缓存清理完成) await this.calcCacheSize() } build() { const color ThemeUtil.getColor() const size ThemeUtil.getSize() Column({ space: size.gapLg }) .width(100%) .height(100%) .padding(size.gapMd) .backgroundColor(color.pageBg) { Text(系统设置) .fontSize(size.fontTitle) .fontColor(color.textPrimary) .margin({ top: 40 }) Row() .width(100%) .padding(size.gapLg) .borderRadius(size.radiusMd) .backgroundColor(color.cardBg) { Text(深色模式) .fontSize(size.fontMain) .fontColor(color.textPrimary) .layoutWeight(1) Toggle({ isOn: this.darkMode }) .onChange((v) { GlobalStore.toggleDark() this.darkMode v }) } Row() .width(100%) .padding(size.gapLg) .borderRadius(size.radiusMd) .backgroundColor(color.cardBg) { Text(缓存占用${this.cacheText}) .fontSize(size.fontMain) .fontColor(color.textPrimary) .layoutWeight(1) Button(清理缓存) .height(size.btnNormal) .backgroundColor(color.danger) .borderRadius(size.radiusSm) .onClick(() this.clearCache()) } } } }五、项目踩坑总结CSDN 干货总结循环依赖报错业务 HAR 之间禁止互相导入只能依赖 har_base跨业务逻辑放 entry 中转深色模式切换页面不变色不要把 getThemeColor () 存入 State 缓存build 内实时调用HSP 内存持续上涨页面 aboutToDisappear 必须调用分包卸载接口释放图片 PixelMap 资源弹窗遮罩残留卡死页面页面销毁统一调用 DialogUtil 批量关闭全部弹窗下拉列表重复请求RefreshListView 内置 isLoadMore 锁禁止自己额外加滚动监听打包图片错乱各模块资源添加专属前缀 common_/note_/user_避免同名资源覆盖六、文末总结这套四层模块化脚手架覆盖鸿蒙应用开发全流程底层工具、通用 UI 组件开箱即用兼顾开发效率、代码规范、运行性能适配 API23 纯血 OpenHarmony NEXT。 无论是课程作业、毕业设计还是小型商用项目都可以直接基于该架构二次开发只需替换业务 HAR 与 HSP 页面底层基础能力无需重复开发。 需要完整工程目录、模块依赖配置、EntryAbility 全局初始化代码可以评论留言 觉得文章有用欢迎点赞收藏持续更新 OpenHarmony 实战干货