鸿蒙ArkTS实战项目:Github掌盟项目开发全解析

鸿蒙ArkTS实战项目:Github掌盟项目开发全解析

一、引言

在鸿蒙系统开发的浪潮中,ArkTS语言凭借其高效、便捷的特性,成为了开发者们的热门选择。本文将详细介绍一个基于华为鸿蒙系统(HarmonyOS)使用ArkTS语言开发的实战项目——掌盟项目。该项目包含丰富的功能和实用的开发技巧,适合有一定ArkTS基础的开发者学习参考。

二、项目概述

本项目主要展示了一个基于鸿蒙系统的应用开发实战案例。项目中展示的数据皆为抓包获取,仅用于项目练手,无商业行为。以下是项目的整体截图:

三、项目结构分析

3.1 项目源码结构

鸿蒙ArkTS实战项目:Github掌盟项目开发全解析_第1张图片

3.1.1 common目录

该目录存放通用代码,包括网络请求 networkingwebview 相关代码。网络请求部分在后续会详细介绍,而 webview 则用于加载网页内容。

3.1.2 entryability目录

EntryAbility.ets 可以设置项目入口。例如,将原入口 pages/index 改为自定义入口 pages/Tabbar/TabsPage,代码如下:

windowStage.loadContent('pages/Tabbar/TabsPage', (err) => {
  if (err.code) {
    hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
    return;
  }
  hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
3.1.3 Images目录

这是自定义的存放项目本地图片的目录。引用方式如下:

Image('Images/home/[email protected]')

也可以将图片放在系统图片目录 src/main/resources/base/media 下,引用方式为:

Image($r('app.media.home_search'))
3.1.4 pages目录

该目录存放项目页面代码,其中 homemallmine 是三个 tabbar 的控制器。tabbar 下的 TabsPage.ets 是项目自定义的入口。项目默认的入口页面是 Index.ets,即入口为 pages/index,如果要使用华为模拟器运行项目,需要在 EntryAbility.ets 中修改入口配置为 pages/tabbar/TabsPage

3.2 项目配置结构

3.2.1 entry/src/main/resources/base/element/string.json

可以在该文件中设置项目名称(包括中文名和英文名),示例如下:

{
  "string": [
    {
      "name": "module_desc",
      "value": "模块描述"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "掌上英雄联盟"
    }
  ]
}
3.2.2 entry/src/main/resources/base/media

该目录可以设置项目图标、启动图等资源。

四、UI页面设计

4.1 tabbar/TabsPage.ets

TabsPage 页面一般作为项目的启动页面,使用 Tabs 控件,里面只可以使用子组件 TabContent,并且子组件 TabContent 可以自定义每个 tabBar,即 TabBuilder。以下是部分源码:

import {QQHomeController} from "../home/Controller/QQHomeController"
import {QQMallController} from  "../mall/Controller/QQMallController"
import {QQMineController} from "../mine/Controller/QQMineController"

@Entry
@Component
struct TabsPage {
  @State currentIndex: number = 0;
  private tabsController: TabsController = new TabsController()

  @Builder TabBuilder(title:string, targetIndex:number, normalImg:string, selectedImg:string) {
    Column() {
      Image(this.currentIndex == targetIndex ? selectedImg : normalImg)
        .width(24)
        .height(24)
      Text(title)
        .fontSize(10)
        .margin({top:5})
        .fontColor(this.currentIndex == targetIndex ? '#0F1114' : '#565D66')
    }
    .backgroundColor('#ffffff')
    .width('100%')
    .height(60)
    .justifyContent(FlexAlign.Center)
    .onClick(()=>{
      this.currentIndex = targetIndex
      this.tabsController.changeIndex(this.currentIndex)
    })
  }

  build() {
    RelativeContainer() {
      Column() {
        Tabs({barPosition:BarPosition.End, controller:this.tabsController, index:0}) {
          TabContent() {
            QQHomeController()
          }.tabBar(this.TabBuilder('首页', 0, 'Images/tabbar/tabbar_home_normal.png','Images/tabbar/tabbar_home_select.png'))
          TabContent() {
            QQMallController()
          }.tabBar(this.TabBuilder('商城', 1, 'Images/tabbar/tabbar_mall_normal.png','Images/tabbar/tabbar_mall_select.png'))
          TabContent() {
            QQMineController()
          }.tabBar(this.TabBuilder('我的', 2, 'Images/tabbar/tabbar_mine_normal.png','Images/tabbar/tabbar_mine_select.png'))
        }
      }
    }
    .height('100%')
    .width('100%')
  }
}

4.2 QQHomeController

QQHomeController 作为首页,使用了一些常用的控件,以下是部分源码解读:

4.2.1 TabSegmentPage

TabSegmentPage 是自定义的 Segment 控制器,使用 Tabs 控件实现不同位置的展示效果。以下是部分源码:

import { QQChannelModel } from '../../Model/QQChannelModel'

@Entry
@Component
export struct TabSegmentPage {
  @State itemWidth: number = 50
  @State currentIndex: number = 0
  @State channelArray:Array<QQChannelModel> = Array<QQChannelModel>()

  @Builder TabBuilder(index: number, name: string) {
    Column() {
      Text(name)
        .fontColor(this.currentIndex === index ? '#161616' : '#868c8d')
        .fontSize(this.currentIndex === index ? 18 : 16)
        .fontWeight(this.currentIndex === index ? 500 : 400)
        .lineHeight(18)
        .margin({top: 10, bottom:5})
      Divider()
        .width(16)
        .strokeWidth(2)
        .color('#202020')
        .opacity(this.currentIndex === index ? 1 : 0)
    }
    .width(this.itemWidth)
    .height('100%')
  }

  build() {
    RelativeContainer() {
      Column() {
        Tabs({ barPosition: BarPosition.Start}) {
          if (this.channelArray.length > 0) {
            ForEach(this.channelArray, (item:QQChannelModel, index) => {
              TabContent() {

              }
              .tabBar(this.TabBuilder(index, item.name ?? ''))
              .margin(0)
            })
          }
        }
        .height('100%')
        .barWidth(this.itemWidth*this.channelArray.length)
        .align(Alignment.Start)
        .vertical(false)
        .scrollable(false)
        .barPosition(BarPosition.Start)
        .barMode(BarMode.Fixed)
        .animationDuration(300)
        .onChange((index:number) => {
          this.currentIndex = index
          console.log('TabSegmentPage index = ',index)
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}
4.2.2 Swiper轮播器

使用 Swiper 实现轮播效果,代码如下:

Swiper() {
  ForEach(this.bannerListArray, (item:QQBannerBody, index) => {
    Image(item.imgUrl)
      .onClick(() => {
        router.pushUrl({
          url:'common/webview/QQWebviewController',
          params:{
            url:item.intent
          }
        })
      })
  })
}
.width('100%')
.height(150)
.loop(true)
.autoPlay(true)
.interval(3000)
.indicator(Indicator.dot()
  .itemWidth(10)
  .itemHeight(2)
  .selectedItemWidth(15)
  .selectedItemHeight(2)
  .color('#918c8e')
  .selectedColor(Color.White))
4.2.3 Grid和GridItem实现金刚区效果
Grid() {
  ForEach(this.iconListArray, (item:QQIconBody, index) => {
    GridItem() {
      Column({space:10}) {
        Image(item.iconUrl)
          .width(55)
          .width(55)
        Text(item.name)
          .fontSize(10)
          .fontColor('#939999')
      }
      .onClick(() => {
        router.pushUrl({
          url:'common/webview/QQWebviewController',
          params:{
            url:item.intent
          }
        })
      })
    }
    .width('20%')
    .height(80)
  })
}
.width('100%')
.backgroundColor(Color.White)

五、网络请求

QQNetworkRequest.ets 中通过 http 封装 postget 请求,以下是部分源码:

import http from '@ohos.net.http'
import { JSON } from '@kit.ArkTS';

let header:Record<string, string> = {
  'Cookie': 'tgw_l7_route=0f7eb4ab2f1d32df0ff24f07ba0cf8db; clientType=10; accountType=255',
  'qimei': '4f1c8ff7-4677-4c0a-9ac3-831c9dd865df',
  'accept': '*/*',
  'accept-encoding': 'gzip, deflate, br',
  'Content-Type': 'application/json',
  'user-agent': 'QTL/9.2.5 (iPhone; IOS 18.0; Scale/3.00)',
  'connection': 'keep-alive',
  'gh-header': '1-2-105-925-0',
  'subchannel': '1',
  'accept-language': 'zh-Hans-CN;q=1, zh-Hant-MO;q=0.9, en-CN;q=0.8',
};

// post
export function postRequest(url:string, param:Object, success:(str:string)=>void, fail:(error:Error)=>void) {
  let httpRequest = http.createHttp()
  let reponseResult = httpRequest.request(url, {
    method: http.RequestMethod.POST,
    readTimeout:60000,
    connectTimeout:60000,
    header: header,
    extraData: param,
    expectDataType:http.HttpDataType.STRING
  }, (error, data) => {
    if (!error) {
      success(data.result.toString())
      console.info('Networking ====================================');
      console.info('Networking Url:' + url);
      console.info('Networking Result 类型:' + typeof data.result);
      console.info('Networking Result:' + data.result);
      console.info('Networking code:' + data.responseCode);
      console.info('Networking header:' + JSON.stringify(data.header));
      console.info('Networking cookies:' + data.cookies); 
      console.info('Networking ====================================');
    } else {
      fail(error)
      console.info('Networking error:' + JSON.stringify(error));
      httpRequest.destroy();
    }
  })
}

// get
export function getRequest(url:string, param:Object, success:(str:string)=>void, fail:(error:Error)=>void) {
  let httpRequest = http.createHttp()
  let reponseResult = httpRequest.request(url, {
    method: http.RequestMethod.GET,
    readTimeout:60000,
    connectTimeout:60000,
    header: header,
    extraData: param,
    expectDataType:http.HttpDataType.STRING
  }, (error, data) => {
    if (!error) {
      success(data.result.toString())
      console.info('Networking ====================================');
      console.info('Networking Url:' + url);
      console.info('Networking Result 类型:' + typeof data.result);
      console.info('Networking Result:' + data.result);
      console.info('Networking code:' + data.responseCode);
      console.info('Networking header:' + JSON.stringify(data.header));
      console.info('Networking cookies:' + data.cookies); 
      console.info('Networking ====================================');
    } else {
      fail(error)
      console.info('Networking error:' + JSON.stringify(error));
      httpRequest.destroy();
    }
  })
}

六、数据封装

对于网络请求获取的 json 数据,可以通过方法 JSON.parse(jsonStr) 来转换成对应的数据模型进行使用。以下是创建对应的数据模型类 QQBannerModel.ets 的部分代码:

export class QQBannerModel {
  code?: number
  data?: QQBannerData
  msg?: string
  result?: number
}

export class QQBannerData {
  result?: number
  next?: string
  feedsInfo?: Array<QQBannerFeedsInfo>
  attach?: object
  scope?: string
  distance?: number
}

export class QQBannerFeedsInfo {
  feedBase?: QQBannerFeedBase
  feedNews?: QQBannerFeedNews
}

export class QQBannerFeedNews {
  body?: Array<QQBannerBody>
}

export class QQBannerBody {
  enableWholeBannerClick?: boolean
  isAmsAd?: boolean
  imgUrl?: string
  bigImgUrl?: string
  intent?: string
  title?: string
  taskName?: string
  contentId?: string
  isVideo?: boolean
  vid?: string
  reservedDesc?: string
  packageName?: string
  universalLink?: string
  algorithmInfo?: QQBannerAlgorithmInfo
  extend?: QQBannerExtend
  commonInfo?: QQBannerCommonInfo
}

export class QQBannerCommonInfo {
  cover?: string
  type?: string
}

export class QQBannerExtend {
  roomid?: string
}

export class QQBannerAlgorithmInfo {
  adid?: string
  actionID?: string
  fname?: string
  bannerId?: string
  ecode?: string
  from?: string
  clickEventId?: string
  expoEventId?: string
  gamecode?: string
  url?: string
}

export class QQBannerFeedBase {
  layoutType?: string
  contentType?: string
  contentId?: string
  intent?: string
  position?: number
  priority?: number
}

在网络请求获取 json 的方法中,可以写入以下代码获取具体数据进行渲染:

let model:QQBannerModel = JSON.parse(str)

网络请求示例:

// 请求banner数据
loadHomeRequestBannerData() {
  this.viewModel.loadHomeRequestBannerData((str) => {
    console.log('home Banner 数据',str)
    let model:QQBannerModel = JSON.parse(str)
    // 刷新数据
    let dataModel:QQBannerData = model.data as QQBannerData
    let feedsInfoArray = dataModel.feedsInfo as Array<QQBannerFeedsInfo>
    if (feedsInfoArray.length > 0) {
      let feedsInfo = feedsInfoArray[0]
      let feedNews = feedsInfo.feedNews
      this.bannerListArray = feedNews?.body as Array<QQBannerBody>
    }
    console.log('home Banner 数据 .data ',JSON.parse(str))
  }, (error) => {
    console.log('home Banner 数据 error ',error)
  })
}

七、路由跳转

路由跳转可以实现一个页面跳转到另一个页面,并可以携带参数。例如,首页 QQHomeController 跳转 webview 页面 QQWebviewController,并携带参数 url

router.pushUrl({
  url:'common/webview/QQWebviewController',
  params:{
    url:'https://www.baidu.com'
  }
})

QQWebviewController 中返回按钮方法中写入路由返回方法:

router.back()

对于跳转携带的参数 url,可以在 QQWebviewController 中的生命周期方法中接收:

aboutToAppear(): void {
  const param = router.getParams() as Map<string, string>
  this.url = param['url']
}

八、华为模拟器的使用

使用模拟器需要提前进行申请并下载,具体的模拟器申请可以参考:华为模拟器申请。模拟器使用中遇见问题可以参考:鸿蒙系统HarmonyOS-ArkTS项目开发问题汇总。

项目实现效果参考:华为模拟器录屏。项目源码:HMApp_ArkTS。

九、总结

通过本文的介绍,我们详细了解了这个鸿蒙ArkTS实战项目的结构、UI设计、网络请求、数据封装、路由跳转等方面的内容。希望本文能为鸿蒙开发者提供一些有用的参考,帮助大家更好地掌握ArkTS语言和鸿蒙系统开发。

以上就是关于该项目的详细介绍,如果你在开发过程中遇到任何问题,欢迎在评论区留言讨论。

你可能感兴趣的:(鸿蒙ArkTS实战项目:Github掌盟项目开发全解析)