作為一名鴻蒙開發(fā)初學(xué)者,最近集中精力啃完了網(wǎng)絡(luò)請(qǐng)求這塊核心知識(shí)點(diǎn)。從一開始對(duì)著官方文檔無從下手,到能獨(dú)立封裝工具類、處理各種異常場(chǎng)景,踩了不少坑,也積累了一些實(shí)用經(jīng)驗(yàn)。今天就以復(fù)盤的形式,把鴻蒙網(wǎng)絡(luò)請(qǐng)求的學(xué)習(xí)重點(diǎn)、實(shí)戰(zhàn)技巧和避坑要點(diǎn)整理出來,適合和我一樣剛?cè)腴T的小伙伴參考,也當(dāng)作自己的學(xué)習(xí)沉淀~
先明確一個(gè)核心:鴻蒙開發(fā)中,網(wǎng)絡(luò)請(qǐng)求的核心依賴是 @ohos.net.http 原生模塊,這是官方提供的功能完整、性能優(yōu)異的網(wǎng)絡(luò)請(qǐng)求工具,幾乎所有應(yīng)用的網(wǎng)絡(luò)交互都離不開它。除此之外,社區(qū)也有適配鴻蒙的三方庫(kù) @ohos/axios,用法和前端 axios 類似,適合熟悉前端開發(fā)的小伙伴快速上手。
整個(gè)學(xué)習(xí)過程,我把它分成了「基礎(chǔ)入門→實(shí)戰(zhàn)封裝→高級(jí)優(yōu)化→避坑總結(jié)」四個(gè)階段,一步步從“會(huì)用”到“好用”,下面逐一拆解。
一、基礎(chǔ)入門:搞定網(wǎng)絡(luò)請(qǐng)求的“敲門磚”
入門階段最核心的目標(biāo),是掌握網(wǎng)絡(luò)請(qǐng)求的基本流程,以及必要的前置配置。這一步看似簡(jiǎn)單,但少了任何一個(gè)環(huán)節(jié),都會(huì)導(dǎo)致請(qǐng)求失敗,新手很容易在這里栽跟頭。
- 必做前置:配置網(wǎng)絡(luò)權(quán)限
鴻蒙應(yīng)用默認(rèn)禁止網(wǎng)絡(luò)訪問,所以第一步必須在 module.json5 文件中聲明網(wǎng)絡(luò)權(quán)限,否則無論代碼寫得多對(duì),都會(huì)直接報(bào)網(wǎng)絡(luò)錯(cuò)誤(錯(cuò)誤信息可能不明確,容易誤導(dǎo)排查)。
配置代碼如下,直接復(fù)制到 requestPermissions 字段中即可:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
- 核心流程:3步完成一次基礎(chǔ)請(qǐng)求
鴻蒙原生網(wǎng)絡(luò)請(qǐng)求的核心流程很固定,無論是 GET 還是 POST,都離不開「導(dǎo)入模塊→創(chuàng)建請(qǐng)求對(duì)象→發(fā)起請(qǐng)求→處理響應(yīng)→釋放資源」這幾個(gè)步驟。這里以最常用的 GET 請(qǐng)求為例,貼出基礎(chǔ)代碼(新手可直接復(fù)制測(cè)試):
// 1. 導(dǎo)入http模塊
import http from '@ohos.net.http';
// 2. 發(fā)起GET請(qǐng)求(回調(diào)函數(shù)寫法)
let httpRequest = http.createHttp(); // 每個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)獨(dú)立對(duì)象,不可復(fù)用
httpRequest.request(
"https://jsonplaceholder.typicode.com/users", // 測(cè)試接口,可直接訪問
{
method: http.RequestMethod.GET, // 請(qǐng)求方法,GET/POST/PUT/DELETE等
connectTimeout: 10000, // 連接超時(shí)時(shí)間(毫秒),建議設(shè)10秒
readTimeout: 10000, // 讀取超時(shí)時(shí)間(毫秒)
header: {
'Content-Type': 'application/json' // 請(qǐng)求頭,根據(jù)接口要求調(diào)整
}
},
(err, data) => {
// 3. 處理響應(yīng)結(jié)果
if (!err) {
// 請(qǐng)求成功,解析響應(yīng)數(shù)據(jù)
console.info('響應(yīng)碼:' + data.responseCode);
if (data.responseCode >= 200 && data.responseCode < 300) {
let result = JSON.parse(data.result as string);
console.info('請(qǐng)求結(jié)果:' + JSON.stringify(result));
}
} else {
// 請(qǐng)求失敗,處理錯(cuò)誤
console.error('請(qǐng)求失敗:' + JSON.stringify(err));
}
// 關(guān)鍵:請(qǐng)求完成后必須銷毀對(duì)象,避免內(nèi)存泄漏
httpRequest.destroy();
}
);
這里有一個(gè)新手必記的點(diǎn):每個(gè)請(qǐng)求都要?jiǎng)?chuàng)建獨(dú)立的 httpRequest 對(duì)象,且請(qǐng)求完成后必須調(diào)用 destroy() 方法釋放資源。如果忘記銷毀,會(huì)導(dǎo)致內(nèi)存泄漏,長(zhǎng)期運(yùn)行會(huì)讓應(yīng)用卡頓甚至崩潰,這是我一開始踩的第一個(gè)大坑。
- 常見請(qǐng)求方法:GET vs POST
實(shí)際開發(fā)中,GET 和 POST 是最常用的兩種請(qǐng)求方法,兩者的核心區(qū)別的在于參數(shù)傳遞方式,這里整理成表格,一目了然:
請(qǐng)求方法
參數(shù)傳遞方式
適用場(chǎng)景
核心注意點(diǎn)
GET
參數(shù)拼接在URL后面
查詢數(shù)據(jù)(如列表、詳情)
參數(shù)長(zhǎng)度有限制,不適合傳遞敏感數(shù)據(jù)
POST
參數(shù)放在請(qǐng)求體(extraData)中
提交數(shù)據(jù)(如登錄、注冊(cè)、新增)
需設(shè)置正確的 Content-Type,參數(shù)無長(zhǎng)度限制
POST 請(qǐng)求的基礎(chǔ)示例(重點(diǎn)看 extraData 和請(qǐng)求頭):
httpRequest.request(
"https://jsonplaceholder.typicode.com/login",
{
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: { username: "admin", password: "123456" }, // 請(qǐng)求體參數(shù)
connectTimeout: 10000,
readTimeout: 10000
},
(err, data) => {
// 處理邏輯和GET一致,記得銷毀資源
if (!err && data.responseCode === 200) {
let result = JSON.parse(data.result as string);
console.info('登錄成功,token:' + result.token);
} else {
console.error('登錄失?。? + err?.message);
}
httpRequest.destroy();
}
);
二、實(shí)戰(zhàn)進(jìn)階:封裝工具類,提升開發(fā)效率
入門之后會(huì)發(fā)現(xiàn),直接在業(yè)務(wù)代碼中寫原生請(qǐng)求,會(huì)出現(xiàn)大量重復(fù)代碼(比如導(dǎo)入模塊、創(chuàng)建對(duì)象、釋放資源、錯(cuò)誤處理),不僅冗余,還不利于后期維護(hù)。這時(shí)候,封裝一個(gè)統(tǒng)一的網(wǎng)絡(luò)工具類,就成了剛需。
封裝的核心思路是:用單例模式創(chuàng)建工具類,統(tǒng)一處理請(qǐng)求頭、超時(shí)時(shí)間、參數(shù)拼接、錯(cuò)誤處理和資源釋放,對(duì)外提供簡(jiǎn)潔的 get、post 方法,讓業(yè)務(wù)代碼只關(guān)注“請(qǐng)求什么”,而不用關(guān)注“怎么請(qǐng)求”。
下面是我封裝的工具類完整代碼(基于 ArkTS,可直接復(fù)制到項(xiàng)目中使用):
// network.ets (建議放在 src/main/ets/common 目錄下)
import http from '@ohos.net.http';
import hilog from '@ohos.hilog';
// 定義響應(yīng)數(shù)據(jù)格式,規(guī)范返回結(jié)果
export interface HttpResponse<T = any> {
code: number; // 響應(yīng)碼
data: T; // 響應(yīng)數(shù)據(jù)
message: string; // 提示信息
}
// 單例模式封裝網(wǎng)絡(luò)工具類
export class HttpService {
private static instance: HttpService;
private baseUrl: string = ''; // 基礎(chǔ)路徑,統(tǒng)一配置
private readonly DEFAULT_TIMEOUT = 10000; // 默認(rèn)超時(shí)時(shí)間10秒
// 私有構(gòu)造函數(shù),禁止外部實(shí)例化
private constructor() {}
// 單例獲取方法
static getInstance(): HttpService {
if (!HttpService.instance) {
HttpService.instance = new HttpService();
}
return HttpService.instance;
}
// 設(shè)置基礎(chǔ)路徑(如:https://api.example.com)
setBaseUrl(url: string): void {
this.baseUrl = url;
}
// GET請(qǐng)求封裝
async get<T>(path: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
const url = this.buildUrl(path, params);
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
connectTimeout: this.DEFAULT_TIMEOUT,
readTimeout: this.DEFAULT_TIMEOUT,
header: this.getDefaultHeaders()
});
// 釋放資源
httpRequest.destroy();
// 處理響應(yīng)結(jié)果
if (response.responseCode >= 200 && response.responseCode < 300) {
return {
code: response.responseCode,
data: JSON.parse(response.result as string),
message: '請(qǐng)求成功'
};
} else {
throw new Error(`HTTP請(qǐng)求失敗,響應(yīng)碼:${response.responseCode}`);
}
} catch (err) {
// 錯(cuò)誤處理,打印日志
hilog.error(0xFF00, 'Network', 'GET請(qǐng)求失敗:%{public}s', err.message);
httpRequest.destroy(); // 異常時(shí)也要釋放資源
throw err;
}
}
// POST請(qǐng)求封裝
async post<T>(path: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
const url = this.baseUrl + path;
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url, {
method: http.RequestMethod.POST,
connectTimeout: this.DEFAULT_TIMEOUT,
readTimeout: this.DEFAULT_TIMEOUT,
header: this.getDefaultHeaders(),
extraData: data // POST請(qǐng)求體
});
httpRequest.destroy();
if (response.responseCode >= 200 && response.responseCode < 300) {
return {
code: response.responseCode,
data: JSON.parse(response.result as string),
message: '請(qǐng)求成功'
};
} else {
throw new Error(`HTTP請(qǐng)求失敗,響應(yīng)碼:${response.responseCode}`);
}
} catch (err) {
hilog.error(0xFF00, 'Network', 'POST請(qǐng)求失敗:%{public}s', err.message);
httpRequest.destroy();
throw err;
}
}
// 拼接URL和參數(shù)(處理GET請(qǐng)求的參數(shù))
private buildUrl(path: string, params?: Record<string, any>): string {
let url = this.baseUrl + path;
if (!params) return url;
// 拼接參數(shù)(編碼處理,避免特殊字符報(bào)錯(cuò))
const query = Object.keys(params)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
return url + (url.includes('?') ? '&' : '?') + query;
}
// 默認(rèn)請(qǐng)求頭(可根據(jù)項(xiàng)目需求擴(kuò)展,如添加token)
private getDefaultHeaders(): Record<string, string> {
return {
'Content-Type': 'application/json',
'Accept': 'application/json',
// 示例:從AppStorage獲取token(需結(jié)合項(xiàng)目狀態(tài)管理)
'Authorization': Bearer ${AppStorage.Get('token') || ''}
};
}
}
工具類的使用示例(簡(jiǎn)潔到離譜,業(yè)務(wù)代碼再也不用寫冗余邏輯):
// 1. 初始化工具類(建議在應(yīng)用入口處初始化一次)
const httpService = HttpService.getInstance();
httpService.setBaseUrl('https://jsonplaceholder.typicode.com');
// 2. 發(fā)起GET請(qǐng)求(async/await寫法,更簡(jiǎn)潔)
async getUserList() {
try {
const resp = await httpService.get('/users');
this.userList = resp.data; // 直接使用響應(yīng)數(shù)據(jù)
} catch (err) {
// 統(tǒng)一錯(cuò)誤提示(如彈窗提示用戶)
showToast('獲取用戶列表失敗,請(qǐng)重試');
}
}
// 3. 發(fā)起POST請(qǐng)求
async login() {
try {
const resp = await httpService.post('/login', {
username: 'admin',
password: '123456'
});
// 存儲(chǔ)token
AppStorage.Set('token', resp.data.token);
showToast('登錄成功');
} catch (err) {
showToast('登錄失敗,賬號(hào)或密碼錯(cuò)誤');
}
}
封裝之后,不僅代碼量大大減少,而且后期如果需要修改超時(shí)時(shí)間、請(qǐng)求頭,只需要修改工具類,不用逐個(gè)修改業(yè)務(wù)代碼,維護(hù)成本直接降低一半。
三、高級(jí)優(yōu)化:緩存策略與異常處理
學(xué)會(huì)封裝工具類后,基本能滿足大部分日常開發(fā)需求,但要想讓應(yīng)用體驗(yàn)更好,還需要做一些高級(jí)優(yōu)化——比如添加緩存策略、完善異常處理,解決弱網(wǎng)、無網(wǎng)場(chǎng)景下的用戶體驗(yàn)問題。
- 緩存策略:減少重復(fù)請(qǐng)求,提升響應(yīng)速度
對(duì)于一些不常變化的數(shù)據(jù)(如首頁(yè)公告、分類列表),每次打開頁(yè)面都發(fā)起網(wǎng)絡(luò)請(qǐng)求,不僅浪費(fèi)流量,還會(huì)影響響應(yīng)速度。這時(shí)候可以采用「LRU內(nèi)存緩存 + Preferences持久化緩存」的雙層緩存策略:
LRU內(nèi)存緩存:緩存最近訪問的數(shù)據(jù),內(nèi)存不足時(shí)自動(dòng)淘汰最久未訪問的數(shù)據(jù),適合短期緩存。
Preferences持久化緩存:將數(shù)據(jù)存儲(chǔ)到本地,應(yīng)用重啟后仍能獲取,適合長(zhǎng)期緩存。
核心思路:發(fā)起請(qǐng)求前,先檢查緩存中是否有數(shù)據(jù);如果有,直接使用緩存數(shù)據(jù);如果沒有,發(fā)起網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求成功后將數(shù)據(jù)存入緩存。(具體實(shí)現(xiàn)可結(jié)合鴻蒙的 Preferences 模塊和 LRU 緩存類,這里就不貼完整代碼了,感興趣的小伙伴可以留言交流)
- 異常處理:覆蓋所有可能的失敗場(chǎng)景
網(wǎng)絡(luò)請(qǐng)求的失敗場(chǎng)景有很多(無網(wǎng)絡(luò)、超時(shí)、響應(yīng)碼錯(cuò)誤、數(shù)據(jù)解析失敗等),新手很容易只處理一部分異常,導(dǎo)致應(yīng)用崩潰。這里整理了常見的異常場(chǎng)景及處理方式:
無網(wǎng)絡(luò):通過鴻蒙的網(wǎng)絡(luò)狀態(tài)API判斷網(wǎng)絡(luò)是否可用,提前提示用戶“請(qǐng)檢查網(wǎng)絡(luò)連接”。
請(qǐng)求超時(shí):在工具類中設(shè)置合理的超時(shí)時(shí)間,超時(shí)后提示用戶“請(qǐng)求超時(shí),請(qǐng)重試”。
響應(yīng)碼錯(cuò)誤:根據(jù)響應(yīng)碼判斷錯(cuò)誤類型(404:接口不存在;401:未登錄;500:服務(wù)器錯(cuò)誤),給出對(duì)應(yīng)的提示。
數(shù)據(jù)解析失?。赫?qǐng)求成功但數(shù)據(jù)格式不符合預(yù)期,捕獲JSON.parse異常,提示“數(shù)據(jù)加載異?!薄?/p>
四、避坑總結(jié):新手必看,少走彎路
結(jié)合自己踩過的坑,整理了10個(gè)新手常犯的錯(cuò)誤,看完能幫你節(jié)省大量排查問題的時(shí)間:
忘記配置網(wǎng)絡(luò)權(quán)限,導(dǎo)致請(qǐng)求直接失敗,報(bào)錯(cuò)信息模糊。
請(qǐng)求完成后未調(diào)用 destroy() 方法,導(dǎo)致內(nèi)存泄漏。
復(fù)用 httpRequest 對(duì)象,導(dǎo)致多個(gè)請(qǐng)求沖突,出現(xiàn)異常。
POST 請(qǐng)求未設(shè)置 Content-Type,導(dǎo)致后端無法解析請(qǐng)求體。
GET 請(qǐng)求參數(shù)未編碼,包含特殊字符(如中文、&),導(dǎo)致請(qǐng)求失敗。
未處理超時(shí)異常,網(wǎng)絡(luò)較差時(shí)應(yīng)用一直處于加載狀態(tài),用戶體驗(yàn)差。
直接使用 response.result,未判斷響應(yīng)碼,導(dǎo)致請(qǐng)求失敗時(shí)解析數(shù)據(jù)報(bào)錯(cuò)。
工具類未用單例模式,多次實(shí)例化,造成資源浪費(fèi)。
緩存數(shù)據(jù)未設(shè)置過期時(shí)間,導(dǎo)致數(shù)據(jù)過時(shí),展示錯(cuò)誤信息。
異常處理不完整,未捕獲所有可能的錯(cuò)誤,導(dǎo)致應(yīng)用崩潰。