鸿蒙NEXT开发 - 商城 (一)
前言
在本文中,我将通过开发一个简单的商城Demo,记录并分享鸿蒙NEXT开发的经验。本文中使用了以下关键技术点:Navigation、Tab、Swiper、List、Image、WebView、PhotoViewPicker、Toast。希望这篇文章能帮助你更好地理解和应用这些技术点。
开发环境搭建
- 安装DevEco Studio开发工具 鸿蒙官方文档
- 通过DevEco Studio 创建鸿蒙NEXT项目
- 学习一下ArkTS 和 声明式UI 语法
开发实战
官方有两种路由容器组件, 一种是Router, 另一种是Navigation。Router官方已经不推荐使用了,所以我们选择使用Navigation。
创建Navigation
使用DevEco Studio创建好project后, UI入口文件路径是src/main/ets/pages/Index.ets
, 在build()
函数下定义Navigation作为app的根容器。我们先import Main
和 WebView
两个页面, 这两个页面是我们自己创建的 , 后面再讲怎么创建。创建一个NavPathStack
,NavPathStack
可以简单理解为Navigation的控制器对象, 我们可以用它来进行页面跳转。PageMap(name: string)
方法是定义了页面的名字映射,根据不同的name渲染不同的页面。
importMain from'./main/Index';
importWebView from'./webView/Index';
@Entry
@Component
struct Index {
pathStack: NavPathStack = newNavPathStack(); // 定义NavPathStack
// 页面生命周期,会在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前)
aboutToAppear(): void {
// 将创建的NavPathStack设置到全局AppStorage中, 方便其他页面获取到这个pathStack对象
AppStorage.setOrCreate("PathStack", this.pathStack)
}
// 定义页面映射,根据不同的name渲染不同的页面
@Builder
PageMap(name: string) {
if(name === "WebView") {
WebView() // WebView页面
}
}
build() {
Navigation(this.pathStack) {
Main() // 子节点设为Main页面,默认显示Main页面
}
.mode(NavigationMode.Stack)
.navDestination(this.PageMap)
.hideTitleBar(true)
}
}
实现底部导航 Tab
现在我们在Main页面创建首页、购物车、我的,三个Tab。
importHome from'./home/Index';
importCart from'./cart/Index';
importMine from'./mine/Index';
@Component
exportdefault struct Index {
build() {
NavDestination() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Home()
}
.tabBar('首页')
TabContent() {
Cart()
}
.tabBar('购物车')
TabContent() {
Mine()
}
.tabBar('我的')
}
.width('100%')
.height('100%')
}
.title('Main')
.hideTitleBar(true)
}
}
首页:显示轮播图和商品列表
先定义和填充轮播图与商品列表的数据,然后展示出来。
@State swiperData: SwiperItemInterface[] = []; // 轮播图数据
@State productListData: ProductInterface[] = []; // 商品列表数据数据
aboutToAppear(): void {
// 填充轮播图假数据
this.swiperData = [
{ id: 1, image: 'https://gips3.baidu.com/it/u=1022347589,1106887837&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.huawei.com' },
{ id: 2, image: 'https://gips3.baidu.com/it/u=764883555,2569275522&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.qq.com' },
{ id: 3, image: 'https://gips1.baidu.com/it/u=1024042145,2716310167&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560', link: 'https://www.harmonyos.com/' },
];
// 填充商品列表假数据
constproductListData: ProductInterface[] = [];
for(letindex = 0; index < 100; index++) {
constproduct: ProductInterface = {
id: index + 1,
title: `冰镇麒麟西瓜单粒约5.5kg ${index + 1}`,
desc: '单果4.5-5.5kg,不足5.5kg退差价。横切面籽粒连片状黄点,正常现象,不影响食用',
image: 'https://inews.gtimg.com/newsapp_bt/0/14993002202/1000',
price: 99.99
};
productListData.push(product)
}
this.productListData = productListData;
}
然后在build()
函数下编写UI代码,这里使用List
组件,用ListItemGroup
展示Header,这里的Header就是轮播图,用ForEach
遍历商品数据,将ListItem
添加到List
组件内。我直接贴一下完整代码
import{ ProductInterface, SwiperItemInterface } from'ets/interfaces/Index'
import{ promptAction } from'@kit.ArkUI';
@Component
exportdefault struct Index {
pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack // 全局的 NavPathStack
@State swiperData: SwiperItemInterface[] = []; // 轮播图数据
@State productListData: ProductInterface[] = []; // 商品列表数据数据
aboutToAppear(): void {
// 填充轮播图假数据
this.swiperData = [
{ id: 1, image: 'https://gips3.baidu.com/it/u=1022347589,1106887837&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.huawei.com' },
{ id: 2, image: 'https://gips3.baidu.com/it/u=764883555,2569275522&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280', link: 'https://www.qq.com' },
{ id: 3, image: 'https://gips1.baidu.com/it/u=1024042145,2716310167&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560', link: 'https://www.harmonyos.com/' },
];
// 填充商品列表假数据
constproductListData: ProductInterface[] = [];
for(letindex = 0; index < 100; index++) {
constproduct: ProductInterface = {
id: index + 1,
title: `冰镇麒麟西瓜单粒约5.5kg ${index + 1}`,
desc: '单果4.5-5.5kg,不足5.5kg退差价。横切面籽粒连片状黄点,正常现象,不影响食用',
image: 'https://inews.gtimg.com/newsapp_bt/0/14993002202/1000',
price: 99.99
};
productListData.push(product)
}
this.productListData = productListData;
}
addProductToCart(product: ProductInterface) {
constcartData: ProductInterface[] = AppStorage.get('CartData') asProductInterface[] || [];
cartData.push(product);
AppStorage.setOrCreate('CartData', cartData);
promptAction.showToast({ message: '加入购物车成功' });
}
@Builder header() {
// 列表头部组件
Swiper() {
// 遍历swiperData, 将子节点填充至Swiper
ForEach(this.swiperData, (item: SwiperItemInterface) => {
Image(item.image)
.width('100%')
.height('100%')
.onClick(() => {
constparams: Record<string, string> = { 'url': item.link };
this.pathStack.pushPathByName('WebView', params);
})
}, (item: SwiperItemInterface) => String(item.id)) // 设置循环的key
}
.width('100%')
.height('35%')
.loop(true)
.autoPlay(true)
}
build() {
List() {
ListItemGroup({
header: this.header
})
// 遍历productListData, 将子节点填充至List
ForEach(this.productListData, (product: ProductInterface) => {
ListItem() {
Column() {
Image(product.image)
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
.margin({ bottom: 10 })
Column() {
Text(product.title)
.fontSize(14)
.fontWeight('bold')
.maxLines(2)
.margin({ bottom: 5 })
Text(product.desc)
.fontSize(10)
.maxLines(2)
.margin({ bottom: 5 })
Row() {
Text(`¥ ${product.price}`)
.fontSize(14)
.fontColor('#FF6331')
.layoutWeight(1)
Image($r('app.media.icon_add_to_cart'))
.width(28)
.height(28)
.onClick(() => this.addProductToCart(product))
}
.alignItems(VerticalAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.padding({ left: 5, right: 5 })
}
.margin(5)
.clip(true)
.padding({ bottom: 10 })
.backgroundColor('#fff')
.borderRadius(8)
}
}, (product: ProductInterface) => String(product.id)) // 设置循环的key
}
.layoutWeight(1)
.lanes(2)
.width('100%')
.height('100%')
.backgroundColor('#F5F6F7')
}
}
下面我们来看看效果
点击轮播图跳转至WebView页面
我们给Swiper的Image组件添加一个onClick
事件,点击图片打开WebView并展示相应的网页。
我们之前在AppStorage
存了一个PathStack
对象,现在把它取出来,用它来做页面跳转
pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack // 全局的 NavPathStack
使用 this.pathStack.pushPathByName('WebView', params)
跳转至WebView页面
@Builder header() {
// 列表头部组件
Swiper() {
// 遍历swiperData, 将子节点填充至Swiper
ForEach(this.swiperData, (item: SwiperItemInterface) => {
Image(item.image)
.width('100%')
.height('100%')
.onClick(() => {
// 跳转至WebView页面,并传入url链接
constparams: Record<string, string> = { 'url': item.link };
this.pathStack.pushPathByName('WebView', params);
})
}, (item: SwiperItemInterface) => String(item.id)) // 设置循环的key
}
.width('100%')
.height('35%')
.loop(true)
.autoPlay(true)
}
接下来我们来看看WebView页面的代码, 先定义一个WebviewController
, 在aboutToAppear
生命周期获取上一个页面传入的参数。这里的 this.pathStack.getParamByName('WebView')
获取的是路由栈内所有WebView
页面的参数,比如现在已打开了一个WebView页面展示华为官网, 现在要再打开一个WebView页面展示腾讯官网,这时 this.pathStack.getParamByName('WebView')
返回的参数url同时包含华为官网和腾讯官网,需要自己取数组最后一个url, 也就是最近的传入的url参数。我觉得这样的API设计挺不合理的,而且也不太健壮,希望后续官方能优化。
import{ webview } from'@kit.ArkWeb';
@Component
exportdefault struct Index {
pathStack: NavPathStack = AppStorage.get("PathStack") asNavPathStack
controller: webview.WebviewController = newwebview.WebviewController();
url: string = '';
aboutToAppear(): void {
constallParams: Record<string, string>[] = this.pathStack.getParamByName('WebView') asRecord<string, string>[];
this.url = allParams[allParams.length - 1]?.url;
console.log('url is', this.url)
}
build() {
NavDestination() {
Web({ src: this.url, controller: this.controller })
.width('100%')
.height('100%')
}
}
}
加入商品到购物车,并弹出加入成功Toast
点击购物车Icon,添加商品到购物车并弹出成功提示。
import{ promptAction } from'@kit.ArkUI';
....
addProductToCart(product: ProductInterface) {
constcartData: ProductInterface[] = AppStorage.get('CartData') asProductInterface[] || [];
cartData.push(product);
AppStorage.setOrCreate('CartData', cartData);
promptAction.showToast({ message: '加入购物车成功' });
}
选用户头像并展示
使用PhotoViewPicker选择用户头像并展示。
import{ photoAccessHelper } from'@kit.MediaLibraryKit';
@Component
exportdefault struct Index {
@State avatar: string = 'https://q0.itc.cn/q_70/images01/20240125/3dc71da769524488be1c3e095c0e6e85.png'
pickAvatar() {
constphotoSelectOptions = newphotoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
constphotoViewPicker = newphotoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
consturis = photoSelectResult.photoUris;
this.avatar = uris?.[0];
})
}
build() {
Column() {
Image(this.avatar)
.width(60)
.height(60)
.borderRadius(30)
.margin({ bottom: 10 })
.onClick(() => this.pickAvatar())
Text('用户名: 鸿蒙开发者')
.fontSize(12)
.fontWeight('bold')
}
}
}
总结
本文不仅展示了鸿蒙NEXT的基本开发过程,还分享了关键技术的应用方法。希望本文对你的开发有所帮助。未来我将继续完善这个商城Demo,并分享更多开发经验。