在构建“美寇商城”这类复杂的鸿蒙应用时,随着业务模块(商品、购物车、订单、用户)的增长,跨页面的数据共享、状态同步和数据流向管理成为架构设计的核心挑战。鸿蒙5.0的ArkUI框架提供了从组件内到应用级的多种状态管理方案,但如何根据业务场景选择并组合它们,设计出清晰、可维护且高性能的全局数据流,则是进阶开发的关键。

本文将深入探讨在鸿蒙5.0环境下,为“美寇商城”设计和实现一套完整的全局数据流方案,涵盖从基础状态管理到复杂状态同步的进阶实践。

一、 美寇商城状态管理全景图与设计思路

在设计数据流之前,首先要对应用的状态进行分类。对于电商应用,状态通常可以分为:

  1. UI状态:与当前页面或组件视觉表现相关的临时状态,如表单输入、弹窗显示、列表滚动位置等。
  2. 业务状态:应用的核心数据,如用户信息、购物车商品列表、商品详情、订单列表等。这些状态需要在多个页面间共享,并且可能持久化。
  3. 应用状态:应用的全局性信息,如网络连接状态、当前主题(深色/浅色模式)、用户登录Token等。

针对不同状态的特点和范围,鸿蒙5.0 ArkUI框架提供了分层级的解决方案。下图清晰地展示了“美寇商城”如何将这三类状态与鸿蒙5.0提供的不同状态管理能力进行映射与整合,形成清晰的数据流架构:
在这里插入图片描述
设计核心思路:单向数据流
为了确保状态变化的可预测性和可调试性,“美寇商城”的全局状态管理遵循单向数据流原则。其核心流程是:

  1. 状态存储在确定的“数据源”(如AppStorage、自定义Store)中。
  2. UI组件通过装饰器(如@StorageLink)订阅这些状态。
  3. 用户交互或异步事件触发一个动作,该动作会通过一个统一的、可追踪的途径(如dispatch函数)去修改数据源中的状态。
  4. 状态更新后,自动通知所有订阅该状态的UI组件进行刷新。

这种模式(类似于Flux/Redux)确保了数据流动的单一方向,使得任何状态变化的原因和路径都清晰明了。

二、 基础到进阶:状态管理方案选型与实践

2.1 组件级状态管理 (@State, @Prop, @Link, @Provide/@Consume)

这些装饰器用于管理组件内部或父子组件间的状态。

场景示例:商品数量选择器组件
这是一个典型的父子组件通信场景,父组件(商品详情页)需要知道子组件(选择器)的变化。

// 子组件:GoodsCounter.ets
@Component
struct GoodsCounter {
  // @Link装饰器:建立父子组件间的双向数据同步
  // 父组件通过`$`符号传递一个引用
  @Link @Watch('onCountChange') count: number;
  // @Prop装饰器:父组件传递的不可变参数
  @Prop maxStock: number;

  // 监听@Link变量变化,可用于执行副作用(如校验)
  onCountChange(): void {
    if (this.count > this.maxStock) {
      console.warn('数量超过库存');
      // 这里可以触发Toast提示,但为了数据流纯净,建议在Action中处理
    }
  }

  build() {
    Row({ space: 10 }) {
      Button('-')
        .onClick(() => {
          if (this.count > 1) {
            this.count--;
          }
        })
        .enabled(this.count > 1)
      Text(`${this.count}`)
        .width(40)
        .textAlign(TextAlign.Center)
      Button('+')
        .onClick(() => {
          if (this.count < this.maxStock) {
            this.count++;
          }
        })
        .enabled(this.count < this.maxStock)
    }
  }
}

// 父组件:GoodsDetailPage.ets
@Component
struct GoodsDetailPage {
  // 父组件使用@State管理该页面的商品数量状态
  @State selectedSkuCount: number = 1;
  private maxStock: number = 99;

  build() {
    Column() {
      // ... 其他商品信息
      // 将父组件的@State变量通过`$`符号创建引用传递给子组件的@Link变量
      GoodsCounter({ count: $selectedSkuCount, maxStock: this.maxStock })
      Button('加入购物车')
        .onClick(() => {
          // 点击时,selectedSkuCount已经是更新后的值
          this.addToCart(this.selectedSkuCount);
        })
    }
  }

  private addToCart(num: number): void {
    // 调用全局的Action来修改购物车状态
    CartStore.dispatch(CartAction.addItem(this.goodsId, num, this.selectedSku));
  }
}

@Provide/@Consume 则用于跨层级组件(例如祖先和后代,中间可能隔了多层)的状态同步,无需逐层传递参数。

2.2 应用级状态管理 (AppStoragePersistentStorage)

当状态需要在多个不相关的页面间共享时,应使用应用级的AppStorage

场景示例:用户登录状态与浅色/深色主题
这些是典型的全局状态。

// src/main/ets/stores/AppGlobalStore.ets
import { PersistentStorage } from './PersistentStorage';

/**
 * 使用AppStorage管理全局应用状态
 * 键名统一管理,避免魔法字符串
 */
export class AppGlobalStore {
  // 键名常量
  static readonly KEY_USER_TOKEN = 'userToken';
  static readonly KEY_USER_INFO = 'userInfo';
  static readonly KEY_APP_THEME = 'appTheme'; // 'light' 或 'dark'
  static readonly KEY_SHOPPING_CART_COUNT = 'cartTotalCount';

  // 初始化应用状态
  static init(): void {
    // 1. 检查持久化存储,恢复主题偏好
    const savedTheme = PersistentStorage.get<string>(this.KEY_APP_THEME);
    if (savedTheme) {
      AppStorage.setOrCreate(this.KEY_APP_THEME, savedTheme);
    } else {
      AppStorage.setOrCreate(this.KEY_APP_THEME, 'light');
    }

    // 2. 从本地恢复用户Token(静默登录)
    const savedToken = PersistentStorage.get<string>(this.KEY_USER_TOKEN);
    if (savedToken) {
      // 验证Token有效性...
      AppStorage.setOrCreate(this.KEY_USER_TOKEN, savedToken);
      // 触发获取用户信息
      this.fetchUserInfo();
    }

    // 3. 初始化购物车数量(可从本地缓存或网络获取)
    AppStorage.setOrCreate(this.KEY_SHOPPING_CART_COUNT, 0);
    this.syncCartCountFromServer();
  }

  // 对外提供的获取状态的方法,便于统一管理
  static getUserToken(): string | undefined {
    return AppStorage.get<string>(this.KEY_USER_TOKEN);
  }

  static isLoggedIn(): boolean {
    return !!this.getUserToken();
  }

  // 更新状态的方法,统一在此处进行,便于加入日志、埋点等副作用
  static setTheme(newTheme: 'light' | 'dark'): void {
    AppStorage.setOrCreate(this.KEY_APP_THEME, newTheme);
    // 同步到持久化存储
    PersistentStorage.set(this.KEY_APP_THEME, newTheme);
    console.log(`主题已切换为: ${newTheme}`);
  }

  static setUserToken(token: string): void {
    AppStorage.setOrCreate(this.KEY_USER_TOKEN, token);
    PersistentStorage.set(this.KEY_USER_TOKEN, token); // 持久化Token
    this.fetchUserInfo(); // 获取用户详情
  }

  static clearUserInfo(): void {
    AppStorage.delete(this.KEY_USER_TOKEN);
    AppStorage.delete(this.KEY_USER_INFO);
    PersistentStorage.delete(this.KEY_USER_TOKEN); // 清除持久化Token
    // 同时需要清空其他依赖登录的业务状态,如购物车
    CartStore.dispatch(CartAction.clearCart());
  }

  private static async fetchUserInfo(): Promise<void> {
    // 网络请求获取用户信息...
    // const userInfo = await UserApi.getProfile();
    // AppStorage.setOrCreate(this.KEY_USER_INFO, userInfo);
  }

  private static async syncCartCountFromServer(): Promise<void> {
    if (this.isLoggedIn()) {
      // 从网络同步购物车数量
      // const count = await CartApi.getTotalCount();
      // AppStorage.setOrCreate(this.KEY_SHOPPING_CART_COUNT, count);
    }
  }
}

// 持久化存储封装
export class PersistentStorage {
  static get<T>(key: string): T | undefined {
    // 实际使用 @ohos.data.preferences 或其他持久化API
    try {
      const value = localStorage.getItem(key); // 示例,非鸿蒙API
      return value ? JSON.parse(value) : undefined;
    } catch (e) {
      console.error(`读取持久化数据 ${key} 失败:`, e);
      return undefined;
    }
  }

  static set(key: string, value: any): void {
    try {
      localStorage.setItem(key, JSON.stringify(value)); // 示例,非鸿蒙API
    } catch (e) {
      console.error(`保存持久化数据 ${key} 失败:`, e);
    }
  }

  static delete(key: string): void {
    localStorage.removeItem(key); // 示例,非鸿蒙API
  }
}

在任何页面或组件中,都可以轻松订阅或修改这些全局状态:

// 在任何需要显示购物车数量的组件中,例如首页右上角
@Component
struct CartIconComponent {
  // @StorageLink装饰器:与AppStorage建立双向同步
  @StorageLink(AppGlobalStore.KEY_SHOPPING_CART_COUNT) cartTotalCount: number = 0;

  build() {
    Badge({
      count: this.cartTotalCount,
      position: BadgePosition.RightTop,
      style: { color: '#FFFFFF', backgroundColor: '#FF0000', fontSize: 10 }
    }) {
      Image($r('app.media.ic_cart'))
        .onClick(() => {
          // 使用之前文章介绍的HmRouter进行跳转
          HmRouter.getInstance().navigateToCart();
        })
    }
  }
}

// 在“我的”页面,根据登录状态显示不同内容
@Component
struct UserCenterPage {
  // @StorageProp装饰器:与AppStorage建立单向同步(只读)
  @StorageProp(AppGlobalStore.KEY_USER_INFO) userInfo: UserInfo | undefined;
  @StorageProp(AppGlobalStore.KEY_APP_THEME) currentTheme: string;

  build() {
    Column() {
      if (this.userInfo) {
        // 已登录状态:显示用户信息
        UserCard({ userInfo: this.userInfo })
      } else {
        // 未登录状态:显示登录按钮
        Button('点击登录')
          .onClick(() => {
            HmRouter.getInstance().navigateToLogin();
          })
      }

      // 主题切换开关
      Toggle({ type: ToggleType.Checkbox, isOn: this.currentTheme === 'dark' })
        .onChange((isOn: boolean) => {
          // 调用全局Store的方法修改状态,而非直接修改AppStorage
          AppGlobalStore.setTheme(isOn ? 'dark' : 'light');
        })
    }
  }
}

三、 复杂业务状态管理:自定义Store与单向数据流

对于购物车、商品列表这类包含复杂业务逻辑和异步操作的状态,仅使用AppStorage会显得力不从心。我们需要更强大的工具来管理状态更新的逻辑(Reducer/Action)和副作用(如网络请求)。此时,可以借鉴Redux、Vuex等思想,结合鸿蒙的响应式系统,创建自定义的业务状态Store

3.1 购物车状态Store设计

购物车是电商应用中最复杂的模块之一,涉及添加、删除、修改数量、选中、结算、同步服务器等多个操作。

// src/main/ets/stores/CartStore.ets
import { AppStorage } from '@ohos.app.storage';
import { CartItem, CartState } from '../model/CartModel';

// 1. 定义Action类型
export type CartAction =
  | { type: 'CART_ADD_ITEM'; payload: { item: CartItem } }
  | { type: 'CART_REMOVE_ITEM'; payload: { skuId: string } }
  | { type: 'CART_UPDATE_QUANTITY'; payload: { skuId: string; quantity: number } }
  | { type: 'CART_TOGGLE_ITEM_SELECT'; payload: { skuId: string; selected: boolean } }
  | { type: 'CART_CLEAR' }
  | { type: 'CART_SYNC_SUCCESS'; payload: { items: CartItem[] } }
  | { type: 'CART_SET_LOADING'; payload: { isLoading: boolean } };

// 2. 定义初始状态
const initialState: CartState = {
  items: [],
  selectedItems: [],
  totalPrice: 0,
  totalCount: 0,
  isLoading: false,
  error: null
};

// 3. 创建响应式状态管理类
export class CartStore {
  // 私有静态实例,单例模式
  private static instance: CartStore;
  // 内部状态(可以是@State,但这里我们与AppStorage联动)
  private state: CartState = initialState;

  // 监听状态变化的回调函数列表(简化版发布订阅)
  private listeners: Array<(state: CartState) => void> = [];

  private constructor() {
    // 尝试从本地存储初始化
    this.loadFromLocal();
    // 如果用户已登录,与服务器同步
    if (AppGlobalStore.isLoggedIn()) {
      this.syncWithServer();
    }
  }

  static getInstance(): CartStore {
    if (!CartStore.instance) {
      CartStore.instance = new CartStore();
    }
    return CartStore.instance;
  }

  // 4. 核心Reducer:纯函数,根据Action和旧状态计算新状态
  private reducer(state: CartState, action: CartAction): CartState {
    switch (action.type) {
      case 'CART_ADD_ITEM': {
        const { item } = action.payload;
        const existingIndex = state.items.findIndex(i => i.skuId === item.skuId);
        let newItems = [...state.items];
        if (existingIndex >= 0) {
          // 商品已存在,增加数量
          newItems[existingIndex] = {
            ...newItems[existingIndex],
            quantity: newItems[existingIndex].quantity + item.quantity
          };
        } else {
          // 新商品,添加到列表
          newItems.push({ ...item, selected: true }); // 默认选中
        }
        return this.calculateDerivedState({ ...state, items: newItems });
      }

      case 'CART_UPDATE_QUANTITY': {
        const { skuId, quantity } = action.payload;
        const newItems = state.items.map(item =>
          item.skuId === skuId ? { ...item, quantity: Math.max(1, quantity) } : item
        );
        return this.calculateDerivedState({ ...state, items: newItems });
      }

      case 'CART_TOGGLE_ITEM_SELECT': {
        const { skuId, selected } = action.payload;
        const newItems = state.items.map(item =>
          item.skuId === skuId ? { ...item, selected } : item
        );
        return this.calculateDerivedState({ ...state, items: newItems });
      }

      case 'CART_SYNC_SUCCESS': {
        const { items } = action.payload;
        // 合并服务器数据与本地临时数据(复杂场景下需更精细的合并策略)
        return this.calculateDerivedState({ ...state, items, isLoading: false });
      }

      case 'CART_SET_LOADING': {
        return { ...state, isLoading: action.payload.isLoading };
      }

      case 'CART_CLEAR': {
        return this.calculateDerivedState({ ...state, items: [] });
      }

      default:
        return state;
    }
  }

  // 计算衍生状态:总价、总数、选中列表等
  private calculateDerivedState(state: CartState): CartState {
    const selectedItems = state.items.filter(item => item.selected);
    const totalCount = selectedItems.reduce((sum, item) => sum + item.quantity, 0);
    const totalPrice = selectedItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    // 更新AppStorage中的全局购物车数量角标
    AppStorage.setOrCreate(AppGlobalStore.KEY_SHOPPING_CART_COUNT, totalCount);
    // 保存到本地持久化(简化处理)
    this.saveToLocal(state.items);
    return { ...state, selectedItems, totalCount, totalPrice };
  }

  // 5. 公共API:dispatch Action
  static dispatch(action: CartAction): void {
    const store = CartStore.getInstance();
    const oldState = store.state;
    const newState = store.reducer(oldState, action);
    // 状态已变化,则更新并通知监听者
    if (newState !== oldState) {
      store.state = newState;
      store.notifyListeners(newState);
    }
  }

  // 6. 获取当前状态(只读)
  static getState(): CartState {
    return CartStore.getInstance().state;
  }

  // 7. 订阅状态变化
  subscribe(listener: (state: CartState) => void): () => void {
    this.listeners.push(listener);
    // 返回取消订阅的函数
    return () => {
      const index = this.listeners.indexOf(listener);
      if (index > -1) {
        this.listeners.splice(index, 1);
      }
    };
  }

  private notifyListeners(state: CartState): void {
    this.listeners.forEach(listener => listener(state));
  }

  // 本地持久化方法(示例)
  private saveToLocal(items: CartItem[]): void {
    PersistentStorage.set('local_cart_items', items);
  }

  private loadFromLocal(): void {
    const localItems = PersistentStorage.get<CartItem[]>('local_cart_items');
    if (localItems && localItems.length > 0) {
      this.state = this.calculateDerivedState({ ...this.state, items: localItems });
    }
  }

  // 异步Action创建函数(处理副作用)
  static async addItemWithCheck(goodsId: string, quantity: number, skuInfo: any): Promise<void> {
    // 1. 设置加载状态
    CartStore.dispatch({ type: 'CART_SET_LOADING', payload: { isLoading: true } });
    try {
      // 2. 调用网络API(此处为模拟)
      // const result = await CartApi.addItem(goodsId, quantity, skuInfo);
      // 3. 根据服务器响应更新本地状态(乐观更新或响应式更新)
      CartStore.dispatch({
        type: 'CART_ADD_ITEM',
        payload: { item: { skuId: `${goodsId}_${skuInfo.skuCode}`, goodsId, ...skuInfo, quantity, price: skuInfo.price } }
      });
      // 4. 可选的Toast提示
      // showToast('已加入购物车');
    } catch (error) {
      // 5. 处理错误,可以dispatch一个错误Action
      console.error('加入购物车失败:', error);
      // showToast('加入失败,请重试');
    } finally {
      CartStore.dispatch({ type: 'CART_SET_LOADING', payload: { isLoading: false } });
    }
  }

  private async syncWithServer(): Promise<void> {
    // if (AppGlobalStore.isLoggedIn()) {
    //   const serverCart = await CartApi.getCart();
    //   CartStore.dispatch({ type: 'CART_SYNC_SUCCESS', payload: { items: serverCart.items } });
    // }
  }
}

3.2 在UI组件中使用自定义Store

组件通过订阅CartStore来获取状态,通过调用CartStore.dispatch来触发状态更新。

// src/main/ets/pages/CartPage.ets
import { CartStore } from '../stores/CartStore';

@Component
struct CartPage {
  // 本地状态,用于存储从Store订阅来的购物车数据
  @State private cartState = CartStore.getState();
  // 存储取消订阅的函数
  private unsubscribe: (() => void) | undefined;

  aboutToAppear(): void {
    // 页面显示时,订阅购物车状态变化
    this.unsubscribe = CartStore.getInstance().subscribe((newState) => {
      // 当Store状态变化时,更新本地@State,触发UI刷新
      this.cartState = newState;
    });
  }

  aboutToDisappear(): void {
    // 页面离开时,取消订阅,防止内存泄漏
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }

  build() {
    Column() {
      if (this.cartState.isLoading) {
        LoadingProgress() // 加载中提示
      } else if (this.cartState.items.length === 0) {
        EmptyCartComponent() // 空购物车组件
      } else {
        List({ space: 10 }) {
          ForEach(this.cartState.items, (item: CartItem) => {
            ListItem() {
              CartItemComponent({ item })
            }
          })
        }

        // 底部结算栏
        CartSummaryBar({
          totalPrice: this.cartState.totalPrice,
          selectedCount: this.cartState.selectedItems.length,
          onCheckout: () => this.goToCheckout()
        })
      }
    }
  }

  private goToCheckout(): void {
    if (this.cartState.selectedItems.length > 0) {
      HmRouter.getInstance().navigateToCheckout(this.cartState.selectedItems);
    }
  }
}

// 购物车商品项组件
@Component
struct CartItemComponent {
  @Prop item: CartItem;

  build() {
    Row() {
      // 选中复选框
      Checkbox({ name: this.item.goodsName, checked: this.item.selected })
        .onChange((checked: boolean) => {
          // 派发Action来更新选中状态
          CartStore.dispatch({
            type: 'CART_TOGGLE_ITEM_SELECT',
            payload: { skuId: this.item.skuId, selected: checked }
          });
        })
      // ... 商品图片、名称等信息
      GoodsCounter({
        count: $item.quantity, // 注意:这里直接修改了Prop,在复杂场景下应避免,最好通过dispatch Action
        maxStock: this.item.stock
      })
      .onCountChange((newCount) => {
        // 更好的做法:监听子组件事件,然后dispatch Action
        CartStore.dispatch({
          type: 'CART_UPDATE_QUANTITY',
          payload: { skuId: this.item.skuId, quantity: newCount }
        });
      })
      Button('删除')
        .onClick(() => {
          CartStore.dispatch({
            type: 'CART_REMOVE_ITEM',
            payload: { skuId: this.item.skuId }
          });
        })
    }
  }
}

四、 状态管理与页面路由 (HmRouter) 的联动

全局状态,尤其是登录状态,经常与页面路由的拦截逻辑紧密相关。我们可以将定义的路由拦截器状态管理结合起来,实现更智能的路由控制。

// 增强版的登录状态拦截器
export class EnhancedLoginInterceptor implements RouterInterceptor {
  async intercept(context: RouterContext, chain: InterceptorChain): Promise<boolean> {
    const requireLoginPages = new Set([RouterPages.CART, RouterPages.PAYMENT, RouterPages.ORDER_LIST]);

    if (requireLoginPages.has(context.url)) {
      // 从AppGlobalStore获取登录状态,而非原始的本地存储
      if (!AppGlobalStore.isLoggedIn()) {
        console.log('[智能登录拦截器] 用户未登录,尝试使用本地Token静默登录...');

        // 1. 检查是否有缓存的Token
        const cachedToken = AppGlobalStore.getUserToken();
        if (cachedToken) {
          // 2. 尝试使用Token恢复登录状态(例如,验证Token有效性)
          const isValid = await this.validateTokenSilently(cachedToken);
          if (isValid) {
            console.log('[智能登录拦截器] 静默登录成功,放行路由');
            // (可选)更新用户信息
            await AppGlobalStore.fetchUserInfo();
            return chain.proceed(context); // 登录成功,继续跳转
          } else {
            // Token无效,清除本地缓存
            AppGlobalStore.clearUserInfo();
          }
        }

        // 3. 需要显式登录
        console.log('[智能登录拦截器] 需要用户显式登录,跳转至登录页');
        await HmRouter.getInstance().navigateToLogin({ redirectContext: context });
        return false;
      }
    }
    // 不需要登录或已登录,继续
    return chain.proceed(context);
  }

  private async validateTokenSilently(token: string): Promise<boolean> {
    // 模拟一个快速的Token验证请求
    try {
      // const response = await AuthApi.validateToken(token);
      // return response.isValid;
      return new Promise(resolve => setTimeout(() => resolve(token.startsWith('VALID')), 100));
    } catch {
      return false;
    }
  }
}

五、 总结与最佳实践

为“美寇商城”设计和实现全局数据流,需要综合运用鸿蒙5.0提供的多种状态管理工具。以下是关键总结与最佳实践:

  1. 分层管理,各司其职

    • UI状态:使用@State@Prop@Link管理组件内部和父子组件间的状态。
    • 应用全局状态:使用AppStorage管理跨页面的、简单的共享状态(如主题、Token)。
    • 复杂业务状态:为购物车、商品列表等模块创建自定义Store,使用单向数据流模式(Action/Reducer)管理逻辑和异步副作用。
  2. 坚持单向数据流:确保状态变更的路径唯一且可追踪。UI组件只应触发Action,而不直接修改核心状态。这大大增强了应用的可调试性和可维护性。

  3. 善用响应式装饰器@StorageLink@StorageProp是连接组件与AppStorage的桥梁,让全局状态变更能自动触发UI更新。

  4. 状态持久化:对于用户登录Token、购物车商品等关键状态,使用PersistentStoragePreferences进行本地持久化,提升用户体验。

  5. 与路由深度集成:将路由拦截器的判断逻辑建立在全局状态(如AppGlobalStore.isLoggedIn())之上,实现安全、智能的页面导航。

  6. 性能考量:避免将整个大型对象通过AppStorage@State传递。对于大型列表,应考虑使用@State管理列表引用,或使用LazyForEach进行优化渲染。

通过这套结合了鸿蒙原生能力与先进前端架构思想的全局数据流方案,“美寇商城”不仅能够高效地管理当前复杂的状态,更能从容应对未来业务规模的持续增长和功能迭代。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐