前言
在之前的文章中,我们已经讲解了关于鸿蒙的ArkTS与ArkUI的基础知识点。在本篇中,我将带你手把手编写企业级WanAndroid鸿蒙版本的APP,你可以根据本文的节奏一步一步创建属于你的APP!
本文所需要掌握的知识点:
话不多说!直接开始!
1、项目结构搭建
1.1 项目结构分析
在 HarmonyOS 开发中,项目结构不仅仅是文件的堆砌,它直接决定了应用的可扩展性(Scalability)和模块间的解耦程度。尤其是面对企业级项目时,合理的目录划分能让我们在面对复杂的业务逻辑时,依然保持清晰的思路。
为了让大家有一个直观的感受,我绘制了一张典型的 HarmonyOS 企业级项目架构草图,它采用了主流的模块化(Modularization设计思想:

如图所示
好的架构应当如乐高积木般清晰。本项目采用了三层解耦设计:
-
顶层:
product_phone(Entry 模块)- 角色: 它是整个 APP 的"组装员"。
- 依赖: 它指向(依赖)所有的
feature模块。
-
中间层:
feature_xxxx(Feature 模块)- 角色: 业务功能实现(首页、广场等)。
- 依赖: 它们彼此独立(不互相指向),但都向下指向(依赖)
base_common。
-
底层:
base_common(Common 模块)- 角色: 基础设施(网络请求、公共 UI、工具类)。
- 依赖: 它是最孤独的,不依赖任何业务模块,被所有人依赖。
为什么这样更"企业级"?
- 单向依赖: 确保依赖链是单向的(从上到下),防止循环依赖导致项目编译失败。
- 解耦:
feature_home挂了,不会影响feature_user,因为它们之间没有箭头往来。 - 易扩展: 以后你想做一个"平板版"或"折叠屏版",只需要新建一个
product_tablet,然后重新组合现有的feature模块即可。
OK,概念讲完了,开始实操了!
1.2 项目结构实操

如图所示
- 根据上面的架构图分别创建了对应的文件夹:
base、feature、product - 将原有的
Entry重命名phone并且转移到了product下 - 对应的
base、feature文件夹内,分别创建了对应的module
OK,项目虽然结构清晰了,但是对应的依赖链还没组成,那我们该如何按照正确的关系依赖呢?
这个就需要用到oh-package.json5文件 ,而这个文件它又分项目工程级 ,模块化级;(这里简单过一下,下面还会详解)
-
项目工程级oh-package.json5文件 :位于工程根目录下,主要用来描述全局配置,如:依赖覆盖(overrides)、依赖关系重写(overrideDependencyMap)和参数化配置(parameterFile)
-
模块化级oh-package.json5文件 :位于工程各个模块的根目录下,用来描述包名、版本、入口文件(类型声明文件)和依赖项等信息
而我们目前要实现的仅仅是模块化之间的依赖关系,因此目前我们仅需要修改对应module下根目录oh-package.json5文件即可
-
因此在各个feature下module下根目录oh-package.json5文件里添加如下代码
json"dependencies": { "common": "file:../../base/common" } -
而在product下的phone下根目录oh-package.json5文件里添加如下代码
json"dependencies": { "common": "file:../../base/common", "home": "file:../../feature/home", "course": "file:../../feature/course", "user": "file:../../feature/user", "square": "file:../../feature/square" }
🧐 深度思考:为什么 Product 层要"重复"依赖 Common?
看到这里,细心的小伙伴可能会问:既然 Feature 已经依赖了 Common,而 Product 又依赖了 Feature,根据传递性,Product 不是自动拥有了 Common 的能力吗?为什么还要多此一举? 这涉及到鸿蒙模块化开发中的两个核心问题:导出权限与维护成本。
-
导出权限(封装隔离)
在鸿蒙工程中,模块间的依赖并不是"透明"的。即便 home 依赖了 common,如果 home 模块的入口文件 index.ets 中没有显式地通过 export * from '...' 将 common 的能力二次暴露出来,那么 product 模块是无法感知 common 存在的。
-
避免"维护地狱"
假设我们不直接依赖 common,而是通过 home 模块中转导出。这时会面临两个致命问题:
• 引用混乱: 当你在 product_phone 中调用一个底层工具类时,IDE 可能会跳出四个选项:来自 home、来自 course、来自 user... 这会破坏代码的语义化,让人分不清这个工具到底属于谁。
• 结构脆弱: 这种模式会将 product 与某个特定的 feature 深度绑定。一旦未来项目重构,删除了被作为"中转站"的 home 模块,整个 product 层就会因为丢失底层引用而引发大面积报错。
总结: 在企业级开发中,我们提倡谁使用,谁显式依赖。让 product 直接依赖 common,不仅能获得更清晰的 import 路径,更能保证架构的健壮性。
OK!到这里,项目的骨架已经搭建完成,各模块之间的通路也已经打通。
但一个成熟的企业级项目,不可能完全"白手起家",我们需要引入各种优秀的开源库(如网络请求 Axios、图片加载、刷新组件等)来充当我们的"血肉"。
在鸿蒙开发中,如果我们在各个模块里随性地引入三方库,会导致版本冲突 和重复打包等灾难。因此,我们将讲解如何优雅地进行三方库的统一配置与引用。
2、三方库的引用
在之前的架构设计中,我们将 base_common 放在了最底层。为了保证项目整洁,我们遵循一个原则:所有的第三方库,原则上都先在 base_common 中引入并完成封装,再暴露给上层业务使用。
2.1 区分两种 oh-package.json5
而在引入三方库之前,我们必须先搞清楚两个同名oh-package.json5但职责完全不同的文件。你可以把整个项目想象成一家公司 ,而每个模块就是一个部门。
2.1.1. 工程级(根目录) oh-package.json5:公司的"行政部"
位于项目最外层的根目录下。
-
它的作用: 负责整个工程的全局配置。
-
核心职能: * 管理公共依赖(Overrides): 它可以强制统一全工程所有模块使用的三方库版本,防止不同部门"各自为政"导致版本冲突。
- 管理参数化配置: 比如定义全局的编译版本、签名信息等。
-
误区提醒: 你不应该 在这里直接
install具体的业务三方库(如 Axios),它更多是做宏观调控的。
2.1.2. 模块级(Module 根目录) oh-package.json5:部门的"采购清单"
位于每个具体的 feature 或 base 模块目录下。
-
它的作用: 负责当前模块所需的具体依赖。
-
核心职能:
- 具体引用: 比如
base_common需要用到axios,来进行二次封装,你就必须在这个模块下的oh-package.json5中声明。 - 定义模块属性: 定义当前模块的名字(name)、版本(version)。
- 具体引用: 比如
-
实战经验: 即使你在根目录配置了某种规则,具体的"体力活"还是得靠模块级的配置文件来完成。
2.1.3 "角色"对比表
为了方便大家记忆,我整理了这张表:
| 特性 | 工程级 (Project Level) | 模块级 (Module Level) |
|---|---|---|
| 位置 | 项目最根部 | entry / feature / base 目录下 |
| 主要职责 | 全局依赖版本协调、全局策略配置 | 声明当前模块私有依赖、模块信息定义 |
| 依赖声明 | 用于控制版本上限/统一版本 | 用于实际引入三方库 |
| 打个比方 | 集团总部的战略方案 | 业务部门的执行清单 |
2.1.4 最佳实践建议
在我们的 WanAndroid 项目中,引入三方库的标准姿势是:
- 明确需求: 比如我们要用
axios做网络请求。 - 定位模块: 按照我们之前的架构,这个库应该属于底层基础设施,所以我们要去
base/common目录下的oh-package.json5中操作。 - 执行安装: 在该目录下运行
ohpm install @ohos/axios。
💡 注意了!敲黑板了!: > 永远优先考虑 将三方库安装在
base_common这种底层模块中,然后通过export暴露给上层,至于怎么暴露,下面会依次讲解。这样可以避免每个 Feature 模块都去重复配置oh-package.json5,让我们的项目结构更加健壮。不强求所有都这样,具体根据项目实际情况而定!
OK! 理论铺垫完了,看看如何在 base_common 模块中完成三方库的引入和封装。
2.2 工具库与刷新库引入
2.2.1 三方工具库引入
一个成熟的企业级项目,离不开高效、稳定的工具类库。在鸿蒙开发中,harmony-utils 是一套非常优秀的开源库,它几乎涵盖了开发中所有的常见场景:从基础的字符串、日期处理,到进阶的线程间通信、异常捕获、加密解密,甚至系统级的扫码、相册和生物认证等,应有尽有。
引入这一类"基石级"工具,能极大提升我们的开发效率。

如图所示
-
安装工具库
首先,我们需要在
base_common模块下集成它。请确保命令行路径正确:npm# 路径:D:...\WanAndroid\base\common ohpm i @pura/harmony-utils -
确认配置
安装完成后,
base_common模块下的oh-package.json5会自动更新,将看到如下声明:json"dependencies": { "@pura/harmony-utils": "^1.4.0" // 请记得点击 "Sync Now" 同步工程 } -
底层能力"透传" (关键步骤)
由于鸿蒙的模块化机制是物理隔离 的,虽然
base_common引入了该库,但feature_home或product_phone等上层模块默认是无法感知的。为了让上层业务模块能够直接使用这些工具,我们不需要在每个模块里重复安装,而是通过
base_common的入口文件进行统一导出:在
base/common/index.ets中添加:ts// 将 harmony-utils 的能力全量透传给上层模块 export * from '@pura/harmony-utils';
2.2.2 三方刷新库引入
-
安装工具库
首先,我们需要在
base_common模块下集成它。请确保命令行路径正确:npm# 路径:D:...\WanAndroid\base\common ohpm install @abner/refresh_v2 -
确认配置
安装完成后,
base_common模块下的oh-package.json5会自动更新,将看到如下声明:json"dependencies": { "@abner/refresh_v2": "^1.0.4" // 请记得点击 "Sync Now" 同步工程 } -
底层能力"透传" (关键步骤)
在
base/common/index.ets中添加:ts// 将 refresh_v2 的能力全量透传给上层模块 export * from '@abner/refresh_v2'+
2.3 Axios引入与封装
2.3.1 Axios引入

如图所示
-
在我们的
base_common根目录下打开命令行Terminal,一定要注意路径哟,运行ohpm install @ohos/axios -
当运行成功后,我们就会看到
base_common根目录下的oh-package.json5文件里有如下信息:"@ohos/axios": "^2.2.7" -
因为
axios涉及到网络请求,因此我们需要给项目配置网络请求权限- 而权限配置相关内容就在
entry模块也就是本项目product_phone\src\main\module.json5 这个文件里

- 如图所示,在该配置文件里添加如下代码 (注意要Sync Now哟)
json"requestPermissions": [ { "name": "ohos.permission.INTERNET" } ], - 而权限配置相关内容就在
OK!到这axios 算是正确集成到base_common里面了,不过框架集成了,但要怎么使用呢?

如图所示
这是axios官方文档的部分使用方式:
当看到官方的使用说明时,我就会有如下思考:
-
如果不二次封装的话,只要有网络请求都要写一遍,URL分散,网络请求也分散,能否把它们都集中起来,统一管理
-
而官方的使用说明,get请求方式都是非常标准的
QUERY参数,那万一接口是RESTful 风格的,参数是Path类型的,那每次请求岂不是还要手动修改请求URL? -
然后就是,我做Android开发的,习惯了
Retrofit网络框架的使用方式,突然切成前端Axios使用方式确实有点不习惯。而且Retrofit网络框架也支持RESTful 风格的接口。
-
如图所示:虽然OpenHarmony三方库中心仓里面有关于
Retrofit网络框架,但是热度以及更新效率远远不及Axios,既然这样想要,那也想要,那就根据Axios封装一个自己的Retrofit网络框架。
封装Axios之前,首先要了解我们接口响应大致是一个什么样的结构。根据接口响应结构来封装属于我们自己的网络框架库。
因此我们来看看WanAndroid的接口响应大致是什么结构:

如图所示
- 接口响应结构大致为:
errorCode、errorMsg、data - 修改未登录的错误码为-1001,其他错误码为-1,成功为0
- 需要登录访问的接口访问是需要cookie来授权的
OK!接口大致结构我们清楚了,直接开干!
2.3.2 Axios 封装:打造"鸿蒙版 Retrofit"
既然要模仿 Retrofit,我们的核心思路就是通过装饰器 来定义请求配置,通过单例管理类来处理拦截逻辑。这里我们主要通过两个核心文件来实现:
NetworkManager.ets:核心管理类。负责 Axios 实例的初始化、拦截器逻辑(处理响应码、日志打印、错误弹窗)以及底层请求的执行。Decorators.ets:装饰器定义。利用 TS 的元数据反射机制,模仿 Retrofit 的@GET,@POST,@Path,@Query等语法。
1. NetworkManager:网络请求的中枢大脑
在 base_common 的 core 目录下,我们创建 NetworkManager.ets。它的思路定义如下:
-
单例模式:确保全应用只有一个网络配置入口。
-
灵活的钩子函数 :通过
NetworkConfig接口,我们将baseUrl、Token注入逻辑以及登录失效后的跳转逻辑交由主模块(Entry)去实现,保持base_common的纯净。 -
响应拦截处理:
- 业务状态码统一判断 :根据 WanAndroid 接口约定,当
errorCode !== 0时,统一视为异常。 - 自动吐司(Toast) :配合我们之前引入的
harmony-utils,当发生网络错误或业务异常时,直接调用ToastUtil弹出提示,省去了在每个页面手动写弹窗的烦恼。 - RESTful 路径解析 :内置
formatUrl方法,自动将/user/{id}替换为实际的数值。
- 业务状态码统一判断 :根据 WanAndroid 接口约定,当
核心代码实现:
TS
/**
* 初始化方法 (必须在 App 启动时调用)
* @param config 全局配置
*/
public init(config: NetworkConfig) {
this.config = config;
// 创建 Axios 实例
this.axiosInstance = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 初始化拦截器
this.initInterceptors();
}
//*****************此处省略一大堆代码*****************
// --- 响应拦截器 ---
this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
// 1. 提取后端返回的数据
const resData = response.data as BaseResponse<Any>;
// 检查 callback 是否存在
if (this.config.onHeadersReceived && response.headers) {
// 将响应的header 抛出去,让应用层能拿到header内容
this.config.onHeadersReceived(response.headers as Record<string, Any>);
}
// 2. 业务状态码判断 0 代表成功
if (resData.errorCode !== 0) {
// 触发全局错误钩子
if (this.config.errorHandler) {
this.config.errorHandler(resData.errorCode, resData.errorMsg);
}
// 打印错误日志,方便调试
console.error(`[HTTP Error] 业务异常: ${resData.errorMsg}`);
ToastUtil.showShort(resData.errorMsg)
// 抛出业务错误,中断流程
return Promise.reject(new Error(resData.errorMsg || 'Unknown Error'));
}
// 3. 成功,直接返回完整对象
return resData.data as Any as AxiosResponse;
}
//*****************此处省略一大堆代码*****************
/**
* 工具方法:处理 URL 路径参数替换
* 示例:formatUrl("/user/{id}", {id: 100}) -> "/user/100"
*/
private formatUrl(url: string, pathParams?: Record<string, Any>): string {
if (!pathParams || Object.keys(pathParams).length === 0) {
return url;
}
let formattedUrl = url;
Object.keys(pathParams).forEach((key) => {
const value = String(pathParams[key]);
// 替换 {key} 格式
formattedUrl = formattedUrl.replace(new RegExp(`\{${key}\}`, 'g'), value);
// 替换 :key 格式 (兼容某些后端风格)
formattedUrl = formattedUrl.replace(new RegExp(`:${key}`, 'g'), value);
});
return formattedUrl;
}
2. Decorators:优雅的请求声明
这是让代码变"整洁"的关键。在 base_common 下创建 Decorators.ets。我们的封装思路是:
-
元数据仓库(metadataStorage) :我们使用
WeakMap自己实现了一套轻量级的元数据存储。它负责记录:哪个方法用了什么请求方式?哪个参数对应 URL 里的哪个占位符? -
参数装饰器 (
Path,Query,Body) :它们不执行逻辑,只负责"打标签"。记录参数的位置和类型。
核心代码实现:
TS
/**
* 4. 参数装饰器工厂 (替代 @Path, @Query)
* 原理:将参数信息存入上面的 metadataStorage
*/
const createParamDecorator = (type: ParamType) => {
return (key?: string) => {
return (target: Object, propertyKey: string, parameterIndex: number) => {
// A. 获取该方法的元数据列表
const existingParameters = getParamsMetadata(target, propertyKey);
// B. 存入新参数信息
existingParameters.push({
key: key || '',
index: parameterIndex,
type: type,
});
// C. 无需手动 save,因为 getParamsMetadata 返回的是引用,直接 push 就已经存进 Map 了
};
};
};
// 导出具体的参数装饰器 (用法不变)
export const Path = createParamDecorator(ParamType.PATH);
export const Query = createParamDecorator(ParamType.QUERY);
export const Body = createParamDecorator(ParamType.BODY)();
-
方法装饰器 (
GET,POST) :它们是真正的"打工人"。- 它们会拦截原始方法的调用。
- 从元数据仓库中取出之前存好的标签。
- 将你传入的实参一一对应,组装成 Axios 需要的配置。
- 最后交给
NetworkManager去发起真正的网络请求。
核心代码实现:
TS
/**
* 5. 方法装饰器工厂 (替代 @GET, @POST)
* 原理:从 metadataStorage 读取信息并发起请求
*/
const createMethodDecorator = (method: string) => {
return (url: string) => {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
// 保留原始方法(虽然不执行,但保持结构)
const originalMethod: Any = descriptor.value;
// --- 核心逻辑:重写方法体 ---
descriptor.value = async (...args: Any[]) => {
// A. 读取元数据 (替换 Reflect.getOwnMetadata)
const paramsMetadata = getParamsMetadata(target, propertyKey);
// B. 准备请求容器
const pathParams: Record<string, Any> = {};
const queryParams: Record<string, Any> = {};
let bodyData: Any = null;
// C. 遍历元数据,从 args 中取值
paramsMetadata.forEach((meta) => {
const value = args[meta.index]; // 获取运行时传入的实参
// D. 将对应类型的参数,放入对应类型的容器里面
if (meta.type === ParamType.PATH) {
pathParams[meta.key] = value;
} else if (meta.type === ParamType.QUERY) {
queryParams[meta.key] = value;
} else if (meta.type === ParamType.BODY) {
bodyData = value;
}
});
// E. 组装 Options
const options: RequestOptions = {
url,
method,
pathParams,
queryParams,
data: bodyData
};
// F. 发起请求
return await net.request<Any>(options);
};
return descriptor;
};
};
};
// 导出具体的方法装饰器 (用法不变)
export const GET = createMethodDecorator('GET');
export const POST = createMethodDecorator('POST');
export const PUT = createMethodDecorator('PUT');
export const DELETE = createMethodDecorator('DELETE');
2.3.3 封装后的实战效果
封装完这两个文件后,我们在 WanAndroid 项目中发起一个请求将变得极其优雅。
以前的使用方式:
TS
// 需要手动拼接 URL,手动处理异常
axios.get('/article/list/0/json').then(res => { ... })
现在的 Retrofit 风格:
TS
class ApiService {
@GET("/article/list/{page}/json")
async getArticleList(@Path("page") page: number): Promise<ArticleModel> {
return null as any; // 实际逻辑已由装饰器接管
}
}
是不是瞬间找回了 Android 开发的感觉?
注意: 记得在index.ets 将该网络框架使用权给导出来哟
TS
export { NetworkManager } from './src/main/ets/retrofit_net/core/NetworkManager';
// 导出类型
export { BaseResponse, NetworkConfig } from './src/main/ets/retrofit_net/core/NetworkManager';
// 导出装饰器
export { GET, POST, PUT, DELETE, Path, Query, Body } from './src/main/ets/retrofit_net/decorators/Decorators';
export { InternalAxiosRequestConfig } from '@ohos/axios';
既然三方库该引用的引用了,该封装的也封装了,现在就该准备初始化它们并且实际使用了!
2.4 三方库初始化
在上面我们提到过product_phone是整个 APP 的"组装员",因此初始化的工作就要交给它了!
而在初始化之前,我们要先了解EntryAbility
2.4.1 理解 EntryAbility
在 Android 中,我们习惯在 Application 的 onCreate 中一把唆地完成所有初始化。但在 HarmonyOS 中,虽然也有 AbilityStage(对应 Application 级别),但从业务组装 的角度来看,EntryAbility 才是 UI 界面与系统交互的真正起点。
那EntryAbility是什么呢?它具备哪些特性?
-
它是 UI 窗口的载体 :如果说
Application是幕后的大管家,那么EntryAbility就是前台的"大堂经理"。它是系统调度应用的最小单元,负责创建窗口、加载Index页面。 -
多入口特性:鸿蒙允许一个 App 存在多个 Ability。我们可以把它理解为一个应用可以有多个"独立的功能入口"(比如主程序入口、扫码快捷入口、独立的支付窗口等)。
-
初始化的最佳时机 :虽然它不完全等同于 Android 的
Application,但在我们的 WanAndroid 实战中,它是获取 UI 上下文(Context) 、配置全局网络基址(BaseURL)以及启动首屏渲染的核心阵地。
2.4.2 实战初始化配置
既然 EntryAbility 是组装员,我们就在它的 onCreate 生命周期中,将之前封装好的 NetworkManager 进行"激活"。
在 product/phone 模块的 EntryAbility.ets 中编写如下代码:
TS
// 定义一个 Key 用来存 Cookie
const APP_COOKIE_KEY = 'app_cookie_key';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.initSdk()
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}
private initSdk(): void {
this.initUtil()
this.initNetWork()
}
private initUtil(): void {
AppUtil.init(this.context);
//初始化日志 0x1010 设置日志对应的领域标识, 'WanAndroid' 设置日志标识 , true 是否打印日志 , true 日志打印方式 true-hilog、false-console
LogUtil.init(0x1010, 'WanAndroid', true, true);
//异常工具捕获初始化
CrashUtil.onHandled((exceptionInfo) => {
})
}
private initNetWork(): void {
NetworkManager.getInstance().init({
baseUrl: "https://www.wanandroid.com",
//请求拦截器
headerHandler: (config: InternalAxiosRequestConfig) => {
// 从内存/缓存中取出 Cookie 字符串
const cookieStr = AppStorage.get<string>(APP_COOKIE_KEY);
if (cookieStr) {
// 这里的 Key 必须叫 'Cookie'
config.headers['Cookie'] = cookieStr;
}
return config
},
// 【响应拦截】提取并保存 Cookie
onHeadersReceived: (headers) => {
// Axios 返回的 set-cookie 通常是一个数组,或者字符串
// 在玩Android接口中,它是 'set-cookie' 字段
const setCookie = headers['set-cookie'] as string[] | string;
if (setCookie) {
// 处理 Cookie 逻辑 (调用下面的工具方法)
const cookieStr = this.processSetCookie(setCookie);
// 如果解析到了有效的 Cookie,就存起来
if (cookieStr && cookieStr.length > 0) {
// 这里为了演示简单直接覆盖,实际业务可能需要和旧 Cookie 合并
AppStorage.setOrCreate(APP_COOKIE_KEY, cookieStr);
console.info('更新 Cookie 成功:', cookieStr);
}
}
},
//网络请求错误、接口业务报错回调
errorHandler: (code, msg) => {
if (code == -1001) {
//登录失效
// todo 这里将会跳转至登录页面
return
}
//其他code 根据业务场景来处理,吐司已经在内部拦截器实现,当然也可以在这里弹吐司
}
})
}
注意: 虽然我们初始化时,在请求与响应拦截 正确处理了
cookie以及错误code,但是我们直接写死了baseUrl: 'https://www.wanandroid.com'。在实际开发中,我们通常会有 Debug(测试环境) 和 Release(生产环境) 之分
在鸿蒙工程中,管理这类差异化配置的核心文件就是根目录下的 build-profile.json5。
1. 认识 build-profile.json5 的职责
如果说 oh-package.json5 管理的是"包依赖",那么 build-profile.json5 管理的就是构建行为。它定义了:
- 项目包含哪些 Module。
- 编译的 Target 目标(比如针对手机、平板)。
- 自定义构建参数(这正是我们需要的)。
2. 配置自定义参数
我们可以在 buildOptionSet -> arkOptions -> buildProfileFields 来注入变量。为了简单直观,我们可以在 app 层级也就是product_phone的配置中定义:
product_phone根目录 -> build-profile.json5
json
//---------------------此处省略一大堆代码--------------------
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
//---------------------此处省略一大堆代码--------------------
"buildProfileFields": {
"BASE_URL": "https://www.wanandroid.com/"
}
}
},
{
"name": "debug",
"arkOptions": {
"buildProfileFields": {
"BASE_URL": "https://www.wanandroid.com/"
}
}
}
]
//---------------------此处省略一大堆代码--------------------
我们先简单Rebuild一下项目,鸿蒙构建工具会自动生成一个名为 BuildProfile 的类(注意:这个类是编译时自动生成的)。

如图所示
- 因为我们的自定义参数是写在
product_phone里面,因此只有APP级别的可以访问,其他module都不会生成自定义参数 - 当然你也可以配置在工程级
build-profile.json5,这样所有module生成的BuildProfile的类都会有自定义参数。(工程级配置在buildModeSet->buildOption->arkOptions->buildProfileFields里面配置)
💡 小技巧: > 建议
BASE_URL配置在APP级,这样如果你有多个products(比如专门的test产品线),你可以为每个product配置不同的API_BASE_URL。
3. 在代码中动态获取
现在,我们可以回到 EntryAbility.ets 优化我们的初始化代码:
TS
//---------------------此处省略一大堆代码--------------------
private initUtil(): void {
AppUtil.init(this.context);
//初始化日志 0x1010 设置日志对应的领域标识, 'WanAndroid' 设置日志标识 , true 是否打印日志 , true 日志打印方式 true-hilog、false-console
LogUtil.init(0x1010, 'WanAndroid', BuildProfile.DEBUG, true);
//异常工具捕获初始化
CrashUtil.onHandled((exceptionInfo) => {
})
}
private initNetWork(): void {
NetworkManager.getInstance().init({
baseUrl: BuildProfile.BASE_URL,
//请求拦截器
headerHandler: (config: InternalAxiosRequestConfig) => {
// 从内存/缓存中取出 Cookie 字符串
const cookieStr = AppStorage.get<string>(APP_COOKIE_KEY);
if (cookieStr) {
// 这里的 Key 必须叫 'Cookie'
config.headers['Cookie'] = cookieStr;
}
return config
}
//---------------------此处省略一大堆代码--------------------
2.4.3 为什么这样做更"企业级"?
这样处理的好处显而易见:
- 安全性:敏感的地址不会散落在代码各处。
- 自动化 :当我们切换 Build Variant(构建变体)进行打包时,系统会自动选择对应的 URL,无需人工修改代码,彻底杜绝了带着测试环境地址上线的事故。
- 统一性 :除了
BaseUrl,你还可以把三方库的AppKey、日志开关等都丢进这里管理。
2.5 路由导航:打通模块间的"任意门"
网络框架搭好了,但在 HarmonyOS 的模块化开发中,我们还面临一个棘手的问题:如何从 feature_home 顺利跳转到 feature_user?
由于各业务模块之间没有直接依赖关系,使用系统原生的 router 往往需要硬编码 url 路径,这不仅容易写错,更违背了我们解耦的初衷。因此,我们需要引入OpenHarmony的 HMRouter 路由框架。
2.5.1 HMRouter引入
HMRouter路由框架与其他三方框架引入不同,其他框架可以在common 引入后,通过export将功能导出来。但它不行,因为它触及了鸿蒙 编译期插件(Compiler Plugin) 与 运行时加载(Runtime Loading) 的本质区别。
简单来说:普通库(Axios、Utils)是"代码逻辑",而 HMRouter 是"底层基座"。
1. 编译期间扫描的"物理隔离" (HMRouter 特有)
HMRouter 不仅仅是一个运行时跳转的工具,它包含一个 Hvigor 编译插件。
- 普通库 :当你写
import { axios } from 'common'时,编译器只需要在运行到那行代码时去找common模块要人就行了。 - HMRouter :它在编译阶段会扫描所有带
@Router注解的页面,并在当前模块的generated文件夹下生成新的.ets代码文件。 - 致命点 :这些生成的代码是由插件硬编码写入的,它们内部固定写着
import { ... } from '@hadss/hmrouter'。 - 结果 :因为生成的代码直接找
@hadss/hmrouter,而不经过你的common/Index.ets转发,所以当前模块必须拥有这个库的直接依赖权,否则编译器在处理这些自动生成的代码时会直接报"找不到模块"。

如图所示
- 在每个moudule都添加依赖
"@hadss/hmrouter": "^1.2.2"
🧐 思考: 此时可能有小伙伴就会想,既然每个module都添加依赖
hmrouter,那这不就违背了单向依赖架构思想?
我们需要从工程逻辑 和鸿蒙构建机制 这两个更高的维度来重新审视这个问题:这并不是违背架构,而是在适配鸿蒙的构建底层逻辑。
-
逻辑依赖 vs 物理声明
- 逻辑依赖(你的目标) :你希望业务模块只跟
common打交道,这没错。在你的 TS/ArkTS 代码里,你依然只import自common,保持了代码层面的解耦。 - 物理声明(系统的要求) :
oh-package.json5是给 OHPM(包管理器) 和 Hvigor(编译器) 看的。因为HMRouter会在你的业务模块里"注入"代码,如果不声明,编译器就无法在当前模块的沙箱里找到对应的库。
- 逻辑依赖(你的目标) :你希望业务模块只跟
-
为什么这不算"违背架构"?
在成熟的工程架构中,依赖通常分为两类:
- 业务依赖 (Axios, Utils) :这些通过
common转发,严格遵守架构分层。 - 环境/插件依赖 (HMRouter) :这些属于基础设施。就像每个房间(模块)都必须有自己的电插座(声明依赖)才能取电,而不能指望只在走廊(common)装一个插座,全楼的电器就能隔空取电。
- 业务依赖 (Axios, Utils) :这些通过
既然将它引入进来了,接下来就是关于它的集成了
2.5.2 HMRoute集成与使用
关于HMRoute集成与使用参考官方文档,写的很详细了,如果在文章里体现也是直接照搬过来,没啥意义。
💡 注意: 当使用
HMRoute后,@HMRouter({ pageUrl: 'xxxx' })这里面的PageUrl最好是带上module前缀 ,例如:@HMRouter({ pageUrl: 'FeatureUser\xxxxPage' }),可以将他们统一放在base_common常量里。或者使用HMRoute里服务器路由 ,将放在base_common常量里的pageUrl下沉至各个Feature中,通过HMServiceProvider,让它们各自实现提供pageUrl的服务,最后统一跳转。
2.6 版本统一管理:parameterFile 的妙用
随着项目规模的扩大,base_common、feature_home 等多个模块可能会重复引用同一批三方库。如果版本号散落在各个模块的 oh-package.json5 中,后期升级时极易出现"漏改"或"版本冲突"的情况。
为了实现"一处修改,全模块同步",鸿蒙提供了 parameterFile 属性来帮助我们进行版本归一化管理。
2.6.1 定义全局版本配置文件
首先,在项目根目录下(与 build-profile.json5 同级),新建一个名为 oh-package-parameter.json5 的文件。
在这个文件里,我们将所有三方库的版本号提取出来进行统一声明:
oh-package-parameter.json5
json
{ //各个三方库版本信息
"parameter": {
"axios_ver": "^2.2.7",
"harmony-utils_ver": "^1.4.0",
"hmrouter_ver": "^1.2.2",
"refresh_v2": "^1.0.4"
}
}
2.6.2 建立工程关联
定义好版本文件后,我们需要告诉工程去哪里读取这些信息。打开项目级根目录 下的 oh-package.json5 ,添加 "parameterFile" 属性进行关联:
oh-package.json5
json
{
"modelVersion": "6.0.1",
"description": "Please describe the basic information.",
"parameterFile": "./oh-package-parameter.json5",// 关联创建的文件 记得Sync Now哟
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.24",
"@ohos/hamock": "1.0.0"
}
}
注意: 修改完成后,请务必点击编辑器右上角的 Sync Now 进行同步,也要Rebuild工程哟
2.6.3 在模块中优雅引用
当配置生效并完成 Rebuild 后,我们就可以在各个子模块中使用这些定义的版本信息了。
使用方法: 在对应模块的 oh-package.json5 中,将具体的版本号替换为 @param:参数名 的形式。

如图所示
通过@param:就能使用 "parameterFile"配置的版本信息,一定要Rebuild后才可以使用哟
💡 为什么这种方案更"企业级"?
- 极简维护 :当
HMRoute发布新版本需要全量升级时,你只需修改根目录下的一个json5文件,整个工程都会自动同步。 - 避免冲突 :强制全模块使用同一版本,彻底解决了由于版本不一致导致的运行时
Class Not Found或Method Incompatibility等隐性 Bug。 - 代码整洁:子模块的配置文件变得非常干净,不再充斥着杂乱的版本数字。
3、实战闭环:启动页的身份核验流
在项目架构搭建的最后,我们需要将网络封装、路由跳转和业务逻辑串联起来。我们通过 SplashPage 的生命周期钩子来实现一个严谨的启动逻辑:

如图所示
1. 业务流程拆解
-
第一步:状态预判。当启动页加载时,首先检查本地存储的登录状态。
TSprivate async checkAndJump() { await new Promise<void>((resolve) => { setTimeout(resolve, 2000); }); const isLogin = UserStorageUtil.isLogin() LogUtil.warn(`WelcomeLifecycle isLogin=${isLogin}`) if (!isLogin) { //如果没有登录,直接跳转登录页面 this.jumpToLogin() return } //如果登录了,先调用用户信息接口,确保cookie没有过期 this.checkLoginStatus() } -
第二步:动态分发。
- 未登录 :直接通过
HMRouter导航至LoginPage。
TSprivate jumpToLogin() { HMRouterMgr.replace({ pageUrl: FeatureHomePath.LOGIN, param: { "from": "welcome" } }); }- 已登录:不急着进首页,而是发起一次用户信息请求。
TSprivate async checkLoginStatus() { LogUtil.warn(`WelcomeLifecycle checkLoginStatus`) try { // // 这里的请求会自动会走配置好的 axios 拦截器 // // 接口校验通过:说明没失效,跳转到首页 PhoneApi.getUserInfo().then((userInfoResult: UserInfoResult) => { LogUtil.debug("checkLoginStatus->getUserInfo-> " + JSON.stringify(userInfoResult)) HMRouterMgr.replace({ pageUrl: FeatureHomePath.HOME, param: { "from": "Loading" } }) }) } catch (er) { console.error("验证登录失效或网络异常", er); } } - 未登录 :直接通过
-
第三步:响应拦截器的妙用。
- 在登录成功时,我们的响应拦截器已经自动将
Set-Cookie持久化到了首选项中。
TS// 2. 【响应拦截】提取并保存 Cookie onHeadersReceived: (headers) => { // Axios 返回的 set-cookie 通常是一个数组,或者字符串 // 在玩Android接口中,它是 'set-cookie' 字段 const setCookie = headers['set-cookie'] as string[] | string; if (setCookie) { // 处理 Cookie 逻辑 (调用下面的工具方法) const cookieStr = this.processSetCookie(setCookie); // 如果解析到了有效的 Cookie,就存起来 if (cookieStr && cookieStr.length > 0) { // 这里为了演示简单直接覆盖,实际业务可能需要和旧 Cookie 合并 // AppStorage.setOrCreate(APP_COOKIE_KEY, cookieStr); UserStorageUtil.saveCookie(cookieStr) console.info('更新 Cookie 成功:', cookieStr); } } }- 此时发起的请求会自动带上这些 Cookie。如果后端返回数据正常,说明身份有效,丝滑进入
HomePage。
TS//请求拦截器 headerHandler: (config: InternalAxiosRequestConfig) => { // 从内存/缓存中取出 Cookie 字符串 const cookieStr = UserStorageUtil.getCookStr(); if (cookieStr) { // 这里的 Key 必须叫 'Cookie' config.headers['Cookie'] = cookieStr; } return config },- 如果拦截器捕获到
errorCode: -1001(登录失效),则会自动触发清除逻辑并弹窗告知用户,重新导向登录页。
javascript//网络请求错误、接口业务报错回调 errorHandler: (code, msg) => { if (code == -1001) { //登录失效 HMRouterMgr.removeAll(); // 这里将会跳转至登录页面 HMRouterMgr.replace({ pageUrl: FeatureHomePath.LOGIN, param: { "from": "NetWork" } }); return } //其他code 根据业务场景来处理,吐司已经在内部拦截器实现,当然也可以在这里弹吐司 } - 在登录成功时,我们的响应拦截器已经自动将
2. 这种设计的优势
这种"页面绑定生命周期"的逻辑,让启动页成为了一个纯粹的"调度中心"。它不持有复杂的业务逻辑,而是作为网络库与路由库的协调者,确保了用户进入首页时,App 的状态是绝对可靠的。
4、结语:开工大吉,架构先行
恭喜你读到了这里!此时春节的烟火气或许已经散去,我们也重新回到了充满挑战的代码世界。
在这篇文章中,我们没有急于去堆砌那些绚丽的 UI 界面,而是耐心地给我们的 WanAndroid 项目打下了一份坚实的基础:
- 模块化分层:实现了业务模块间的物理隔离与逻辑解耦。
- Retrofit 风格封装:利用 ArkTS 装饰器让 Axios 网络请求变得优雅且易于维护。
- 版本归一化 :通过
parameterFile实现了全工程依赖的统一管理。 - 启动导航流 :构建了从
SplashPage到HomePage的身份核验闭环。
"磨刀不误砍柴工",这套架构虽然在前期投入了一定的思考成本,但它将保证我们在接下来的业务开发中如鱼得水,不再为配置冲突或逻辑耦合而头疼。
下集预告: 架构已成,接下来便是"填肉"。在接下来的实战篇中,我们将正式杀入 UI 开发阵地,带大家手把手实现WanAndroid 核心业务逻辑。
新的一年,新的开始。愿你的代码 无 Bug,无告警,全是 Feature!我们下一篇实战见!
4.1 饮水思源:致谢与开源
一个项目的成功离不开巨人肩上的力量。在编写 WanAndroid 鸿蒙版 的过程中,特别感谢以下开源项目与平台提供的支持:
感谢数据支持
- WanAndroid :感谢 鸿洋 (Hongyang) 大佬提供的稳定 API 支持。正是因为有了如此完善的业务接口,才让我们的鸿蒙实战能够真正落地。
感谢核心开源框架
正是这些优秀的开源工具,构建了我们项目稳固的底层:
- @ohos/axios:项目网络请求的核心基石。
- @hadss/hmrouter:助力我们实现企业级模块化导航与守卫逻辑。
- @abner/refresh_v2:让页面的刷新与加载体验更上一层楼。
- @pura/harmony-utils:提供了极其便利的工具类库,极大提升了开发效率。
欢迎关注我的开源项目
本项目已在 GitHub 开源,包含完整的架构封装与业务实现,欢迎大家 Star 与 Fork,我们一起完善它!
- WanAndroid 鸿蒙版仓库 :github.com/Huangqianku...