系列文章目录
HarmonyOS应用开发之道:掌握未来,领略技术的魅力-01-ArkTS基础知识
HarmonyOS应用开发全攻略:从入门到精通-02-程序框架UIAbility、启动模式与路由跳转
HarmonyOS应用开发系列:探索未来的技术前沿-03-基础组件-让我们来码出复杂酷炫的UI
HarmonyOS应用开发指南:为未来做好准备-04-组件状态管理
HarmonyOS应用开发必修课-05-让我们的界面动起来!- 显示动画与属性动画
HarmonyOS应用开发探索之旅:探索未来的技术前沿-06-炫酷的转场动画
HarmonyOS应用开发之源-07-从加载分页列表探索鸿蒙网络数据技术
前言
中间隔了好久没有更新HarmonyOS系列的文章了,今天更新一节HTTP网络请求相关的Demo分析,然后我们可以利用手里的OpenApi设计我们自己的APP来对表Android、iOS以及跨平台技术开发的APP了,对比下实现效果、丝滑度以及我们实际开发中的真实感觉,相信你心中会有一个客观的打分。
本节Demo实现效果:
一、HTTP请求
HTTP数据请求功能主要由http模块提供,包括发起请求、中断请求、订阅/取消订阅HTTP Response Header 事件等。
在进行网络请求前,您需要在module.json5文件中申明网络访问权限。
{
"module" : {
"requestPermissions":[
{
"name": "ohos.permission.INTERNET"
}
]
}
}
1. 导入http模块。
import http from '@ohos.net.http';
2. 创建httpRequest对象。
使用createHttp()
创建一个httpRequest
对象,里面包括常用的一些网络请求方法,比如request
、destroy
、on('headerReceive')
等。
let httpRequest = http.createHttp();
需要注意的是每一个httpRequest对象对应一个http请求任务,不可复用。
3. 订阅请求头(可选)。
用于订阅http
响应头,此接口会比request
请求先返回,可以根据业务需要订阅此消息。
httpRequest.on('headersReceive', (header) => {
console.info('header: ' + JSON.stringify(header));
});
4. 发起http请求。
http
模块支持常用的POST
和GET
等方法,封装在RequestMethod
中。调用request
方法发起网络请求,需要传入两个参数
。第一个是请求的url地址
,第二个是可选参数,类型为HttpRequestOptions
,用于定义可选参数的类型和取值范围,包含请求方式、连接超时时间、请求头字段等。
5、GET请求
使用Get
请求,参数内容需要拼接到URL
中进行发送,如下示例中在url后面拼接了两个自定义参数,分别命名为param1和param2,值分别为value1和value2:
let url= "https://EXAMPLE_URL?param1=v1¶m2=v2";
let promise = httpRequest.request(
// 请求url地址
url,
{
// 请求方式
method: http.RequestMethod.GET,
// 可选,默认为60s
connectTimeout: 60000,
// 可选,默认为60s
readTimeout: 60000,
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
}
});
6、POST请求
POST
请求参数需要添加到extraData
里面,如下示例中在extraData里面定义添加了两个自定义参数param1和param2,值分别为value1和value2:
let url = "https://EXAMPLE_URL";
let promise = httpRequest.request(
// 请求url地址
url,
{
// 请求方式
method: http.RequestMethod.POST,
// 请求的额外数据。
extraData: {
"param1": "value1",
"param2": "value2",
},
// 可选,默认为60s
connectTimeout: 60000,
// 可选,默认为60s
readTimeout: 60000,
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
}
});
7、处理响应结果
data为网络请求返回的结果,err为请求异常时返回的结果。data的类型为HttpResponse。
promise.then((data) => {
if (data.responseCode === http.ResponseCode.OK) {
console.info('Result:' + data.result);
console.info('code:' + data.responseCode);
}
}).catch((err) => {
console.info('error:' + JSON.stringify(err));
});
- 其中
data.responseCode
为http请求返回的状态码,如果状态码为http.ResponseCode.OK(即200)
,则表示请求成功
,更多状态码可以在ResponseCode中查看。 data.result
为服务器返回的业务数据,开发者可以根据自身业务场景解析此数据。
二、Demo演示
1、效果图
1 | 2 | 3 | 4 |
---|---|---|---|
2、ArticleItem
代码
import { WanArticleBean } from '../common/bean/WanArticleBean'
import prompt from '@system.prompt';
import router from '@ohos.router';
import CommonConstants from '../common/constants/CommonConstants';
import Logger from '../common/utils/Logger';
@Component
export default struct ArticleItem {
private articleData: WanArticleBean;
private index: number;
showToast(message: string) {
prompt.showToast({
message: message
})
}
build() {
Column() {
Row() {
Text((this.articleData.tags != null && this.articleData.tags.length > 0) ? this.articleData.tags[0].name : '站内')
.fontSize('10fp')
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.border({ width: 1 })
.backgroundColor(Color.Red)
.borderColor(Color.Red)
.borderRadius(5)
.padding({ left: 3, right: 3, top: 1, bottom: 1 })
Blank()
Text('发布时间:' + this.articleData.niceDate)
.fontSize('10fp')
}
.width('100%')
Text(this.articleData.title)
.width('100%')
.fontSize('16fp')
.fontColor(Color.Black)
.maxLines(2)
.margin({ top: 8, bottom: 8 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('No.' + (this.index + 1) + ' ')
.fontColor(Color.Red)
.fontSize('14fp')
.fontWeight(FontWeight.Bold)
Text(this.articleData.superChapterName)
.fontColor(Color.Black)
.fontSize('14fp')
Text('·')
.fontColor(Color.Red)
.margin({ left: 3, right: 3 })
Text(this.articleData.chapterName)
.fontColor(Color.Black)
.fontSize('14fp')
Blank().layoutWeight(1)
Text('❤ ' + (this.articleData.author != null ? this.articleData.author : 'author'))
.fontColor(Color.Blue)
.fontSize('14fp')
}
.width('100%')
}
.onClick(() => {
this.showToast(this.articleData.title)
router.pushUrl({
url: CommonConstants.ARTICLE_DETAIL_PAGE,
params: {
// linkUrl: this.articleData.link
articleData: this.articleData
}
}).catch((error) => {
Logger.error('ArticlePage', 'Error:' + JSON.stringify(error))
})
})
.padding('10vp')
.width('100%')
.borderRadius('10vp')
.backgroundColor($r('app.color.start_window_background'))
}
}
3、ArticlePage
代码:
import { WanArticleBean } from '../common/bean/WanArticleBean';
import CommonConstants, { PageState, WanAndroidApiService } from '../common/constants/CommonConstants';
import WanArticleViewModel from '../viewmodel/WanArticleViewModel';
import promptAction from '@ohos.promptAction';
import { ArticlePageListBean } from '../common/bean/ArticlePageListBean';
import ArticleItem from './ArticleItem';
import { WanBannerBean } from '../common/bean/WanBannerBean';
import prompt from '@system.prompt';
import Logger from '../common/utils/Logger';
import router from '@ohos.router';
@Component
export default struct ArticlePage {
// 数据源
@State articleListData: Array<WanArticleBean> = [];
// 当前页码 ArticleList第一页起始为0
@State currentPage: number = CommonConstants.FIRST_PAGE;
// 每页请求数量
@State pageSize: number = CommonConstants.PAGE_SIZE;
// 请求状态
@State pageState: number = PageState.Loading;
// 是否还能加载更多
@State canLoadMore: boolean = true;
// 是否正在刷新
@State isRefreshing: boolean = false;
// 是否正在刷新2
@State isRefreshingFromButton: boolean = false;
// 是否开始加载下一页
@State isLoadMore: boolean = false;
// List滑动监听-需绑定列表或宫格组件
private scroller: Scroller = new Scroller();
// 是否显示悬浮Button,初始状态不显示
@State isShowFloatingButton: boolean = false;
// Swiper控制器
private swiperController: SwiperController = new SwiperController();
// Banner数据
private wanHomeBannerList: Array<WanBannerBean> = [];
// Banner跳转组装数据
private articleData: WanArticleBean = new WanArticleBean();
// 自定义方式:完全自定义转场过程的效果
@State scale1: number = 1
@State opacity1: number = 1
/**
* 自定义方式:完全自定义转场过程的效果
*/
pageTransition() {
// 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0% -- 100%)
PageTransitionEnter({ duration: 1200, curve: Curve.Linear })
.onEnter((type: RouteType, progress: number) => {
this.scale1 = 1
this.opacity1 = progress
})
// 退场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0% -- 100%)
PageTransitionExit({ duration: 1500, curve: Curve.Ease })
.onExit((type: RouteType, progress: number) => {
this.scale1 = 1 - progress
this.opacity1 = 1
})
}
/**
* 请求列表数据
*/
getWanArticleList() {
if (this.isRefreshing) {
// 刷新请偶第一页数据
// this.currentPage = 0;
this.currentPage = CommonConstants.TEST_FIRST_PAGE;
}
if (this.isRefreshingFromButton) {
// 刷新请偶第一页数据
// this.currentPage = 0;
this.currentPage = CommonConstants.TEST_FIRST_PAGE;
}
if (this.isLoadMore) {
// 加载下一页
this.currentPage++;
}
// 增加耗时
setTimeout(() => {
WanArticleViewModel.getWanArticleList(this.currentPage, this.pageSize, WanAndroidApiService.HOME_ARTICLE_LIST)
.then((data: ArticlePageListBean) => {
if (this.isRefreshing || this.currentPage === CommonConstants.TEST_FIRST_PAGE) {
// 赋值
this.articleListData = data.datas;
promptAction.showToast({ message: 'Loading ' + this.currentPage })
} else {
// 赋值
// this.articleListData = data.datas;
this.articleListData = this.articleListData.concat(data.datas);
promptAction.showToast({ message: 'Loading ' + this.currentPage })
}
// 如果是悬浮按钮返回第一页,请求成功后修改状态,且将List滑动到顶部
if (this.isRefreshingFromButton) {
this.isRefreshingFromButton = false;
this.scroller.scrollToIndex(0)
}
// 判断是否是最后一页
// 接口中有个字段over为true代表没有更多数据了
// 这里使用通用的方法,判断当前请求页返回数据数量是否小于每页数量
// 如果等于每页请求数量,则还有更多数据,否则当前页即为最后一页
if (data.datas.length === this.pageSize) {
this.canLoadMore = true;
} else {
this.canLoadMore = false;
}
// 请求成功状态更新
this.pageState = PageState.Success;
// 停止加载更多
this.isLoadMore = false;
// 停止刷新第一页数据
this.isRefreshing = false;
}).catch((error: string | Resource) => {
// 请求失败状态更新
this.pageState = PageState.Fail;
// 提示Toast
promptAction.showToast({
message: error,
duration: CommonConstants.ANIMATION_DURATION
});
})
}, 1000)
}
getWanHomeBannerList() {
WanArticleViewModel.getWanHomeBanner().then((data: WanBannerBean[]) => {
// 赋值
this.wanHomeBannerList = data;
}).catch((error: string | Resource) => {
// 请求失败状态更新
// 提示Toast
promptAction.showToast({
message: error,
duration: CommonConstants.ANIMATION_DURATION
});
})
}
aboutToAppear() {
// 请求列表数据
this.getWanArticleList()
// 请求Banner数据
//this.studentList2 = DataModel.getStudentList2();
this.getWanHomeBannerList();
}
@Builder LoadingComponent() {
Row() {
LoadingProgress().width(30).height(30)
Blank().width(10)
Text('Loading~').fontSize('16vp')
}
.margin('30vp')
.justifyContent(FlexAlign.Center)
}
@Builder ClickLoadMoreComponent() {
Text('---点击加载更多---')
.fontSize('16vp')
.margin('30vp')
.onClick(() => {
this.isLoadMore = true;
this.getWanArticleList()
})
}
@Builder LoadMoreAndEnd() {
Column() {
if (this.canLoadMore) {
if (this.isLoadMore) {
// 加载更多组件
this.LoadingComponent();
} else {
// 点击加载下一页
this.ClickLoadMoreComponent();
}
} else {
Text('---没有更多了---').fontSize('16vp').margin('30vp')
}
}
.width('100%')
}
@Builder ArticleListComponent() {
List({ space: 16, scroller: this.scroller }) {
// HomeBanner-Swiper可以不包含在ListItem中
this.HomeBanner(this.wanHomeBannerList)
ForEach(this.articleListData, (item: WanArticleBean, index: number) => {
ListItem() {
ArticleItem({ articleData: item, index: index })
}
.padding({ left: '16vp', right: 16 })
}, (item, index) => JSON.stringify(item) + index)
ListItem() {
this.LoadMoreAndEnd();
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.page_background'))
// .padding({ left: '16vp', right: 16 })
.divider({ strokeWidth: 0.1, color: Color.Red })
.onScrollStart(() => {
// List滑动时不显示悬浮按钮
this.isShowFloatingButton = false;
})
.onScrollStop(() => {
// List停止滑动时不显示悬浮按钮
this.isShowFloatingButton = true;
})
}
@Builder ReloadPageButton() {
Button() {
Image($r('app.media.ic_refresh'))
.rotate({ angle: 90 })
.width(30)
.height(30)
}
.backgroundColor(Color.Yellow)
.border({ width: 1 })
.borderColor(Color.Blue)
.borderStyle(BorderStyle.Dotted)
.width(35)
.height(35)
.position({ x: '80%', y: '70%' })
.shadow({ radius: 10 })
.onClick(() => {
this.pageState = PageState.Loading;
this.isRefreshing = true;
this.getWanArticleList();
})
}
@Builder LoadFirstPageButton() {
Button() {
Image($r('app.media.ic_back'))
.rotate({ angle: 90 })
.width(30)
.height(30)
}
.backgroundColor(Color.Red)
.border({ width: 1 })
.borderColor(Color.Blue)
.borderStyle(BorderStyle.Dotted)
.width(35)
.height(35)
.position({ x: '80%', y: '80%' })
.shadow({ radius: 10 })
.onClick(() => {
// if (this.currentPage > 0) {
if (this.currentPage > CommonConstants.TEST_FIRST_PAGE) {
// 不与Refresh组件联动
this.isRefreshingFromButton = true;
this.getWanArticleList();
} else {
promptAction.showToast({ message: '当前已经是第一页啦~' })
}
})
}
@Builder LoadNextPageButton() {
Button() {
Image($r('app.media.ic_back'))
.rotate({ angle: -90 })
.width(30)
.height(30)
}
.backgroundColor(Color.Red)
.border({ width: 1 })
.borderColor(Color.Yellow)
.borderStyle(BorderStyle.Dashed)
.width(35)
.height(35)
.position({ x: '80%', y: '90%' })
.shadow({ radius: 10 })
.onClick(() => {
if (this.canLoadMore === false) {
promptAction.showToast({ message: '没有更多数据啦~' })
} else {
this.isLoadMore = true;
this.getWanArticleList()
}
})
}
/**
* Banner
*/
@Builder HomeBanner(dataList: Array<WanBannerBean>) {
Swiper(this.swiperController) {
ForEach(dataList, (item: WanBannerBean) => {
RelativeContainer() {
Image(item.imagePath)
.borderRadius($r('app.float.home_swiper_borderRadius'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Fill)
.onClick(() => {
// 组装数据
this.articleData.link = item.url;
this.articleData.title = item.title;
this.articleData.author = '站内';
router.pushUrl({
url: CommonConstants.ARTICLE_DETAIL_PAGE,
params: {
articleData: this.articleData
}
}).catch((error) => {
Logger.error('ArticlePage', 'Error:' + JSON.stringify(error))
})
this.showToast(item.title)
})
.alignRules({
top: { anchor: "__container__", align: VerticalAlign.Top },
left: { anchor: "__container__", align: HorizontalAlign.Start },
// bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
// right: { anchor: "__container__", align: HorizontalAlign.End }
})
.id("image")
Text(item.title)
.width('100%')
.padding(10)
.fontColor(Color.White)
.backgroundColor($r('app.color.transparent_backgroundColor'))
.alignRules({
bottom: { anchor: "image", align: VerticalAlign.Bottom },
left: { anchor: "image", align: HorizontalAlign.Start }
})
.id("text")
}
}, (item: WanBannerBean) => JSON.stringify(item))
}
.borderRadius($r('app.float.home_swiper_borderRadius'))
.width('100%')
.height('25%')
.autoPlay(true)
.indicatorStyle({ mask: true, bottom: '10vp', right: '20vp', selectedColor: Color.Red })
.margin({ top: '5vp', bottom: '10vp' })
}
@Builder ArticleListLayout() {
// 可以进行页面下拉操作并显示刷新动效的容器组件
// 1、refreshing : boolean 必填参数 当前组件是否正在刷新。该参数支持$$双向绑定变量
// 2、offset : string | number 非必填参数 (下拉起点距离组件顶部的距离。默认值:16,单位vp,offset取值范围[0vp,64vp]。大于64vp按照64vp处理。不支持百分比,不支持负数)
// 3、friction number | string 非必填参数 下拉摩擦系数,取值范围为0到100。默认值:62
// - 0表示下拉刷新容器不跟随手势下拉而下拉。
// - 100表示下拉刷新容器紧紧跟随手势下拉而下拉。
// - 数值越大,下拉刷新容器跟随手势下拉的反应越灵敏。
Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100 }) {
this.ArticleListComponent()
}
.onStateChange((refreshStatus: RefreshStatus) => {
})
.onRefreshing(() => {
this.getWanArticleList();
})
// 注意:在List组件上添加了Text组件后,List组件无法拖动到底部
// 解决方法:
// 在List的父容器加上代码layoutWeight(1)。
// 原理:List属于可滚动容器组件,默认高度是占满全屏幕高度,当出现其他固定高度的组件占领了屏幕的部分高度时,
// 需要开发人员显性的指定List组件占满剩余高度,而不是全屏幕高度。.layoutWeight(1)
// .layoutWeight(1)
// 但是如果联动滚动的话,就不能添加.layoutWeight(1)
}
showToast(message: string) {
prompt.showToast({
message: message
})
}
build() {
Column(){
if (this.pageState === PageState.Loading) {
Column() {
LoadingProgress().width('100vp').width('100vp').color(Color.Red);
}
} else if (this.pageState === PageState.Fail) {
this.FailLayout(() => {
this.pageState = PageState.Loading;
this.getWanArticleList();
})
} else if (this.pageState === PageState.Success) {
Stack() {
Column() {
// 注意:在List组件上添加了Text组件后,List组件无法拖动到底部
// 解决方法:
// 在List的父容器加上代码layoutWeight(1)。
// 原理:List属于可滚动容器组件,默认高度是占满全屏幕高度,当出现其他固定高度的组件占领了屏幕的部分高度时,
// 需要开发人员显性的指定List组件占满剩余高度,而不是全屏幕高度。.layoutWeight(1)
this.ArticleListLayout()
}
.backgroundColor($r('app.color.page_background'))
if (this.isShowFloatingButton) {
this.ReloadPageButton()
this.LoadFirstPageButton()
this.LoadNextPageButton()
}
if (this.isRefreshing || this.isRefreshingFromButton) {
LoadingProgress()
.width(75)
.height(75)
.color(Color.Red)
}
}
}
}
.scale({ x: this.scale1 })
.opacity(this.opacity1)
}
@Builder FailLayout(onClick: () => void) {
Column() {
Image($r('app.media.ic_none'))
.height(CommonConstants.NONE_IMAGE_SIZE)
.width(CommonConstants.NONE_IMAGE_SIZE)
Text($r('app.string.page_none_msg'))
.opacity(CommonConstants.NONE_TEXT_opacity)
.fontSize(CommonConstants.NONE_TEXT_size)
.fontColor($r('app.color.fontColor_text3'))
.margin({ top: CommonConstants.NONE_TEXT_margin })
}
.onClick(onClick)
}
}
4、请求数据-WanArticleViewModel
:
import { ArticlePageListBean } from '../common/bean/ArticlePageListBean';
import ResponseBean from '../common/bean/ResponseBean';
import CommonConstants, { WanAndroidApiService } from '../common/constants/CommonConstants';
import { httpRequestByGet } from '../common/utils/HttpUtils';
import Logger from '../common/utils/Logger';
import { WanBannerBean } from '../common/bean/WanBannerBean';
class WanArticleViewModel {
// 首页文章 https://www.wanandroid.com/article/list/0/json?page_size=2
getWanArticleList(currentPage: number, pageSize: number, path: string): Promise<ArticlePageListBean> {
return new Promise(async (resolve: Function, reject: Function) => {
let url = `${WanAndroidApiService.WAN_ANDROID_SERVER}${path}${currentPage}`
url += '/json' + '?page_size=' + pageSize;
httpRequestByGet(url).then((data: ResponseBean) => {
Logger.debug('接口请求地址: HomeArticleList:' + url);
if (data.errorCode === CommonConstants.SERVER_CODE_SUCCESS) {
Logger.debug('getWanArticleList success', JSON.stringify(data));
resolve(data.data);
} else {
Logger.error('getWanArticleList failed', JSON.stringify(data));
reject($r('app.string.page_none_msg'));
}
}).catch((error: Error) => {
Logger.error('getWanArticleList failed', JSON.stringify(error));
reject($r('app.string.http_error_message'))
})
})
}
// 首页Banner-GET-https://www.wanandroid.com/banner/json
getWanHomeBanner(): Promise<WanBannerBean[]> {
return new Promise(async (resolve: Function, reject: Function) => {
let url = `${WanAndroidApiService.WAN_ANDROID_SERVER}${WanAndroidApiService.HOME_BANNER_LIST}`
httpRequestByGet(url).then((data: ResponseBean) => {
Logger.debug('接口请求地址: HomeBanner:' + url);
if (data.errorCode === CommonConstants.SERVER_CODE_SUCCESS) {
Logger.debug('getWanHomeBanner success', JSON.stringify(data));
resolve(data.data);
} else {
Logger.error('getWanHomeBanner failed', JSON.stringify(data));
reject($r('app.string.page_none_msg'));
}
}).catch((error: Error) => {
Logger.error('getWanHomeBanner failed', JSON.stringify(error));
reject($r('app.string.http_error_message'))
})
})
}
}
let wanArticleViewModel = new WanArticleViewModel();
export default wanArticleViewModel as WanArticleViewModel;
5、简单封装网络请求框架工具
import ResponseBean from '../bean/ResponseBean';
import http from '@ohos.net.http';
import CommonConstants, { ContentType } from '../constants/CommonConstants';
/**
* HTTP请求方法封装-GET
*/
export function httpRequestByGet(url: string): Promise<ResponseBean> {
let httpRequest = http.createHttp();
let responseResult = httpRequest.request(url, {
method: http.RequestMethod.GET,
readTimeout: CommonConstants.HTTP_READ_TIMEOUT,
header: {
'Content-Type': ContentType.JSON
},
connectTimeout: CommonConstants.HTTP_READ_TIMEOUT,
extraData: {}
});
let serverData: ResponseBean = new ResponseBean();
// 处理数据并返回
return responseResult.then((value: http.HttpResponse) => {
if (value.responseCode == CommonConstants.HTTP_CODE_200) {
let result = `${value.result}`;
let resultJson: ResponseBean = JSON.parse(result);
if (resultJson.errorCode === CommonConstants.SERVER_CODE_SUCCESS) {
serverData.data = resultJson.data;
}
serverData.errorCode = resultJson.errorCode;
serverData.errorMsg = resultJson.errorMsg;
} else {
serverData.errorMsg = `${$r('app.string.http_error_message')}&${value.responseCode}`;
}
return serverData;
}).catch(() => {
serverData.errorMsg = $r('app.string.http_error_message');
return serverData;
})
}
网络请求这块很简单,没什么需要特别强调的地方,需要注意的点都在代码注释里标注了,大家可以看下代码。
总结
本节介绍了使用HarmonyOS-httpRequest进行HTTP网络请求获取数据处理我们的业务逻辑绘制UI,学会本章节,我们就可以利用手里的OpenApi设计我们自己的APP来对表Android、iOS以及跨平台技术开发的APP了,对比下实现效果、丝滑度以及我们实际开发中的真实感觉,相信你心中会有一个客观的打分。