OpenHarmony API23 四层模块化脚手架完整实现(含全套工具源码)
·
前言
现在网上大部分鸿蒙教程都是单页面 Demo,代码全部堆在 entry 里,没有分层、没有封装,稍微做大一点就出现循环依赖、内存泄漏、UI 样式混乱、主包体积过大等问题。 本文基于 API23 搭建一套企业级四层架构脚手架,整合 HAR 静态复用 + HSP 动态分包,封装网络、数据库、路由、弹窗、权限、文件、日志、全局状态、双主题全套底层工具,附带通用列表、页面状态组件完整源码,所有代码可直接复制运行,适合课程设计、毕业设计、商用项目初始化。
一、整体四层架构设计
依赖流向单向不可逆:页面 / HSP → 业务 HAR → har_base 底层库
- entry:应用入口、常驻页面、全局初始化、HSP 加载调度
- har_base:无业务底层工具、全局常量、通用 UI 组件(全项目共享)
- har_biz:业务静态 HAR,存放业务实体、数据库操作、业务卡片
- hsp_large:重型低频页面动态分包,按需加载,用完卸载优化内存与包体积
架构核心优势
- 解耦分层,新增业务只新增模块,不改动原有代码
- HSP 动态分包大幅缩小主 HAP 安装包,提升冷启动速度
- 全套底层工具一次封装全项目复用,无需重复写原生 API 模板
- 统一深浅双主题,无硬编码色值、尺寸,全局一键换肤
- 完善生命周期资源回收机制,杜绝弹窗残留、请求残留、内存泄漏
二、底层公共工具完整源码(har_base)
2.1 全局主题工具 theme.ets
ets
import 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.Get<boolean>("darkMode") ?? false
return isDark ? DarkTheme : LightTheme
}
export default {
getColor: getThemeColor,
getSize: SizeConstant
}
2.2 全局持久化状态管理 store.ets
ets
import 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", "持久化状态绑定完成")
}
set<T>(key: string, value: T) {
AppStorage.SetOrCreate(key, value)
}
get<T>(key: string): T {
return AppStorage.Get<T>(key)
}
// 登录批量赋值
login(token: string) {
this.set("isLogin", true)
this.set("token", token)
}
// 退出登录清空登录状态
logout() {
this.set("isLogin", false)
this.set("token", "")
}
// 切换深色模式
toggleDark() {
const val = this.get<boolean>("darkMode")
this.set("darkMode", !val)
}
}
export default new GlobalStore()
2.3 分级脱敏日志工具 log_util.ets
ets
import 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.ets
ets
import 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): Promise<boolean> {
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): Promise<boolean> {
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): Promise<string> {
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): Promise<boolean> {
try {
const exist = await fs.access(path)
if (!exist) return true
await fs.unlink(path)
return true
} catch {
return false
}
}
// 递归删除文件夹
async deleteDir(path: string): Promise<boolean> {
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): Promise<number> {
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 页面状态兜底组件
ets
import 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 下拉刷新 + 上拉分页列表
ets
import ThemeUtil from '../utils/theme'
export interface PageInfo {
pageIndex: number
pageSize: number
isNoMore: boolean
}
@Component
export struct RefreshListView<T> {
@Param list: T[] = []
@Param page: PageInfo
@Param onRefresh: () => Promise<void>
@Param onLoadMore: () => Promise<void>
@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%")
}
}
四、页面调用示例(设置页:深色模式 + 缓存清理)
ets
import 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.get<boolean>("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 实战干货!
更多推荐


所有评论(0)