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包含的内容,其中的 s t r i n g : I n t e r n e t 和 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 ( ′ a p p . s t r i n g . N e t w o r k A v a i l a b l e ’ ) 、 r('app.string.Network_Available’)、 r(′app.string.NetworkAvailable’)、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();
@Builder
imageButton(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 ability
hilog.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 resources
hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
ConnectionUtils.closeRegister();
}
onForeground(): void {
// Ability has brought to foreground
hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.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; // 返回结果
}