系列文章《鸿蒙开发从入门到专家》第 48 篇 · 高级
做鸿蒙应用开发到一定规模,你会发现一个问题:每个业务模块都在重复封装系统能力。
网络模块在封装 @kit.NetworkKit 的 http,存储模块在封装 @kit.ArkData 的 preferences,日志模块在封装 @kit.PerformanceAnalysisKit 的 hilog。每个模块的封装方式不同,接口风格不统一,升级系统 API 时要改 N 个地方。
这时候该不该抽象一个内部 framework?答案是看规模。如果你的应用只有 3-5 个模块,各模块自己封装没问题。如果超过 10 个模块,或者有多个应用要复用同一套封装,那就该做。
这篇讲怎么设计一个内部 framework,以及设计过程中最容易踩的坑。
一、什么时候该做 Framework
如果只有 1-2 个信号,可以先在模块内做封装。如果 3 个以上信号同时出现,做 framework。
二、Framework 的分层设计
2.1 三层架构
一个合理的内部 framework 分三层:
┌─────────────────────────────────┐
│ Business Modules │ ← 业务层:各业务模块
│ (login, order, payment, ...) │
├─────────────────────────────────┤
│ Internal Framework │ ← 框架层:统一封装
│ ├── network │
│ ├── storage │
│ ├── log │
│ ├── router │
│ └── permission │
├─────────────────────────────────┤
│ HarmonyOS Kit APIs │ ← 系统层:官方 API
│ (@kit.NetworkKit, etc.) │
└─────────────────────────────────┘
框架层做的是"系统能力的适配和增强",不是"业务逻辑的封装"。框架层不应该知道"登录"或"订单"是什么。
2.2 模块划分原则
模块划分的依据是"系统能力的边界",不是"业务功能"。不要把"用户管理"和"网络请求"放一个模块——前者是业务,后者是基础设施。
三、Network 模块设计
以网络模块为例,讲完整的封装思路。
3.1 设计目标
统一请求/响应格式。
统一错误处理。
统一拦截器机制(添加 token、日志、重试)。
超时和重试可配置。
不绑定具体业务,不包含业务逻辑。
3.2 接口定义
// framework/network/types.ts
export interface RequestConfig {
url: string;
method: HttpMethod;
headers?: Record<string, string>;
data?: string | Object | ArrayBuffer;
timeout?: number;
retry?: number;
}
export interface Response<T = any> {
code: number;
data: T;
headers: Record<string, string>;
}
export enum HttpMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE'
}
export interface Interceptor {
onRequest?(config: RequestConfig): Promise<RequestConfig>;
onResponse?(response: Response): Promise<Response>;
onError?(error: NetworkError): Promise<NetworkError>;
}
export class NetworkError extends Error {
constructor(
public code: number,
message: string,
public config?: RequestConfig
) {
super(message);
}
}
3.3 实现
// framework/network/HttpClient.ts
import { http } from '@kit.NetworkKit';
import type { RequestConfig, Response, Interceptor, HttpMethod } from './types';
export class HttpClient {
private interceptors: Interceptor[] = [];
private defaultTimeout: number = 10000;
addInterceptor(interceptor: Interceptor): void {
this.interceptors.push(interceptor);
}
async request<T = any>(config: RequestConfig): Promise<Response<T>> {
let finalConfig = { timeout: this.defaultTimeout, ...config };
// 请求拦截器
for (const interceptor of this.interceptors) {
if (interceptor.onRequest) {
finalConfig = await interceptor.onRequest(finalConfig);
}
}
// 执行请求
const httpResponse = await this.executeRequest(finalConfig);
// 构造响应
let response: Response<T> = {
code: httpResponse.responseCode,
data: this.parseData(httpResponse.result),
headers: httpResponse.header
};
// 响应拦截器
for (const interceptor of this.interceptors) {
if (interceptor.onResponse) {
response = await interceptor.onResponse(response);
}
}
return response;
}
private async executeRequest(config: RequestConfig): Promise<http.HttpResponse> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(config.url, {
method: this.mapMethod(config.method),
header: config.headers,
extraData: config.data,
connectTimeout: config.timeout,
readTimeout: config.timeout,
});
return response;
} finally {
httpRequest.destroy();
}
}
private mapMethod(method: HttpMethod): http.RequestMethod {
const map: Record<HttpMethod, http.RequestMethod> = {
[HttpMethod.GET]: http.RequestMethod.GET,
[HttpMethod.POST]: http.RequestMethod.POST,
[HttpMethod.PUT]: http.RequestMethod.PUT,
[HttpMethod.DELETE]: http.RequestMethod.DELETE,
};
return map[method];
}
private parseData(raw: string | Object | ArrayBuffer): any {
if (typeof raw === 'string') {
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
return raw;
}
}
3.4 预置拦截器
框架层提供常用的拦截器,业务层按需挂载:
// framework/network/interceptors/TokenInterceptor.ts
export class TokenInterceptor implements Interceptor {
constructor(private getToken: () => string) {}
async onRequest(config: RequestConfig): Promise<RequestConfig> {
const token = this.getToken();
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
}
}
// framework/network/interceptors/RetryInterceptor.ts
export class RetryInterceptor implements Interceptor {
constructor(private maxRetries: number = 3) {}
async onError(error: NetworkError): Promise<NetworkError> {
const retryCount = (error.config as any)?._retryCount || 0;
if (retryCount < this.maxRetries && this.isRetryable(error.code)) {
(error.config as any)._retryCount = retryCount + 1;
// 返回特殊标记,由 HttpClient 重新执行
throw new RetrySignal(error.config);
}
return error;
}
private isRetryable(code: number): boolean {
return code >= 500 || code === 0; // 服务端错误或网络超时
}
}
3.5 业务层使用
// 业务模块的初始化代码
const client = new HttpClient();
client.addInterceptor(new TokenInterceptor(() => AppStorage.get('token')));
client.addInterceptor(new LogInterceptor());
// 业务调用
const response = await client.request({
url: '/api/users',
method: HttpMethod.GET
});
业务层只管调 request,不用关心 token 怎么加、错误怎么重试、超时怎么设。这些都在框架层处理好了。
四、Storage 模块设计
存储模块的封装重点是统一接口,屏蔽 preferences 和 RDB 的差异。
// framework/storage/StorageManager.ts
import { preferences } from '@kit.ArkData';
export class StorageManager {
private store: preferences.Preferences;
static async create(context: Context, name: string): Promise<StorageManager> {
const prefs = await preferences.getPreferences(context, name);
return new StorageManager(prefs);
}
private constructor(store: preferences.Preferences) {
this.store = store;
}
async get<T>(key: string, defaultValue: T): Promise<T> {
const value = await this.store.get(key, JSON.stringify(defaultValue));
try {
return JSON.parse(value as string) as T;
} catch {
return defaultValue;
}
}
async put<T>(key: string, value: T): Promise<void> {
await this.store.put(key, JSON.stringify(value));
await this.store.flush();
}
async remove(key: string): Promise<void> {
await this.store.delete(key);
await this.store.flush();
}
}
业务层用的时候不关心底层是 preferences 还是别的存储引擎:
const storage = await StorageManager.create(context, 'user_settings');
await storage.put('theme', { mode: 'dark' });
const theme = await storage.get('theme', { mode: 'light' });
五、设计原则与常见错误
5.1 框架层的边界
框架层封装系统能力,不包含业务逻辑。判断标准:
框架层不应该出现 “user”、“order”、“product” 等业务名词。
框架层不应该知道 API 的具体路径和参数含义。
框架层提供通用能力(缓存、重试、日志),业务层组合使用。
如果框架层里出现了业务逻辑,说明抽象出了问题,需要重新划线。
5.2 避免过度设计
不是所有东西都需要抽象。过度设计的表现:
为只有 2 个调用点的函数封装了一层接口。
为未来"可能"的需求预留了 10 个配置参数,实际只用了 2 个。
抽象层太多,调用栈深到看不清逻辑。
原则:先有三处重复,再抽封装。 两处重复可以先复制粘贴,三处再考虑抽象。过早抽象和错误抽象的成本比重复代码更高。
5.3 版本管理
内部 framework 应该独立为一个 HAR(Harmony Archive)包,有自己的版本号和 changelog。业务模块通过 oh-package.json5 依赖:
{
"dependencies": {
"@internal/framework": "file:../framework"
}
}
升级 framework 时走版本管理流程,业务模块按需升级。不要让 framework 和业务代码混在一个仓库里随意改动。
5.4 文档
framework 的每个模块应该有:
接口定义(
.d.ts文件自带)。使用示例。
升级时的 breaking changes 列表。
没有文档的 framework 没人敢用,最后会变成只有作者自己维护的"私有库"。
小结
下一篇讲 H5 容器化。当你的应用有大量 H5 页面时,需要一个专门的容器框架来管理加载、通信和离线缓存。