HarmonyOS开发:完整电商应用实战
HarmonyOS开发:完整电商应用实战
📌 核心要点:从架构设计到功能实现,首页、详情、购物车、订单、支付全链路打通,模块化架构+状态管理+数据层是电商App的三大支柱。
背景与动机
前面9篇文章,我们把电商App的每个模块拆开讲了——首页、详情、购物车、订单、支付、社交、IM、个人中心、搜索。每个模块都能跑,但拼到一起呢?
你把10个模块的代码往一个项目里一扔,发现:购物车的数据怎么传给订单页?支付结果怎么通知订单列表?用户登录状态怎么全局共享?首页的商品点击怎么跳到详情页?详情页的加购怎么更新购物车角标?
单模块能跑≠整个App能跑。模块之间的数据流转、状态同步、页面导航,才是电商App真正的难点。
这篇文章把所有模块串起来,从架构设计到数据管理到页面导航,给你一个能跑的完整电商App。
核心原理
完整电商App的核心是模块化架构+全局状态管理+统一数据层。每个模块独立开发,通过全局状态和数据层通信。
flowchart TD
A[App入口] --> B[全局状态管理 AppStore]
A --> C[数据层 Repository]
A --> D[路由管理 Router]
B --> B1[用户状态]
B --> B2[购物车状态]
B --> B3[订单状态]
B --> B4[搜索状态]
C --> C1[用户Repository]
C --> C2[商品Repository]
C --> C3[购物车Repository]
C --> C4[订单Repository]
D --> E[首页]
D --> F[详情页]
D --> G[购物车]
D --> H[订单页]
D --> I[支付页]
D --> J[个人中心]
D --> K[搜索页]
E --> C2
F --> C2
G --> C3
H --> C4
I --> C4
B2 --> G
B2 --> E
classDef core fill:#1565C0,color:#fff,stroke:#0D47A1
classDef state fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef data fill:#E65100,color:#fff,stroke:#BF360C
classDef page fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,C,D,core
class B1,B2,B3,B4,state
class C1,C2,C3,C4,data
class E,F,G,H,I,J,K,page
架构分层
| 层级 | 职责 | 示例 |
|---|---|---|
| 展示层 | UI渲染、用户交互 | 页面组件、Builder |
| 状态层 | 全局状态管理、状态同步 | AppStore、ViewModel |
| 数据层 | 数据获取、缓存、持久化 | Repository、Service |
| 网络层 | HTTP请求、WebSocket | HttpClient、WSManager |
数据流转规则
- 页面→状态层:用户操作触发状态更新
- 状态层→数据层:状态变更需要持久化时调用数据层
- 数据层→网络层:需要服务端数据时发起网络请求
- 网络层→数据层→状态层→页面:数据回流,更新UI
严禁跨层调用。页面不能直接调网络层,网络层不能直接更新UI。
代码实战
基础用法:全局状态管理
电商App有大量全局状态:用户登录状态、购物车数量、未读消息数。这些状态需要在多个页面间共享。
// AppStore.ets — 全局状态管理
// 用户状态
interface UserState {
isLogin: boolean
userId: string
nickname: string
avatar: string
phone: string
}
// 购物车状态
interface CartState {
totalCount: number // 购物车商品总数
selectedCount: number // 选中商品数
totalPrice: number // 选中商品总价
}
// 应用全局状态
interface AppState {
user: UserState
cart: CartState
unreadMessageCount: number
currentTab: number // 当前Tab索引
}
// 全局状态管理器
class AppStore {
private static instance: AppStore
// 全局状态
private state: AppState = {
user: {
isLogin: false,
userId: '',
nickname: '',
avatar: '',
phone: ''
},
cart: {
totalCount: 0,
selectedCount: 0,
totalPrice: 0
},
unreadMessageCount: 0,
currentTab: 0
}
// 状态变更监听器
private listeners: Array<() => void> = []
private constructor() {}
static getInstance(): AppStore {
if (!AppStore.instance) {
AppStore.instance = new AppStore()
}
return AppStore.instance
}
// ========== 用户状态 ==========
login(userId: string, nickname: string, avatar: string, phone: string): void {
this.state.user = { isLogin: true, userId, nickname, avatar, phone }
this.notifyListeners()
}
logout(): void {
this.state.user = { isLogin: false, userId: '', nickname: '', avatar: '', phone: '' }
this.state.cart = { totalCount: 0, selectedCount: 0, totalPrice: 0 }
this.notifyListeners()
}
getUser(): UserState {
return this.state.user
}
isLogin(): boolean {
return this.state.user.isLogin
}
// ========== 购物车状态 ==========
updateCartState(totalCount: number, selectedCount: number, totalPrice: number): void {
this.state.cart = { totalCount, selectedCount, totalPrice }
this.notifyListeners()
}
getCartCount(): number {
return this.state.cart.totalCount
}
getCartState(): CartState {
return { ...this.state.cart }
}
// ========== 消息状态 ==========
setUnreadCount(count: number): void {
this.state.unreadMessageCount = count
this.notifyListeners()
}
getUnreadCount(): number {
return this.state.unreadMessageCount
}
// ========== Tab状态 ==========
setCurrentTab(index: number): void {
this.state.currentTab = index
}
getCurrentTab(): number {
return this.state.currentTab
}
// ========== 监听器 ==========
addListener(listener: () => void): void {
this.listeners.push(listener)
}
removeListener(listener: () => void): void {
const idx = this.listeners.indexOf(listener)
if (idx >= 0) this.listeners.splice(idx, 1)
}
private notifyListeners(): void {
this.listeners.forEach(l => l())
}
// 获取完整状态
getState(): AppState {
return { ...this.state }
}
}
进阶用法:数据层与Repository
数据层封装了所有数据获取逻辑,页面不需要关心数据来自网络还是本地缓存。
// Repository.ets — 数据层
// ========== 商品Repository ==========
class ProductRepository {
private static instance: ProductRepository
private cache: Map<string, { data: ProductDetail; expireTime: number }> = new Map()
private cacheDuration: number = 5 * 60 * 1000 // 缓存5分钟
private constructor() {}
static getInstance(): ProductRepository {
if (!ProductRepository.instance) {
ProductRepository.instance = new ProductRepository()
}
return ProductRepository.instance
}
// 获取商品详情(先查缓存,再请求网络)
async getProductDetail(productId: string): Promise<ProductDetail | null> {
// 1. 查缓存
const cached = this.cache.get(productId)
if (cached && cached.expireTime > Date.now()) {
console.info(`[ProductRepo] 命中缓存: ${productId}`)
return cached.data
}
// 2. 请求网络
try {
// 实际项目:const response = await http.get(`/api/product/${productId}`)
const data = this.getMockProductDetail(productId)
// 3. 写入缓存
this.cache.set(productId, {
data: data,
expireTime: Date.now() + this.cacheDuration
})
return data
} catch (error) {
console.error(`[ProductRepo] 获取商品详情失败: ${JSON.stringify(error)}`)
return null
}
}
// 搜索商品
async searchProducts(
keyword: string,
page: number,
size: number,
filter?: SearchFilter
): Promise<{ items: SearchResultItem[]; total: number }> {
try {
// 实际项目:const response = await http.post('/api/search', { keyword, page, size, filter })
return {
items: this.getMockSearchResults(keyword, page, size),
total: 100
}
} catch (error) {
return { items: [], total: 0 }
}
}
// 获取首页推荐商品
async getHomeProducts(page: number, size: number): Promise<ProductItem[]> {
// 实际项目:const response = await http.get(`/api/home/products?page=${page}&size=${size}`)
return this.getMockHomeProducts(page, size)
}
// 清除缓存
clearCache(): void {
this.cache.clear()
}
// 模拟数据
private getMockProductDetail(productId: string): ProductDetail {
return {
id: productId,
title: '精选商品详情展示',
subtitle: '高品质好物推荐',
images: [
{ id: '1', url: `https://picsum.photos/750/750?random=${productId}1`, width: 750, height: 750, isVideo: false },
{ id: '2', url: `https://picsum.photos/750/750?random=${productId}2`, width: 750, height: 750, isVideo: false },
],
specGroups: [
{ id: '1', name: '颜色', options: [
{ id: '1', name: '黑色', available: true, selected: false },
{ id: '2', name: '白色', available: true, selected: false },
]},
{ id: '2', name: '尺码', options: [
{ id: '3', name: 'M', available: true, selected: false },
{ id: '4', name: 'L', available: true, selected: false },
]},
],
skuList: [
{ id: 'sku1', specCombination: { '颜色': '黑色', '尺码': 'M' }, price: 299, originalPrice: 399, stock: 10, imageUrl: '' },
{ id: 'sku2', specCombination: { '颜色': '黑色', '尺码': 'L' }, price: 319, originalPrice: 419, stock: 5, imageUrl: '' },
{ id: 'sku3', specCombination: { '颜色': '白色', '尺码': 'M' }, price: 299, originalPrice: 399, stock: 8, imageUrl: '' },
{ id: 'sku4', specCombination: { '颜色': '白色', '尺码': 'L' }, price: 319, originalPrice: 419, stock: 0, imageUrl: '' },
],
description: '',
specs: [{ name: '材质', value: '纯棉' }, { name: '风格', value: '休闲' }],
rating: 4.8,
reviewCount: 2365,
reviews: [],
shopName: '品牌旗舰店',
shopId: 'shop_001'
}
}
private getMockSearchResults(keyword: string, page: number, size: number): SearchResultItem[] {
return Array.from({ length: size }, (_, i) => ({
id: `search_${page}_${i}`,
title: `${keyword}相关商品 ${page * size + i + 1}`,
price: Math.round(Math.random() * 500 + 10),
originalPrice: Math.round(Math.random() * 800 + 100),
coverUrl: `https://picsum.photos/200/200?random=${page * size + i}`,
sales: Math.round(Math.random() * 2000),
brand: '品牌',
category: '分类',
tags: []
}))
}
private getMockHomeProducts(page: number, size: number): ProductItem[] {
return Array.from({ length: size }, (_, i) => ({
id: `home_${page}_${i}`,
title: `精选好物推荐 ${page * size + i + 1}`,
price: Math.round(Math.random() * 500 + 10),
originalPrice: Math.round(Math.random() * 800 + 100),
coverUrl: `https://picsum.photos/300/${200 + Math.round(Math.random() * 100)}?random=${page * size + i}`,
sales: Math.round(Math.random() * 2000),
tags: [],
imageHeight: 150 + Math.round(Math.random() * 80)
}))
}
}
// ========== 购物车Repository ==========
class CartRepository {
private static instance: CartRepository
private cartItems: CartItem[] = []
private constructor() {}
static getInstance(): CartRepository {
if (!CartRepository.instance) {
CartRepository.instance = new CartRepository()
}
return CartRepository.instance
}
async getCartItems(): Promise<CartItem[]> {
// 实际项目:const response = await http.get('/api/cart/list')
return this.cartItems
}
async addToCart(item: CartItem): Promise<boolean> {
// 实际项目:const response = await http.post('/api/cart/add', item)
const existing = this.cartItems.find(i => i.skuId === item.skuId)
if (existing) {
existing.quantity += item.quantity
} else {
this.cartItems.push(item)
}
this.updateAppStoreCart()
return true
}
async removeFromCart(cartItemId: string): Promise<boolean> {
this.cartItems = this.cartItems.filter(i => i.id !== cartItemId)
this.updateAppStoreCart()
return true
}
async updateQuantity(cartItemId: string, quantity: number): Promise<boolean> {
const item = this.cartItems.find(i => i.id === cartItemId)
if (item) {
item.quantity = Math.max(1, Math.min(quantity, item.stock))
this.updateAppStoreCart()
}
return true
}
private updateAppStoreCart(): void {
const totalCount = this.cartItems.reduce((sum, i) => sum + i.quantity, 0)
const selectedItems = this.cartItems.filter(i => i.selected && i.isValid)
const selectedCount = selectedItems.reduce((sum, i) => sum + i.quantity, 0)
const totalPrice = selectedItems.reduce((sum, i) => sum + i.price * i.quantity, 0)
AppStore.getInstance().updateCartState(totalCount, selectedCount, totalPrice)
}
}
// ========== 订单Repository ==========
class OrderRepository {
private static instance: OrderRepository
private constructor() {}
static getInstance(): OrderRepository {
if (!OrderRepository.instance) {
OrderRepository.instance = new OrderRepository()
}
return OrderRepository.instance
}
async createOrder(items: CartItem[], addressId: string): Promise<OrderInfo | null> {
// 实际项目:const response = await http.post('/api/order/create', { items, addressId })
const order: OrderInfo = {
id: `order_${Date.now()}`,
orderNo: `20241225${Date.now()}`,
status: OrderStatus.PENDING_PAYMENT,
totalPrice: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
items: items.map(i => ({
productId: i.productId,
skuId: i.skuId,
title: i.title,
imageUrl: i.imageUrl,
specDesc: i.specDesc,
price: i.price,
quantity: i.quantity
})),
createTime: new Date().toISOString(),
shopName: items[0]?.shopName || ''
}
return order
}
async getOrderList(status?: OrderStatus): Promise<OrderInfo[]> {
// 实际项目:const response = await http.get('/api/order/list', { status })
return []
}
async cancelOrder(orderId: string): Promise<boolean> {
// 实际项目:const response = await http.post('/api/order/cancel', { orderId })
return true
}
}
完整示例:电商App主框架
把所有模块串成完整App——底部Tab导航、页面路由、全局状态同步。
// ECommerceApp.ets — 完整电商应用
import { router } from '@kit.ArkUI'
// ========== 底部Tab配置 ==========
interface TabConfig {
title: string
icon: Resource
activeIcon: Resource
pageUrl: string
}
@Entry
@Component
struct ECommerceApp {
@State currentTab: number = 0
@State cartCount: number = 0
@State unreadCount: number = 0
@State isLogin: boolean = false
private appStore: AppStore = AppStore.getInstance()
private tabs: TabConfig[] = [
{ title: '首页', icon: $r('app.media.ic_home'), activeIcon: $r('app.media.ic_home_active'), pageUrl: 'pages/HomePage' },
{ title: '分类', icon: $r('app.media.ic_category'), activeIcon: $r('app.media.ic_category_active'), pageUrl: 'pages/CategoryPage' },
{ title: '购物车', icon: $r('app.media.ic_cart'), activeIcon: $r('app.media.ic_cart_active'), pageUrl: 'pages/CartPage' },
{ title: '消息', icon: $r('app.media.ic_message'), activeIcon: $r('app.media.ic_message_active'), pageUrl: 'pages/MessagePage' },
{ title: '我的', icon: $r('app.media.ic_profile'), activeIcon: $r('app.media.ic_profile_active'), pageUrl: 'pages/ProfilePage' },
]
aboutToAppear() {
// 监听全局状态变化
this.appStore.addListener(() => {
this.cartCount = this.appStore.getCartCount()
this.unreadCount = this.appStore.getUnreadCount()
this.isLogin = this.appStore.isLogin()
})
// 初始化数据
this.initApp()
}
build() {
Tabs() {
// 首页Tab
TabContent() {
this.HomeTabContent()
}
.tabBar(this.TabBar(0))
// 分类Tab
TabContent() {
this.CategoryTabContent()
}
.tabBar(this.TabBar(1))
// 购物车Tab
TabContent() {
this.CartTabContent()
}
.tabBar(this.TabBar(2))
// 消息Tab
TabContent() {
this.MessageTabContent()
}
.tabBar(this.TabBar(3))
// 我的Tab
TabContent() {
this.ProfileTabContent()
}
.tabBar(this.TabBar(4))
}
.barPosition(BarPosition.End)
.scrollable(false)
.onChange((index: number) => {
this.currentTab = index
this.appStore.setCurrentTab(index)
})
.width('100%')
.height('100%')
}
// ========== 自定义TabBar ==========
@Builder
TabBar(index: number) {
Column() {
Stack() {
Image(index === this.currentTab ? this.tabs[index].activeIcon : this.tabs[index].icon)
.width(24)
.height(24)
.fillColor(index === this.currentTab ? '#FF4444' : '#999999')
// 购物车角标
if (index === 2 && this.cartCount > 0) {
Text(this.cartCount > 99 ? '99+' : `${this.cartCount}`)
.fontSize(9)
.fontColor(Color.White)
.backgroundColor('#FF4444')
.borderRadius(8)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
.position({ x: 14, y: -4 })
}
// 消息角标
if (index === 3 && this.unreadCount > 0) {
Circle()
.width(8)
.height(8)
.fill('#FF4444')
.position({ x: 16, y: -2 })
}
}
.width(24)
.height(24)
Text(this.tabs[index].title)
.fontSize(10)
.fontColor(index === this.currentTab ? '#FF4444' : '#999999')
.margin({ top: 2 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
// ========== 首页内容 ==========
@Builder
HomeTabContent() {
Column() {
// 顶部搜索栏
Row() {
Image($r('app.media.ic_scan'))
.width(24).height(24).fillColor('#333333').margin({ right: 12 })
Row() {
Image($r('app.media.ic_search')).width(16).height(16).fillColor('#999999')
Text('搜索商品').fontSize(14).fontColor('#999999').margin({ left: 6 })
}
.layoutWeight(1).height(36).padding({ left: 12 })
.backgroundColor('#F0F0F0').borderRadius(18)
.onClick(() => {
router.pushUrl({ url: 'pages/SearchPage' })
})
Image($r('app.media.ic_message')).width(24).height(24).fillColor('#333333').margin({ left: 12 })
}
.width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
// 首页内容区(简化展示)
Scroll() {
Column() {
// Banner
Swiper() {
ForEach([1, 2, 3], (item: number) => {
Image(`https://picsum.photos/750/360?random=${item}`)
.width('100%').height(180).borderRadius(8).objectFit(ImageFit.Cover)
}, (item: number) => `${item}`)
}
.autoPlay(true).interval(4000).loop(true)
.indicator(new DotIndicator().color('#80FFFFFF').selectedColor('#FFFFFF'))
.width('100%').height(180).margin({ top: 8, left: 12, right: 12 })
// 金刚区
Grid() {
ForEach(['数码', '服装', '食品', '家居', '美妆', '运动', '图书', '母婴', '家电', '更多'], (name: string) => {
GridItem() {
Column() {
Circle().width(36).height(36).fill('#F0F0F0')
Text(name).fontSize(11).fontColor('#333333').margin({ top: 4 })
}
}
}, (name: string) => name)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsGap(12).columnsGap(0)
.width('100%').padding({ left: 12, right: 12, top: 16, bottom: 16 })
.backgroundColor(Color.White).borderRadius(8).margin({ top: 8, left: 12, right: 12 })
// 推荐商品
Text('为你推荐')
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
.width('100%').padding({ left: 16, top: 16, bottom: 8 })
// 瀑布流商品(简化展示)
Grid() {
ForEach(Array.from({ length: 6 }, (_, i) => i), (i: number) => {
GridItem() {
Column() {
Image(`https://picsum.photos/300/${200 + i * 20}?random=${i + 10}`)
.width('100%').height(150 + i * 10).borderRadius({ topLeft: 8, topRight: 8 })
.objectFit(ImageFit.Cover)
Column() {
Text(`精选好物推荐 ${i + 1}`).fontSize(13).fontColor('#333333')
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`¥${Math.round(Math.random() * 500 + 10)}`).fontSize(16)
.fontWeight(FontWeight.Bold).fontColor('#FF4444').margin({ top: 4 })
}.padding({ left: 8, right: 8, top: 4, bottom: 8 }).alignItems(HorizontalAlign.Start)
}
.backgroundColor(Color.White).borderRadius(8)
.onClick(() => {
router.pushUrl({ url: 'pages/ProductDetailPage', params: { productId: `p_${i}` } })
})
}
}, (i: number) => `${i}`)
}
.columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8)
.padding({ left: 12, right: 12 })
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
// ========== 分类Tab ==========
@Builder
CategoryTabContent() {
Column() {
Text('分类页面').fontSize(16).fontColor('#999999')
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
// ========== 购物车Tab ==========
@Builder
CartTabContent() {
Column() {
Row() {
Text('购物车')
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
if (this.cartCount > 0) {
Text(`(${this.cartCount})`).fontSize(14).fontColor('#999999').margin({ left: 4 })
}
Blank()
Text('编辑').fontSize(14).fontColor('#333333')
}
.width('100%').height(48).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
if (this.cartCount === 0) {
Column() {
Text('🛒').fontSize(64)
Text('购物车空空如也').fontSize(16).fontColor('#999999').margin({ top: 16 })
Button('去逛逛').fontSize(14).fontColor(Color.White)
.backgroundColor('#FF4444').borderRadius(20).width(120).height(40).margin({ top: 24 })
.onClick(() => { this.currentTab = 0 })
}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
} else {
Text('购物车商品列表区域')
.fontSize(14).fontColor('#999999')
.layoutWeight(1).width('100%').textAlign(TextAlign.Center)
.padding({ top: 100 })
}
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
// ========== 消息Tab ==========
@Builder
MessageTabContent() {
Column() {
Text('消息').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333')
.width('100%').padding({ left: 16, top: 16 })
Text('会话列表区域').fontSize(14).fontColor('#999999')
.layoutWeight(1).width('100%').textAlign(TextAlign.Center).padding({ top: 100 })
}
.width('100%').height('100%').backgroundColor(Color.White)
}
// ========== 我的Tab ==========
@Builder
ProfileTabContent() {
Column() {
// 用户信息头部
Row() {
Image($r('app.media.ic_avatar_default'))
.width(64).height(64).borderRadius(32).fillColor('#1DA1F2')
Column() {
Text(this.isLogin ? '鸿蒙开发者' : '点击登录')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
if (this.isLogin) {
Text('138****8000').fontSize(13).fontColor('#CCFFFFFF').margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.Start).margin({ left: 16 })
Blank()
Image($r('app.media.ic_arrow_right')).width(20).height(20).fillColor('#FFFFFF')
}
.width('100%').padding({ left: 20, right: 20, top: 24, bottom: 20 })
.linearGradient({ direction: GradientDirection.Right, colors: [['#1DA1F2', 0], ['#0D7CC4', 1]] })
.onClick(() => {
if (!this.isLogin) {
router.pushUrl({ url: 'pages/LoginPage' })
} else {
router.pushUrl({ url: 'pages/UserProfilePage' })
}
})
// 订单入口
Row() {
ForEach(['待付款', '待发货', '待收货', '已完成', '退换货'], (title: string) => {
Column() {
Circle().width(28).height(28).fill('#F0F0F0')
Text(title).fontSize(11).fontColor('#333333').margin({ top: 4 })
}.layoutWeight(1)
}, (title: string) => title)
}
.width('100%').padding({ top: 16, bottom: 16 })
.backgroundColor(Color.White).borderRadius(8)
.margin({ top: -16, left: 12, right: 12 })
// 功能菜单
Column() {
ForEach(['优惠券', '我的收藏', '收货地址', '设置'], (title: string) => {
Row() {
Text(title).fontSize(15).fontColor('#333333')
Blank()
Image($r('app.media.ic_arrow_right')).width(16).height(16).fillColor('#CCCCCC')
}
.width('100%').height(52).padding({ left: 16, right: 16 }).backgroundColor(Color.White)
}, (title: string) => title)
}
.borderRadius(8).margin({ top: 8, left: 12, right: 12 })
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
// ========== 初始化App ==========
async initApp() {
// 1. 检查登录状态
const token = await this.loadToken()
if (token) {
// 自动登录
this.appStore.login('user_001', '鸿蒙开发者', 'https://picsum.photos/128/128?random=1', '13800138000')
this.isLogin = true
// 2. 加载购物车数据
const cartRepo = CartRepository.getInstance()
await cartRepo.getCartItems()
// 3. 加载未读消息数
this.appStore.setUnreadCount(5)
}
}
async loadToken(): Promise<string | null> {
// 实际项目:从加密存储读取Token
return 'mock_token'
}
}
踩坑与注意事项
坑1:购物车角标不更新
首页Tab上购物车角标显示3,加了一个商品后还是3——因为角标数据没有同步。
解决方案:购物车数据变更时,调用AppStore.updateCartState()更新全局状态,TabBar监听全局状态变化自动刷新角标。
坑2:页面间数据传递
详情页加购后,购物车页怎么知道有新商品?
解决方案:不要用页面间参数传递,用全局状态。加购操作更新CartRepository,CartRepository更新AppStore,购物车页从AppStore读取数据。
坑3:登录状态过期
用户登录后Token过期了,请求接口返回401,但页面还显示已登录。
解决方案:网络层统一处理401,自动跳转登录页。登录后刷新Token,重新发起失败的请求。
坑4:首页数据缓存策略
每次切到首页Tab都重新请求数据?那用户来回切换Tab,首页数据反复加载。
解决方案:首页数据缓存5分钟。5分钟内切回首页不重新请求,超过5分钟才刷新。下拉刷新时强制更新。
坑5:Tab切换时页面状态丢失
用户在首页滑到第50个商品,切到购物车再切回来——首页又回到顶部了。
解决方案:每个Tab的内容用独立组件包裹,组件状态由@State管理。Tabs组件设置scrollable(false),避免预加载导致状态丢失。
HarmonyOS 6适配说明
HarmonyOS 6对完整App架构做了以下更新:
- AppStorage全局状态:HarmonyOS 6增强了
AppStorage能力,支持跨组件、跨页面的状态共享。不需要自己写全局状态管理器,直接用@StorageProp和@StorageLink装饰器。
// HarmonyOS 6全局状态
@Entry
@Component
struct SomePage {
@StorageProp('cartCount') cartCount: number = 0 // 单向绑定
@StorageLink('userName') userName: string = '' // 双向绑定
}
// 设置全局状态
AppStorage.setOrCreate('cartCount', 5)
AppStorage.setOrCreate('userName', '鸿蒙开发者')
-
Navigation组件增强:Navigation新增了
NavPathStack路由栈管理,支持声明式路由配置。不需要手动管理页面跳转和返回。 -
Tabs组件状态保持:Tabs新增了
offscreenMode属性,切到其他Tab时当前Tab的内容不会销毁,切回来时状态保持不变。 -
模块化开发HSP:HarmonyOS 6的HSP(Harmony Shared Package)支持模块化开发,首页、购物车、订单等模块可以独立编译、独立测试、独立发布。
-
应用启动优化:新增
StartupTask框架,App启动时并行初始化各模块。登录、购物车、消息等模块的初始化任务可以并行执行,启动速度提升约40%。
总结
完整电商App的核心不是"每个模块都能跑",而是模块之间的数据流转和状态同步。购物车加购→角标更新→订单创建→支付完成→订单状态变更——这条链路任何一个环节断开,用户体验就崩了。
核心记住三点:
- 全局状态管理是枢纽,用户状态、购物车状态、消息状态必须全局共享
- 数据层统一封装,页面不直接调网络接口,通过Repository获取数据
- 页面间不要传数据,用全局状态同步,不要用路由参数传递业务数据
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐⭐ 架构设计、状态管理、数据流转都需要全局视角 |
| 使用频率 | ⭐⭐⭐⭐⭐ 完整App开发必须掌握 |
| 重要程度 | ⭐⭐⭐⭐⭐ 架构做不好,后期维护成本指数级增长 |
模块拼到一起跑不起来——这不是bug,这是架构问题。
- 点赞
- 收藏
- 关注作者
评论(0)