HarmonyOS學習記錄5
本文為個人學習記錄,僅供參考,如有錯誤請指出。本文主要記錄網絡請求的開發知識。
參考文檔:HTTP和RCP訪問網絡
網絡連接
概述
網絡連接管理提供管理網絡一些基礎能力,包括WiFi/蜂窩/Ethernet等多網絡連接優先級管理、網絡質量評估、訂閱默認/指定網絡連接狀態變化、查詢網絡連接信息、DNS解析等功能。
接收指定網絡的狀態變化通知
-
聲明接口調用所需要的權限:ohos.permission.GET_NETWORK_INFO
-
從@kit.NetworkKit中導入connection命名空間
// 引入包名。
import { connection } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
- 調用調用createNetConnection方法,指定網絡能力、網絡類型和超時時間(可選,如不傳入代表默認網絡;創建不同于默認網絡時可通過指定這些參數完成),創建一個NetConnection對象
let netSpecifier: connection.NetSpecifier = {netCapabilities: {// 假設當前默認網絡是WiFi,需要創建蜂窩網絡連接,可指定網絡類型為蜂窩網。bearerTypes: [connection.NetBearType.BEARER_CELLULAR],// 指定網絡能力為Internet。networkCap: [connection.NetCap.NET_CAPABILITY_INTERNET]},
};// 指定超時時間為10s(默認值為0)。
let timeout = 10 * 1000;// 創建NetConnection對象。
let conn = connection.createNetConnection(netSpecifier, timeout);
- 調用該對象的register方法,訂閱指定網絡狀態變化的通知。當網絡可用時,會收到netAvailable事件的回調;當網絡不可用時,會收到netUnavailable事件的回調
// 訂閱指定網絡狀態變化的通知。
conn.register((err: BusinessError, data: void) => {console.log(JSON.stringify(err));
});
- 調用該對象的on()方法,傳入type和callback,訂閱關心的事件
// 訂閱事件,如果當前指定網絡可用,通過on_netAvailable通知用戶。
conn.on('netAvailable', ((data: connection.NetHandle) => {console.log("net is available, netId is " + data.netId);
}));// 訂閱事件,如果當前指定網絡不可用,通過on_netUnavailable通知用戶。
conn.on('netUnavailable', ((data: void) => {console.log("net is unavailable, data is " + JSON.stringify(data));
}));
- 當不使用該網絡時,可以調用該對象的unregister()方法,取消訂閱
// 當不使用該網絡時,可以調用該對象的unregister()方法,取消訂閱。
conn.unregister((err: BusinessError, data: void) => {
});
HTTP數據請求
概述
應用通過HTTP發起一個數據請求,支持常見的GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT方法。當前提供了2種HTTP請求方式,若請求發送或接收的數據量較少,可使用HttpRequest.request,若是大文件的上傳或者下載,且關注數據發送和接收進度,可使用HTTP請求流式傳輸HttpRequest.requestInstream。
發起HTTP數據請求
- 導入HTTP一般數據請求所需模塊
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
- 調用createHttp()方法,創建HttpRequest對象
let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
// 每一個httpRequest對應一個HTTP請求任務,不可復用。
let httpRequest = http.createHttp();
- 調用該對象的on()方法,訂閱HTTP響應頭事件,此接口會比request請求先返回。可以根據業務需要訂閱此消息
// 用于訂閱HTTP響應頭,此接口會比request請求先返回。可以根據業務需要訂閱此消息。
// 從API 8開始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。
httpRequest.on('headersReceive', (header) => {console.info('header: ' + JSON.stringify(header));
});
- 發起HTTP請求,解析服務器響應事件,調用該對象的request()方法,傳入HTTP請求的url地址和可選參數,發起網絡請求,按照實際業務需要,解析返回結果
httpRequest.request(// 填寫HTTP請求的URL地址,可以帶參數或不帶參數。URL地址由開發者自定義。請求的參數可以在extraData中指定。"EXAMPLE_URL",{method: http.RequestMethod.POST, // 可選,默認為http.RequestMethod.GET,用于從服務器獲取數據,而POST方法用于向服務器上傳數據。// 開發者根據自身業務需要添加header字段。header: {'Content-Type': 'application/json'},// 當使用POST請求時此字段用于傳遞請求體內容,具體格式與服務端協商確定。extraData: "data to send",expectDataType: http.HttpDataType.STRING, // 可選,指定返回數據的類型。usingCache: true, // 可選,默認為true。priority: 1, // 可選,默認為1。connectTimeout: 60000, // 可選,默認為60000ms。readTimeout: 60000, // 可選,默認為60000ms。usingProtocol: http.HttpProtocol.HTTP1_1, // 可選,協議類型默認值由系統自動指定。usingProxy: false, // 可選,默認不使用網絡代理,自API 10開始支持該屬性。caPath:'/path/to/cacert.pem', // 可選,默認使用系統預制證書,自API 10開始支持該屬性。clientCert: { // 可選,默認不使用客戶端證書,自API 11開始支持該屬性。certPath: '/path/to/client.pem', // 默認不使用客戶端證書,自API 11開始支持該屬性。keyPath: '/path/to/client.key', // 若證書包含Key信息,傳入空字符串,自API 11開始支持該屬性。certType: http.CertType.PEM, // 可選,默認使用PEM,自API 11開始支持該屬性。keyPassword: "passwordToKey" // 可選,輸入key文件的密碼,自API 11開始支持該屬性。},multiFormDataList: [ // 可選,僅當Header中,'content-Type'為'multipart/form-data'時生效,自API 11開始支持該屬性,該屬性用于支持向服務器上傳二進制數據,根據上傳的具體數據類型進行選擇。{name: "Part1", // 數據名,自API 11開始支持該屬性。contentType: 'text/plain', // 數據類型,自API 11開始支持該屬性,上傳的數據類型為普通文本文件。data: 'Example data', // 可選,數據內容,自API 11開始支持該屬性。remoteFileName: 'example.txt' // 可選,自API 11開始支持該屬性。}, {name: "Part2", // 數據名,自API 11開始支持該屬性。contentType: 'text/plain', // 數據類型,自API 11開始支持該屬性,上傳的數據類型為普通文本文件。// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.txt。filePath: `${context.filesDir}/fileName.txt`, // 可選,傳入文件路徑,自API 11開始支持該屬性。remoteFileName: 'fileName.txt' // 可選,自API 11開始支持該屬性。}, {name: "Part3", // 數據名,自API 11開始支持該屬性。contentType: 'image/png', // 數據類型,自API 11開始支持該屬性,上傳的數據類型為png格式的圖片。// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.png。filePath: `${context.filesDir}/fileName.png`, // 可選,傳入文件路徑,自API 11開始支持該屬性。remoteFileName: 'fileName.png' // 可選,自API 11開始支持該屬性。}, {name: "Part4", // 數據名,自API 11開始支持該屬性。contentType: 'audio/mpeg', // 數據類型,自API 11開始支持該屬性,上傳的數據類型為mpeg格式的音頻。// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.mpeg。filePath: `${context.filesDir}/fileName.mpeg`, // 可選,傳入文件路徑,自API 11開始支持該屬性。remoteFileName: 'fileName.mpeg' // 可選,自API 11開始支持該屬性。}, {name: "Part5", // 數據名,自API 11開始支持該屬性。contentType: 'video/mp4', // 數據類型,自API 11開始支持該屬性,上傳的數據類型為mp4格式的視頻。// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.mp4。filePath: `${context.filesDir}/fileName.mp4`, // 可選,傳入文件路徑,自API 11開始支持該屬性。remoteFileName: 'fileName.mp4' // 可選,自API 11開始支持該屬性。}]}, (err: BusinessError, data: http.HttpResponse) => {if (!err) {// data.result為HTTP響應內容,可根據業務需要進行解析。console.info('Result:' + JSON.stringify(data.result));console.info('code:' + JSON.stringify(data.responseCode));// data.header為HTTP響應頭,可根據業務需要進行解析。console.info('header:' + JSON.stringify(data.header));console.info('cookies:' + JSON.stringify(data.cookies)); // 8+// 當該請求使用完畢時,調用destroy方法主動銷毀。httpRequest.destroy();} else {console.error('error:' + JSON.stringify(err));// 取消訂閱HTTP響應頭事件。httpRequest.off('headersReceive');// 當該請求使用完畢時,調用destroy方法主動銷毀。httpRequest.destroy();}});
- 調用該對象的off()方法,取消訂閱HTTP響應頭事件
// 在不需要該回調信息時,需要取消訂閱HTTP響應頭事件,該方法調用的時機,可以參考步驟4中的示例代碼。
httpRequest.off('headersReceive');
- 當該請求使用完畢時,調用destory()方法銷毀
// 當該請求使用完畢時,調用destroy方法主動銷毀,該方法調用的時機,可以參考步驟4中的示例代碼。
httpRequest.destroy();
基于RCP的網絡請求
概述
Remote Communication Kit中的@hms.collaboration.rcp(后續簡稱RCP)指的是遠程通信平臺(remote communication platform),RCP提供了網絡數據請求功能,相較于Network Kit中HTTP請求能力,RCP更具易用性,且擁有更多的功能。在開發過程中,如果有些場景使用Network Kit中HTTP請求能力達不到預期或無法實現,那么就可以嘗試使用RCP中的數據請求功能來實現。
RCP與HTTP的區別
功能分類 | 功能名稱 | 功能描述 | HTTP | RCP |
---|---|---|---|---|
基礎功能 | 發送PATCH類型請求 | 以PATCH的方式請求 | ? | ? |
基礎功能 | 設置會話中URL的基地址 | 會話中URL的基地址將自動加在URL前面,除非URL是一個絕對的URL | ? | ? |
基礎功能 | 取消自動重定向 | HTTP請求不會自動重定向 | ? | ? |
基礎功能 | 攔截請求和響應 | 在請求后或響應前進行攔截 | ? | ? |
基礎功能 | 取消請求 | 發送請求前取消、發送請求過程中取消、請求接收后取消 | ? | ? |
基礎功能 | 響應緩存 | 是否使用緩存,請求時優先讀取緩存。緩存跟隨當前進程生效,新緩存會替換舊緩存 | ? | ? |
基礎功能 | 設置響應數據的類型 | 設置數據以何種方式返回,將要響應的數據類型可設置為string、object、arraybuffer等類型 | ? | ? |
基礎功能 | 定義允許的HTTP響應內容的最大字節數 | 服務器成功響應時,在獲取數據前校驗響應內容的最大字節數 | ? | ? |
證書驗證 | 自定義證書校驗 | 自定義邏輯校驗客戶端和服務端的證書,判斷是否可以連接 | ? | ? |
證書驗證 | 忽略SSL校驗 | 在建立SSL連接時不驗證服務器端的SSL證書 | ? | ? |
DNS | 自定義DNS解析 | 包括自定義DNS服務器或靜態DNS規則 | ? | ? |
RCP特有 | 捕獲詳細的跟蹤信息 | 在會話中的HTTP請求期間捕獲詳細的跟蹤信息。跟蹤有助于調試、性能分析和深入了解通信過程中的數據流 | ? | ? |
RCP特有 | 數據打點,獲取HTTP請求的具體數據 | HTTP請求各階段的定時信息 | ? | ? |
發起基礎的RCP網絡請求
通過RCP模塊能夠發起基礎的網絡請求,如GET、POST、HEAD、PUT、DELETE、PATCH、OPTIONS等請求。以PATCH請求為例,開發過程中經常會遇到發送請求修改資源的場景,假設有一個UserInfo,里面有userId、userName、 userGender等10個字段。可編輯功能因為需求,在某個特別的頁面里只能修改userName,這時就可以用PATCH請求,來更新局部資源
- 導入RCP模塊
- 創建headers,設置可接受的數據內容的類型為json字符串;創建modifiedContent,傳入想要修改的內容
- 調用rcp.createSession()創建通信會話對象session
- 使用new rcp.Requset()方法創建請求對象req
- 調用session.fetch()方法發起請求
- 獲取響應結果
核心代碼
// 定義請求頭
let headers: rcp.RequestHeaders = {'accept': 'application/json'
};
// 定義要修改的內容
let modifiedContent: UserInfo = {'userName': 'xxxxxx'
};
const securityConfig: rcp.SecurityConfiguration = {tlsOptions: {tlsVersion: 'TlsV1.3'}
};
// 創建通信會話對象
const session = rcp.createSession({ requestConfiguration: { security: securityConfig } });
// 定義請求對象rep
let req = new rcp.Request('http://example.com/fetch', 'PATCH', headers, modifiedContent);
// 發起請求
session.fetch(req).then((response) => {Logger.info(`Request succeeded, message is ${JSON.stringify(response)}`);
}).catch((err: BusinessError) => {Logger.error(`err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);
});
實驗
一、網絡狀態監聽的實現
本實驗希望實現的效果是在首頁登錄時,不僅僅對賬號密碼進行非空校驗,還需要對網絡狀態進行檢測,在沒有網絡的情況下,拒絕登錄操作并提示用戶網絡未連接。
首先,我們在ets目錄下創建一個common文件夾,用于存放一些公共能力。隨后在common文件夾下創建network文件夾并創建ConnectionUtils.ets(網絡連接工具類),我們將在這個文件中實現網絡狀態監聽功能。
step1 分析需要實現的功能:
- 能夠完成是否有網絡連接的校驗,從而在點擊登錄時,不僅對賬號密碼進行非空校驗,還要完成對網絡狀態的校驗,無網絡時不進行跳轉
- 能夠訂閱網絡狀態,在網絡狀態變化時,主動觸發彈窗提醒用戶網絡狀態的變化
step2 網絡連接工具類的封裝:
在實現網絡連接工具類的封裝前,首先需要申請權限,在entry/src/main目錄下的module.json5中,添加網絡訪問權限和網絡信息查看權限(即代碼塊中requestPermissions包含的內容,其中的string:Internet和string:Internet和string:Internet和string:network_info字段需要在resource/base/element/string.json文件中自行添加)
{"module": {"name": "entry","type": "entry","description": "$string:module_desc","mainElement": "EntryAbility","deviceTypes": ["phone"],"routerMap": "$profile:route_map","deliveryWithInstall": true,"installationFree": false,"pages": "$profile:main_pages","abilities": [{"name": "EntryAbility","srcEntry": "./ets/entryability/EntryAbility.ets","description": "$string:EntryAbility_desc","icon": "$media:startIcon","label": "$string:EntryAbility_label","startWindowIcon": "$media:icon","startWindowBackground": "$color:start_window_background","exported": true,"skills": [{"entities": ["entity.system.home"],"actions": ["action.system.home"]}]}],"requestPermissions": [{"name": "ohos.permission.INTERNET","reason": "$string:Internet","usedScene": {"abilities": ["EntryAbility"],"when": "inuse"}},{"name": "ohos.permission.GET_NETWORK_INFO","reason": "$string:network_info","usedScene": {"abilities": ["EntryAbility"],"when": "inuse"}}]}
}
接下來,在剛才創建的ConnectionUtils.ets文件中寫入下方的代碼。實現是否有網絡的校驗,需要使用@kit.NetworkKit的connection模塊,首先我們定義一個名為isNetworkConnected()的方法用于判斷是否有網絡連接,所以該函數應該返回一個布爾值。而具體的方法實現,需要使用connection模塊提供的connection.getDefaultNet()方法,該方法可以獲取到當前連接網絡的信息,同時也可以判斷是否有網絡連接。connection模塊還提供了connection.getNetCapabilities()方法,可以獲取當前網絡的具體信息,包括網絡類型、網絡具體能力等
import { connection } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG: string = 'ConnectionUtils';/*** 這里的ConnectionUtils類用于提供網絡監聽的能力*/
export class ConnectionUtils {/*** 這里的isNetworkConnected方法用于檢查監測網絡是否連接*/async isNetworkConnected(): Promise<boolean> {let result: boolean = false;await connection.getDefaultNet().then(async (data: connection.NetHandle) => {if (data.netId === 0) {hilog.info(0x0000, TAG, 'network error');return;}await connection.getNetCapabilities(data).then((data: connection.NetCapabilities) => {let bearerTypes: Set<number> = new Set(data.bearerTypes);let bearerTypesNum = Array.from(bearerTypes.values());for (let item of bearerTypesNum) {if (item === 0) {result = true;hilog.info(0x0000, TAG, 'BEARER_CELLULAR');} else if (item === 1) {result = true;hilog.info(0x0000, TAG, 'BEARER_WIFI');} else if (item === 3) {result = true;hilog.info(0x0000, TAG, 'BEARER_ETHERNET');} else {return;}}})})return result;}
}export default new ConnectionUtils();
在上述代碼的基礎上繼續完善監聽功能。完成了關于網絡狀態判斷的方法后,我們來實現網絡狀態的監聽功能,目的是在網絡狀態發生變更時,及時通過彈窗提醒用戶。主要需要使用NetConnection提供的訂閱能力以及網絡監聽能力,在使用網絡狀態監聽之前,我們需要首先進行訂閱,在這里,我們繼續為工具類中添加自定義封裝方法,首先是訂閱方法openRegister()以及取消訂閱方法closeRegister(),另外封裝了一個用于網絡狀態監聽的方法registerNetworkAvailableStatus()(其中的r(′app.string.NetworkAvailable’)、r('app.string.Network_Available’)、r(′app.string.NetworkA?vailable’)、r('app.string.Network_Unavailable’)和$r(‘app.string.Network_Lost’)字段需要在resource/base/element/string.json文件中自行添加)
import { connection } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG: string = 'ConnectionUtils';/*** 這里的ConnectionUtils類用于提供網絡監聽的能力*/
export class ConnectionUtils {netConnection = connection.createNetConnection();/*** 這里的isNetworkConnected方法用于檢查監測網絡是否連接*/async isNetworkConnected(): Promise<boolean> {let result: boolean = false;await connection.getDefaultNet().then(async (data: connection.NetHandle) => {if (data.netId === 0) {hilog.info(0x0000, TAG, 'network error');return;}await connection.getNetCapabilities(data).then((data: connection.NetCapabilities) => {let bearerTypes: Set<number> = new Set(data.bearerTypes);let bearerTypesNum = Array.from(bearerTypes.values());for (let item of bearerTypesNum) {if (item === 0) {result = true;hilog.info(0x0000, TAG, 'BEARER_CELLULAR');} else if (item === 1) {result = true;hilog.info(0x0000, TAG, 'BEARER_WIFI');} else if (item === 3) {result = true;hilog.info(0x0000, TAG, 'BEARER_ETHERNET');} else {return;}}})})return result;}/*** 該方法用于打開注冊表*/openRegister() {this.netConnection.register((error: BusinessError) => {hilog.info(0x0000, TAG, JSON.stringify(error));});}/*** 該方法用于監聽網絡的狀態*/registerNetworkAvailableStatus() {this.netConnection.on('netAvailable', () => {promptAction.showToast({message: $r('app.string.Network_Available'),duration: 2000});});this.netConnection.on('netUnavailable', () => {promptAction.showToast({message: $r('app.string.Network_Unavailable'),duration: 2000});});this.netConnection.on('netLost', () => {promptAction.showToast({message: $r('app.string.Network_Lost'),duration: 2000});});}/*** 該方法用于關閉注冊表*/closeRegister() {this.netConnection.unregister((error: BusinessError) => {hilog.info(0x0000, TAG, JSON.stringify(error));});}
}export default new ConnectionUtils();
至此,我們完成了網絡狀態監聽工具類的封裝。接下來,我們來使用工具類中的方法,豐富登錄頁面的網絡校驗功能
step3 使用網絡連接工具類實現網絡檢驗登錄
封裝好網絡狀態工具類后,我們來使用工具類中的方法實現網絡檢驗登錄。首先我們修改一下首頁登錄按鈕的邏輯,使用網絡狀態工具類中的判斷是否有網絡連接的方法,并根據返回的布爾值進行網絡狀態的校驗,無網絡不進行跳轉(需要先在pages文件夾中創建一個LoginPage.ets文件)
import { promptAction } from '@kit.ArkUI';
import ConnectionUtils from '../common/network/ConnectionUtils';@Extend(TextInput)
function inputStyle() {.placeholderColor('#99182431').height('45vp').fontSize('18fp').backgroundColor('#F1F3F5').width('328vp').margin({ top: 12 })
}@Extend(Line)
function lineStyle() {.width('328vp').height('1vp').backgroundColor('#33182431')
}@Extend(Text)
function blueTextStyle() {.fontColor('#007DFF').fontSize('14fp').fontWeight(FontWeight.Medium)
}/*** Login page*/
@Entry
@Component
struct LoginPage {@State account: string = '';@State password: string = '';@State isShowProgress: boolean = false;private timeOutId: number = -1;pathStack: NavPathStack = new NavPathStack();@BuilderimageButton(src: Resource) {Button({ type: ButtonType.Circle, stateEffect: true }) {Image(src)}.height('48vp').width('48vp').backgroundColor('#F1F3F5')}login(result: boolean): void {if (this.account === '' || this.password === '') {promptAction.showToast({message: $r('app.string.input_empty_tips')});} else {this.isShowProgress = true;if (this.timeOutId === -1) {this.timeOutId = setTimeout(async () => {this.isShowProgress = false;this.timeOutId = -1;if (result) {this.pathStack.pushPathByName('MainPage', null);} else {promptAction.showToast({message: '無網絡連接,無法登錄'});}}, 2000);}}}aboutToDisappear() {clearTimeout(this.timeOutId);this.timeOutId = -1;}build() {Navigation(this.pathStack) {Column() {Image($r('app.media.logo')).width('78vp').height('78vp').margin({top: '150vp',bottom: '8vp'})Text($r('app.string.login_page')).fontSize('24fp').fontWeight(FontWeight.Medium).fontColor('#182431')Text($r('app.string.login_more')).fontSize('16fp').fontColor('#99182431').margin({bottom: '30vp',top: '8vp'})TextInput({ placeholder: $r('app.string.account') }).maxLength(11).type(InputType.Number).inputStyle().onChange((value: string) => {this.account = value;})Line().lineStyle()TextInput({ placeholder: $r('app.string.password') }).maxLength(8).type(InputType.Password).inputStyle().onChange((value: string) => {this.password = value;})Line().lineStyle()Row() {Text($r('app.string.message_login')).blueTextStyle()Text($r('app.string.forgot_password')).blueTextStyle()}.justifyContent(FlexAlign.SpaceBetween).width('328vp').margin({ top: '8vp' })Button($r('app.string.login'), { type: ButtonType.Capsule }).width('328vp').height('40vp').fontSize('16fp').fontWeight(FontWeight.Medium).backgroundColor('#007DFF').margin({top: '48vp',bottom: '12vp'}).onClick(async () => {await ConnectionUtils.isNetworkConnected().then((value) => {this.login(value);})})Text($r('app.string.register_account')).fontColor('#007DFF').fontSize('16fp').fontWeight(FontWeight.Medium)if (this.isShowProgress) {LoadingProgress().color('#182431').width('30vp').height('30vp').margin({ top: '20vp' })}Blank()Text($r('app.string.other_login_method')).fontColor('#838D97').fontSize('12fp').fontWeight(FontWeight.Medium).margin({top: '50vp',bottom: '12vp'})Row({ space: 44 }) {this.imageButton($r('app.media.login_method1'))this.imageButton($r('app.media.login_method2'))this.imageButton($r('app.media.login_method3'))}.margin({ bottom: '16vp' })}.height('100%').width('100%').padding({left: '12vp',right: '12vp',bottom: '24vp'})}.backgroundColor('#F1F3F5').width('100%').height('100%').hideTitleBar(true).hideToolBar(true)}
}
接下來我們還需要啟動網絡狀態的監聽,主要在EntryAbility.ets中完成。在onCreate()方法中開啟訂閱,onWindowStageCreate()方法中調用監聽方法,在onWindowStageDestroy()方法中關閉訂閱
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import ConnectionUtils from '../common/network/ConnectionUtils';/*** Lift cycle management of Ability.*/
export default class entryAbility extends UIAbility {onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {ConnectionUtils.openRegister();hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? '');hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? '');}onDestroy(): void | Promise<void> {hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');}onWindowStageCreate(windowStage: window.WindowStage): void {// Main window is created, set main page for this abilityhilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');windowStage.getMainWindow().then((data: window.Window) => {// Window immersive.data.setWindowLayoutFullScreen(true);});windowStage.loadContent('pages/LoginPage', (err, data) => {if (err.code) {hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.ERROR);hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}ConnectionUtils.registerNetworkAvailableStatus();hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');});}onWindowStageDestroy(): void {// Main window is destroyed, release UI related resourceshilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');ConnectionUtils.closeRegister();}onForeground(): void {// Ability has brought to foregroundhilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');}
}
這樣,我們就完成了登錄邏輯中網絡狀態校驗及訂閱網絡狀態變化的監聽
二、使用HTTP請求網絡數據
這個部分需要在ets/common/network中重新創建一個文件,命名為HttpUtils.ets
step1 分析具體實現功能
- 使用HTTP發起get請求,獲取一張箭頭圖片并保存在沙箱路徑中,并返回其沙箱路徑對應的uri,以便使用箭頭圖片作為首頁列表的布局元素
- 使用HTTP發起post請求,獲取官網中的圖片與文本信息,并將其封裝在實體類數組中,以便將這些信息作為首頁列表的布局元素
step2 HTTP工具類的封裝
首先,使用@kit.NteworkKit中的http模塊提供的createHttp()方法獲取一個HttpRequest對象,再通過封裝request()方法實現通過get請求網絡圖片,在獲取到網絡圖片之后,我們需要使用到@kit.CoreFileKit提供的文件管理能力將獲取的圖片資源保存到沙箱路徑。然后需要再實現一個與get請求一致的postHttpRequest()方法,用于傳入網絡請求地址url以及網絡請求配置參數HttpRequestOptions
import { http } from '@kit.NetworkKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import ListInfo from '../../viewmodel/ListInfo';
import ResponseData from '../../viewmodel/ResponseData';const TAG: string = 'HttpUtils';/*** 類的頭部*/
class Header{contentType: string;constructor(contentType: string) {this.contentType = contentType;}
}/*** HttpUtils提供通過HTTP協議進入網絡的能力*/
export class HttpUtils {httpRequest: http.HttpRequest;constructor() {this.httpRequest = http.createHttp();}/*** 通過 HTTP 發起 GET 請求的方法*/async getHttpRequest(cacheDir: string): Promise<string> {let responsePictureUri: string = '';await this.httpRequest.request('https://developer.huawei.com/system/modules/org.opencms.portal.template.core/' +'resources/harmony/img/jiantou_right.svg',{ method: http.RequestMethod.GET }).then((data: http.HttpResponse) => {let filePath = cacheDir + '/test.svg';let file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);responsePictureUri = fileUri.getUriFromPath(filePath);fileIo.writeSync(file.fd, data.result as ArrayBuffer);fileIo.closeSync(file.fd);})return responsePictureUri;}async postHttpRequest(): Promise<ListInfo[]> {let responseData: Array<ListInfo> = [];await this.httpRequest.request('https://svc-drcn.developer.huawei.com/community/servlet/consumer' +'/partnerActivityService/v1/developer/activity/terminalActivities/list',{method: http.RequestMethod.POST, extraData: {'status': '1','belong': '1','language': 'cn','needTop': 1,'displayChannel': [1, 3],'count': 4,'pagestart': 1,'type': '1,4,5,6'},header: new Header('application/json;charset=UTF-8')}).then((data: http.HttpResponse) => {let result: ResponseData = JSON.parse(data.result as string);responseData = result.value.list;}).catch((err: Error) => {hilog.info(0x0000, TAG, JSON.stringify(err));});return responseData;}/*** 銷毀HttpRequest對象的方法*/destroyHttpRequest(){this.httpRequest.destroy();}
}
其中,還需要在/ets/viewmodel中創建ResponseData文件夾并創建ListInfo、ResponseData、Value三個實體類,注意,這三個類需要分別存放,本文中將代碼寫在了同一塊中只是為了便于查看,這三個類用于承接回調函數中HttpResponse類型的對象data獲取的返回值
// ListInfo.ets
export default class ListInfo{public activityName: string;public theme: string;public indexNavPic: string;constructor(activityName: string,theme: string,indexNavPic: string) {this.activityName = activityName;this.theme = theme;this.indexNavPic = indexNavPic;}
}// ResponseData.ets
import Value from "./Value";export default class ResponseData{public code: string;public value: Value;constructor(code: string,value: Value) {this.code = code;this.value = value;}
}// Value.ets
import ListInfo from "./ListInfo";export default class Value{public list: Array<ListInfo>;constructor(list: Array<ListInfo>) {this.list = list;}
}
至此,就完成了HttpUtils工具類的封裝
step3 使用HTTP工具類實現首頁列表
首先需要做的是在Home組件的aboutToAppear生命周期中,通過postHttpRequest()和getHttpRequest()方法,分別獲取到列表項中的文字以及圖片數據源和箭頭圖片的uri,并定義兩個狀態變量httpGridItems和pictureUri用來獲取返回結果。注意,使用完之后記得通過前面封裝好的destoryHttpRequest()方法銷毀HttpRequest對象。在獲取到List所需要的資源并存儲到狀態變量后,我們通過List組件配合ForEach進行循環渲染,從而實現首頁List的渲染
import { HttpUtils } from "../common/network/HttpUtils";
import { common } from "@kit.AbilityKit";
import GridData from "../viewmodel/GridData";
import ListInfo from "../viewmodel/ResponseData/ListInfo";
import mainViewModel from "../viewmodel/MainViewModel";@Component
export default struct Home {@State httpGridItems: Array<ListInfo> = [];@State pictureUri: string = '';private TopSwiperController: SwiperController = new SwiperController();private context = getContext(this) as common.UIAbilityContext;private applicationContext = this.context.getApplicationContext();private cacheDir = this.applicationContext.filesDir;async aboutToAppear(): Promise<void> {let httpUtil: HttpUtils = new HttpUtils();await httpUtil.postHttpRequest().then((value: Array<ListInfo>) => {this.httpGridItems = value;});await httpUtil.getHttpRequest(this.cacheDir).then((value: string) => {this.pictureUri = value;});httpUtil.destroyHttpRequest();}build() {Column() {Text('首頁').width('95%').textAlign(TextAlign.Start).fontWeight(FontWeight.Bold).fontSize(25).margin({ top: 10, bottom: 10 })Scroll() {Column() {Swiper(this.TopSwiperController) {ForEach(mainViewModel.getSwiperImages(), (img: Resource) => {Image(img).width('95%').objectFit(ImageFit.Auto)})}.autoPlay(true).loop(true).width('95%').height(220).margin({ bottom: 10 })Grid() {ForEach(mainViewModel.getGridItem(), (item: GridData) => {GridItem() {Column() {Image(item.img).size({width: 30,height: 30})Text(item.title).fontSize(16)}}})}.rowsTemplate('1fr 1fr').columnsTemplate('1fr 1fr 1fr 1fr').backgroundColor('#ffffff').width('95%').height(120).borderRadius(15)Text('列表').fontWeight(FontWeight.Bold).fontSize(20).width('95%').textAlign(TextAlign.Start).margin({top: 10,bottom: 10})List() {ForEach(this.httpGridItems, (secondItem: ListInfo) => {ListItem() {Row() {Image(secondItem.indexNavPic).width('130vp').height('80vp').objectFit(ImageFit.TOP_START).borderRadius('8vp').margin({ right: '12vp' })Column() {Text(secondItem.activityName).width('190vp').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize('16fp').fontWeight(FontWeight.Medium)Text(secondItem.theme).width('190vp').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(2).margin({ top: '4vp' }).fontSize('12fp').fontColor('#99182431')Row() {Image(this.pictureUri).width('20vp').opacity(0.5)}.width('170vp').margin({ top: '10.5vp' }).justifyContent(FlexAlign.End).alignItems(VerticalAlign.Bottom)}.alignItems(HorizontalAlign.Start)}.width('100%').padding({left: '12vp',right: '12vp',top: '12vp',bottom: '12vp'}).justifyContent(FlexAlign.SpaceBetween)}.margin({ bottom: '8vp' }).borderRadius('16vp').backgroundColor('#ffffff').align(Alignment.TopStart).width('100%')}, (secondItem: ListInfo) => JSON.stringify(secondItem))}.scrollBar(BarState.Off).width('100%')}}.scrollBar(BarState.Off)}.justifyContent(FlexAlign.Start).height('100%').width('100%')}
}
整體效果:
三、使用RCP請求網絡數據
我們不僅可以通過HTTP訪問網絡,還可以通過RCP發起網絡請求實現相同的功能
step1 分析具體實現功能
- 使用RCP發起get請求,獲取一張箭頭圖片并保存在沙箱路徑中,并返回其沙箱路徑對應的uri,以便使用箭頭圖片作為首頁列表的布局元素
- 使用RCP發起post請求,獲取官網中的圖片與文本信息,并將其封裝在實體類數組中,以便將這些信息作為首頁列表的布局元素
step2 RCP工具類的封裝
我們通過@kit.RemoteCommunicationKit中的rcp模塊來使用RCP能力。在使用RCP發起網絡請求前,首先需要獲取一個Session類型的網絡會話對象,在這里,我們將該類型的對象作為工具類的對象屬性,并在構造方法中通過模塊提供的rcp.createSession()方法為該對象屬性賦值,以便后續方便的使用它。完成了獲取網絡會話對象Session后,我們就可以進一步封裝get請求和post請求獲取網絡信息的方法了。網絡會話對象Session提供了get()方法與post()方法,可以用于實現發起get請求和post請求,具體實現與http模塊類似,這里就不過多贅述了。最后,我們還需要像HTTP工具類一樣,封裝一個銷毀會話對象的方法,需要使用會話對象的close()方法。
import { rcp } from '@kit.RemoteCommunicationKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import ResponseData from '../../viewmodel/ResponseData/ResponseData';
import ListInfo from '../../viewmodel/ResponseData/ListInfo';const TAG: string = 'RCPUtils';
const list_source: string = 'https://svc-drcn.developer.huawei.com/community/servlet/consumer/' +'partnerActivityService/v1/developer/activity/terminalActivities/list';/*** RCPUtils提供了通過RCP訪問網絡的功能*/
export class RCPUtils {rcpSession: rcp.Session;constructor() {this.rcpSession = rcp.createSession();}/*** 通過RCP發起GET請求的方法*/async getRCPRequest(cacheDir: string): Promise<string> {let responsePictureUri: string = '';await this.rcpSession.get('https://developer.huawei.com/system/modules/org.opencms.portal.template.core/' +'resources/harmony/img/jiantou_right.svg').then((response) => {let filePath = cacheDir + '/test.svg';let file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);responsePictureUri = fileUri.getUriFromPath(filePath);fileIo.writeSync(file.fd, response.body);fileIo.closeSync(file.fd);}).catch((err: BusinessError) => {hilog.error(0x0000, TAG, `err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);});return responsePictureUri;}/*** 通過RCP發起POST請求的方法*/async postRCPRequest(): Promise<ListInfo[]> {let responseData: Array<ListInfo> = [];let requestContent: rcp.RequestContent = {'status': '1','belong': '1','language': 'cn','needTop': 1,'displayChannel': [1, 3],'count': 4,'pagestart': 1,'type': '1,4,5,6'}await this.rcpSession.post(list_source, requestContent).then((response) => {let result: ResponseData = response.toJSON() as ResponseData;responseData = result.value.list;}).catch((err: BusinessError) => {hilog.error(0x0000, TAG, `err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);});return responseData;}/*** 關閉RCP會話的方法*/destroySession() {this.rcpSession.close();}
}
至此,我們就完成了RCP工具類的封裝
step3 使用RCP工具類實現首頁列表
完成RCP工具類的封裝后,我們也可以通過RCP請求網絡數據,與HTTP實現渲染類似,只需要在生命周期aboutToAppear中,創建工具類對象,使用工具類封裝好的方法發起請求并存儲在在狀態變量中,然后通過List渲染即可。這個部分的代碼只需要在之前頁面的基礎上更換aboutToAppear生命周期中的方法就行將原來HTTP的方式修改成封裝好的RCP即可,代碼的最終效果和上面一樣,只是網絡請求的方式不同
import { common } from "@kit.AbilityKit";
import GridData from "../viewmodel/GridData";
import ListInfo from "../viewmodel/ResponseData/ListInfo";
import mainViewModel from "../viewmodel/MainViewModel";
import { RCPUtils } from "../common/network/RCPUtils";@Component
export default struct Home {@State httpGridItems: Array<ListInfo> = [];@State pictureUri: string = '';private TopSwiperController: SwiperController = new SwiperController();private context = getContext(this) as common.UIAbilityContext;private applicationContext = this.context.getApplicationContext();private cacheDir = this.applicationContext.filesDir;async aboutToAppear(): Promise<void> {let rcpUtil: RCPUtils = new RCPUtils();await rcpUtil.postRCPRequest().then((value: Array<ListInfo>) => {this.httpGridItems = value;});await rcpUtil.getRCPRequest(this.cacheDir).then((value: string) => {this.pictureUri = value;});rcpUtil.destroySession();}build() {Column() {Text('首頁').width('95%').textAlign(TextAlign.Start).fontWeight(FontWeight.Bold).fontSize(25).margin({ top: 10, bottom: 10 })Scroll() {Column() {Swiper(this.TopSwiperController) {ForEach(mainViewModel.getSwiperImages(), (img: Resource) => {Image(img).width('95%').objectFit(ImageFit.Auto)})}.autoPlay(true).loop(true).width('95%').height(220).margin({ bottom: 10 })Grid() {ForEach(mainViewModel.getGridItem(), (item: GridData) => {GridItem() {Column() {Image(item.img).size({width: 30,height: 30})Text(item.title).fontSize(16)}}})}.rowsTemplate('1fr 1fr').columnsTemplate('1fr 1fr 1fr 1fr').backgroundColor('#ffffff').width('95%').height(120).borderRadius(15)Text('列表').fontWeight(FontWeight.Bold).fontSize(20).width('95%').textAlign(TextAlign.Start).margin({top: 10,bottom: 10})List() {ForEach(this.httpGridItems, (secondItem: ListInfo) => {ListItem() {Row() {Image(secondItem.indexNavPic).width('130vp').height('80vp').objectFit(ImageFit.TOP_START).borderRadius('8vp').margin({ right: '12vp' })Column() {Text(secondItem.activityName).width('190vp').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1).fontSize('16fp').fontWeight(FontWeight.Medium)Text(secondItem.theme).width('190vp').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(2).margin({ top: '4vp' }).fontSize('12fp').fontColor('#99182431')Row() {Image(this.pictureUri).width('20vp').opacity(0.5)}.width('170vp').margin({ top: '10.5vp' }).justifyContent(FlexAlign.End).alignItems(VerticalAlign.Bottom)}.alignItems(HorizontalAlign.Start)}.width('100%').padding({left: '12vp',right: '12vp',top: '12vp',bottom: '12vp'}).justifyContent(FlexAlign.SpaceBetween)}.margin({ bottom: '8vp' }).borderRadius('16vp').backgroundColor('#ffffff').align(Alignment.TopStart).width('100%')}, (secondItem: ListInfo) => JSON.stringify(secondItem))}.scrollBar(BarState.Off).width('100%')}}.scrollBar(BarState.Off)}.justifyContent(FlexAlign.Start).height('100%').width('100%')}
}
至此,完成了網絡狀態的監聽以及使用HTTP和RCP請求網絡數據的學習
遇到的問題
-
stack組件的布局的使用問題,我想編寫一個層疊的效果,將按鈕顯示在頂部圖片的右側并緊貼,設置align屬性好像沒有效果,暫時設置了position屬性設置絕對位置來解決這個問題。最終,再次嘗試修改Stack的alignContent屬性并刪除了對position屬性的修改,實現了想要的效果,但是暫時不清楚之前出現問題的原因
-
scroll滾動組件在整體頁面中顯示不完整。因為原來使用的是NavDestination和Tabs的組合來構建底部標簽欄。Tabs 組件默認會在其包含的內容下方顯示標簽欄,這可能導致內容被標簽欄遮擋。修改之后將NavDestination組件修改成Flex組件,根據內容的大小動態地改變高度,現在就可以完整地顯示內容了
-
修改前:
-
修改后:
-
-
網絡問題,設置好了網絡監聽,檢查過判斷邏輯應該沒有出現問題,但是在部署到模擬器上時,斷開網絡仍可以進行登錄操作
-
在進行關系型數據庫案例開發的時候,遇到點擊“新增計劃”按鈕頁面中內容不顯示的問題。進行了逐個部分的排查,首先,在對應頁面Goal.ets的aboutToAppear()生命周期中查看了一下創建數據表和初始化數據表的方法調用代碼是否正確,發現正常;然后,前往關系型數據庫的相關文件中查看調用的方法本身是否有編寫或者邏輯錯誤,和官方代碼沒有區別,沒有問題;因為這個部分涉及到數據庫的使用,所以也需要檢查了一下CommonConstants.ets文件中的SQL語句是否有誤,也沒問題;然后仔細校對了官方代碼最后發現問題很簡單,但是容易遺漏,就是需要在EntryAbility.ets文件中,添加“RDBStoreUtil.createObjectiveRDB(this.context);”這段代碼,在組件實例化之前創建和獲取RdbStore實例,添加完這個代碼之后,終于可以正常點擊“新增計劃”按鈕并顯示相關數據了
-
但是在上述問題解決之后又出現了新的問題。在添加完新的數據之后,使用篩選功能對任務是否完成為條件進行篩選無法選出正確的結果
- 效果展示:
- 效果展示:
-
對網絡請求的加深學習與函數的加深理解
// 通過HTTP發起POST請求的方法
async postHttpRequest(): Promise<ListInfo[]> { // async為關鍵字,說明這個函數將進行異步操作let responseData: Array<ListInfo> = []; // 定義了一個Array類型結構為ListInfo的變量并賦了一個空值await this.httpRequest.request('https://svc-drcn.developer.huawei.com/community/servlet/consumer' + //await也為關鍵字,代表同步,需與async同時出現,有async必有await,但是有await不一定有async'/partnerActivityService/v1/developer/activity/terminalActivities/list', // 這里是發起一個request請求,具體參數類型可點擊request查看,第一個參數為請求的接口地址,第二個參數為{請求方法(如POST、PUT、GET等),請求的數據的結構,請求的頭部}{method: http.RequestMethod.POST, extraData: {'status': '1','belong': '1','language': 'cn','needTop': 1,'displayChannel': [1, 3],'count': 4,'pagestart': 1,'type': '1,4,5,6'},header: new Header('application/json;charset=UTF-8')}).then((data: http.HttpResponse) => { // 這里的.then的作用是在上述request之后進行的一個串行操作,就是接收上述request請求返回的值并作為匿名函數的參數進行后續步驟let result: ResponseData = JSON.parse(data.result as string); // 在上述接收到request請求返回的值之后,將該值轉譯并賦值給ResponseData類型的result變量responseData = result.value.list; // 再次賦值}).catch((err: Error) => { // 獲取報錯信息hilog.info(0x0000, TAG, JSON.stringify(err));});return responseData; // 返回結果
}