uniapp開發鴻蒙:數據綁定與狀態管理實戰

引入:響應式數據的重要性

在uniapp開發中,數據綁定與狀態管理是構建複雜應用的核心。Vue3的組合式API為我們提供了更靈活、更強大的響應式系統,而Pinia作為新一代狀態管理工具,讓跨組件、跨頁面的數據共享變得更加簡單高效。今天,我們將深入探討uniapp在鴻蒙平台下的數據綁定與狀態管理方案。

一、Vue3組合式API:ref與reactive

1.1 ref:基本類型的響應式包裝

ref是組合式API中最常用的響應式工具,用於包裝基本類型數據:

import { ref } from 'vue'

// 創建響應式數據
const count = ref(0)
const message = ref('Hello')
const isActive = ref(false)

// 修改數據(必須通過.value)
count.value++
message.value = 'Hello World'

// 在模板中自動解包,無需.value
// <view>{{ count }}</view>

特性説明

  • 支持所有數據類型(基本類型+對象)
  • 必須通過.value訪問和修改
  • 模板中自動解包,簡化使用
  • 適合管理單個值或需要整體替換的對象

1.2 reactive:對象的深度響應式

reactive用於創建深度響應式的對象或數組:

import { reactive } from 'vue'

// 創建響應式對象
const user = reactive({
  name: '張三',
  age: 25,
  address: {
    city: '北京',
    street: '朝陽區'
  }
})

// 直接修改屬性,無需.value
user.name = '李四'
user.address.city = '上海'

// 數組操作
const list = reactive([1, 2, 3])
list.push(4)

特性説明

  • 僅支持對象和數組類型
  • 深度響應式,嵌套屬性也是響應式的
  • 直接訪問屬性,無需.value
  • 適合管理複雜對象或表單數據

1.3 ref vs reactive:如何選擇?

特性

ref

reactive

適用類型

所有類型

僅對象/數組

訪問方式

.value

直接訪問屬性

深度響應

對象類型自動深度響應

深度響應

解構影響

解構後仍保持響應式

解構後丟失響應式

替換對象

支持整體替換

不支持整體替換

最佳實踐

  • 基本類型(數字、字符串、布爾值)使用ref
  • 複雜對象或表單數據使用reactive
  • 需要整體替換的對象用ref包裝

1.4 toRefs:保持解構響應式

當需要解構reactive對象時,使用toRefs保持響應式:

import { reactive, toRefs } from 'vue'

const user = reactive({
  name: '張三',
  age: 25
})

// 解構會丟失響應式
const { name, age } = user // ❌ 錯誤

// 使用toRefs保持響應式
const { name, age } = toRefs(user) // ✅ 正確
name.value = '李四' // 觸發更新

二、組件通信:props與emit

2.1 父組件向子組件傳值(props)

父組件

<template>
  <view>
    <child-component :message="parentMessage" :count="count"></child-component>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from '@/components/ChildComponent.vue'

const parentMessage = ref('Hello from parent')
const count = ref(0)
</script>

子組件

<template>
  <view>
    <text>{{ message }}</text>
    <text>{{ count }}</text>
  </view>
</template>

<script setup>
defineProps({
  message: {
    type: String,
    required: true,
    default: '默認值'
  },
  count: {
    type: Number,
    default: 0
  }
})
</script>

props驗證

  • type:數據類型(String、Number、Boolean、Array、Object等)
  • required:是否必填
  • default:默認值
  • validator:自定義驗證函數

2.2 子組件向父組件傳值(emit)

子組件

<template>
  <view>
    <button @click="sendMessage">發送消息</button>
  </view>
</template>

<script setup>
const emit = defineEmits(['updateMessage'])

const sendMessage = () => {
  emit('updateMessage', 'Hello from child')
}
</script>

父組件

<template>
  <view>
    <child-component @update-message="handleMessage"></child-component>
    <text>{{ message }}</text>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from '@/components/ChildComponent.vue'

const message = ref('')

const handleMessage = (msg) => {
  message.value = msg
}
</script>

emit事件命名規範

  • 使用kebab-case(短橫線命名)
  • 推薦使用update:xxx格式,配合v-model使用

2.3 v-model雙向綁定

子組件

<template>
  <view>
    <input :value="modelValue" @input="updateValue" />
  </view>
</template>

<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const updateValue = (event) => {
  emit('update:modelValue', event.target.value)
}
</script>

父組件

<template>
  <view>
    <child-component v-model="message"></child-component>
    <text>{{ message }}</text>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from '@/components/ChildComponent.vue'

const message = ref('')
</script>

三、Pinia狀態管理

3.1 安裝與配置

uniapp內置了Pinia,無需額外安裝:

main.js配置

import { createSSRApp } from 'vue'
import * as Pinia from 'pinia'
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = Pinia.createPinia()
  app.use(pinia)
  
  return {
    app,
    Pinia // 必須返回Pinia
  }
}

3.2 創建Store模塊

stores/user.js

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    token: '',
    isLogin: false
  }),
  
  getters: {
    // 計算屬性
    userName: (state) => state.userInfo?.name || '',
    isAdmin: (state) => state.userInfo?.role === 'admin'
  },
  
  actions: {
    // 同步操作
    setUserInfo(userInfo) {
      this.userInfo = userInfo
      this.isLogin = true
    },
    
    // 異步操作
    async login(account, password) {
      try {
        const res = await uni.request({
          url: '/api/login',
          method: 'POST',
          data: { account, password }
        })
        
        this.setUserInfo(res.data.userInfo)
        this.token = res.data.token
        
        // 持久化存儲
        uni.setStorageSync('token', this.token)
        uni.setStorageSync('userInfo', this.userInfo)
        
        return res.data
      } catch (error) {
        throw error
      }
    },
    
    logout() {
      this.userInfo = null
      this.token = ''
      this.isLogin = false
      
      // 清除本地存儲
      uni.removeStorageSync('token')
      uni.removeStorageSync('userInfo')
    }
  }
})

3.3 在組件中使用Store

<template>
  <view>
    <view v-if="userStore.isLogin">
      <text>歡迎,{{ userStore.userName }}</text>
      <button @click="userStore.logout">退出登錄</button>
    </view>
    <view v-else>
      <button @click="login">登錄</button>
    </view>
  </view>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 使用storeToRefs保持響應式
const { userInfo, isLogin } = storeToRefs(userStore)

const login = async () => {
  try {
    await userStore.login('admin', '123456')
    uni.showToast({ title: '登錄成功' })
  } catch (error) {
    uni.showToast({ title: '登錄失敗', icon: 'error' })
  }
}
</script>

3.4 跨頁面狀態共享

頁面A

import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const updateUser = () => {
  userStore.setUserInfo({
    name: '張三',
    age: 25,
    role: 'admin'
  })
}

頁面B

import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 自動獲取更新後的數據
console.log(userStore.userInfo.name) // 張三

3.5 數據持久化

安裝持久化插件:

npm install pinia-plugin-persistedstate

配置持久化

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    token: ''
  }),
  
  persist: {
    key: 'user-store',
    storage: {
      getItem: uni.getStorageSync,
      setItem: uni.setStorageSync,
      removeItem: uni.removeStorageSync
    },
    paths: ['userInfo', 'token'] // 只持久化指定字段
  }
})

四、鴻蒙平台特有配置

4.1 條件編譯處理平台差異

// 鴻蒙平台使用原生存儲
// #ifdef HARMONYOS
import preferences from '@ohos.data.preferences'

const saveData = async (key, value) => {
  const pref = await preferences.getPreferences(key)
  await pref.put(key, value)
  await pref.flush()
}
// #endif

// 其他平台使用uni存儲
// #ifndef HARMONYOS
const saveData = (key, value) => {
  uni.setStorageSync(key, value)
}
// #endif

4.2 鴻蒙原生狀態管理

uniapp在鴻蒙平台下會自動將Pinia狀態映射到鴻蒙原生狀態管理系統,實現更好的性能:

// 鴻蒙平台下,Pinia狀態會自動同步到ArkTS狀態管理
// 無需額外配置,uniapp會自動處理

五、實戰案例:購物車狀態管理

5.1 創建購物車Store

stores/cart.js

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // 購物車商品列表
    total: 0, // 總價
    count: 0  // 商品數量
  }),
  
  getters: {
    // 計算總價
    totalPrice: (state) => {
      return state.items.reduce((sum, item) => {
        return sum + item.price * item.quantity
      }, 0)
    },
    
    // 計算商品數量
    totalCount: (state) => {
      return state.items.reduce((sum, item) => {
        return sum + item.quantity
      }, 0)
    }
  },
  
  actions: {
    // 添加商品到購物車
    addItem(product) {
      const existingItem = this.items.find(item => item.id === product.id)
      
      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({
          ...product,
          quantity: 1
        })
      }
      
      this.updateTotal()
    },
    
    // 減少商品數量
    decreaseItem(id) {
      const item = this.items.find(item => item.id === id)
      if (item && item.quantity > 1) {
        item.quantity--
        this.updateTotal()
      }
    },
    
    // 刪除商品
    removeItem(id) {
      this.items = this.items.filter(item => item.id !== id)
      this.updateTotal()
    },
    
    // 清空購物車
    clearCart() {
      this.items = []
      this.updateTotal()
    },
    
    // 更新總價和數量
    updateTotal() {
      this.total = this.totalPrice
      this.count = this.totalCount
    }
  }
})

5.2 在商品列表頁使用

<template>
  <view>
    <view v-for="product in productList" :key="product.id">
      <text>{{ product.name }}</text>
      <text>¥{{ product.price }}</text>
      <button @click="addToCart(product)">加入購物車</button>
    </view>
  </view>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()
const productList = ref([
  { id: 1, name: '商品1', price: 99 },
  { id: 2, name: '商品2', price: 199 }
])

const addToCart = (product) => {
  cartStore.addItem(product)
  uni.showToast({ title: '添加成功' })
}
</script>

5.3 在購物車頁使用

<template>
  <view>
    <view v-for="item in cartStore.items" :key="item.id">
      <text>{{ item.name }}</text>
      <text>¥{{ item.price }}</text>
      <text>數量:{{ item.quantity }}</text>
      <button @click="cartStore.decreaseItem(item.id)">-</button>
      <button @click="cartStore.addItem(item)">+</button>
      <button @click="cartStore.removeItem(item.id)">刪除</button>
    </view>
    
    <view>
      <text>總計:¥{{ cartStore.total }}</text>
      <text>共{{ cartStore.count }}件商品</text>
    </view>
    
    <button @click="cartStore.clearCart">清空購物車</button>
  </view>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'

const cartStore = useCartStore()
const { items, total, count } = storeToRefs(cartStore)
</script>

六、性能優化建議

6.1 避免過度響應式

// ❌ 不推薦:每個對象都創建響應式
const list = ref([
  reactive({ id: 1, name: '商品1' }),
  reactive({ id: 2, name: '商品2' })
])

// ✅ 推薦:只對需要響應式的字段創建響應式
const list = ref([
  { id: 1, name: '商品1' },
  { id: 2, name: '商品2' }
])

// 或者使用shallowRef/shallowReactive
import { shallowRef } from 'vue'
const largeData = shallowRef({ /* 大型對象 */ })

6.2 合理使用計算屬性

import { computed } from 'vue'

// 計算屬性緩存結果,避免重複計算
const filteredList = computed(() => {
  return list.value.filter(item => item.price > 100)
})

// 複雜計算拆分
const totalPrice = computed(() => {
  return list.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

6.3 避免內存泄漏

// 組件卸載時清理事件監聽
onUnmounted(() => {
  uni.$off('customEvent')
})

// 定時器清理
const timer = setInterval(() => {
  // do something
}, 1000)

onUnmounted(() => {
  clearInterval(timer)
})

總結

通過本篇文章的學習,我們掌握了uniapp在鴻蒙平台下的數據綁定與狀態管理核心知識:

  1. 響應式基礎refreactive的區別與使用場景
  2. 組件通信propsemit實現父子組件數據傳遞
  3. 狀態管理:Pinia的安裝、配置和使用方法
  4. 實戰案例:購物車狀態管理的完整實現
  5. 性能優化:避免過度響應式、合理使用計算屬性

關鍵要點

  • 基本類型用ref,複雜對象用reactive
  • 解構reactive對象時使用toRefs保持響應式
  • Pinia是Vue3推薦的狀態管理工具,支持同步和異步操作
  • 鴻蒙平台下uniapp會自動處理狀態管理的跨平台適配

下一篇文章,我們將深入講解網絡請求與數據交互,包括uni.request的封裝、攔截器、錯誤處理、數據緩存等核心內容,幫助大家構建更健壯的應用。