HarmonyOS開發之多端協同案例——分佈式購物車
第一部分:引入
在日常購物場景中,我們經常遇到這樣的困擾:手機上瀏覽商品添加到購物車,走到電腦前想要結算時,卻發現購物車空空如也;或者與家人一起購物時,想要合併結算卻需要反覆分享商品鏈接。這種設備孤島和協作壁壘嚴重影響了購物體驗的連貫性。
HarmonyOS的分佈式購物車技術正是為解決這一痛點而生。它通過分佈式數據管理能力,將多個設備的購物車狀態實時同步,讓手機、平板、手錶等設備成為購物流程中的不同觸點。無論你切換設備還是與他人協作,購物車數據始終無縫流轉,真正實現"一處添加,處處可見"的超級終端體驗。
第二部分:講解
一、分佈式購物車的架構設計
1.1 核心架構原理
分佈式購物車基於HarmonyOS的分佈式數據管理框架,採用"發佈-訂閲"模式實現數據同步。其架構分為三層:
數據層:使用分佈式鍵值數據庫(KV Store)存儲購物車商品信息。每個商品被封裝為可觀察對象,當數據變更時自動觸發同步機制。
同步層:負責設備發現、連接管理和數據傳輸。通過分佈式軟總線自動建立設備間安全通道,採用增量同步策略減少網絡開銷。
UI層:各設備根據自身屏幕特性和交互方式,展示統一的購物車數據。平板採用雙列網格佈局,手機使用單列列表,手錶則顯示精簡信息。
1.2 數據同步流程
// 文件:src/main/ets/service/DistributedCartService.ts
import distributedData from '@ohos.data.distributedData';
import distributedDevice from '@ohos.distributedDevice';
@Component
export class DistributedCartService {
private kvManager: distributedData.KVManager | null = null;
private kvStore: distributedData.SingleKVStore | null = null;
private deviceList: distributedDevice.DeviceInfo[] = [];
// 初始化KV Store
async initKVStore(context: Context): Promise<void> {
try {
// 創建KV管理器配置
const kvManagerConfig: distributedData.KVManagerConfig = {
context: context,
bundleName: 'com.example.distributedcart'
};
this.kvManager = distributedData.createKVManager(kvManagerConfig);
// 配置KV Store選項
const options: distributedData.KVStoreOptions = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true, // 開啓自動同步
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
securityLevel: distributedData.SecurityLevel.S1
};
this.kvStore = await this.kvManager.getKVStore('shopping_cart_store', options) as distributedData.SingleKVStore;
// 訂閲數據變更事件
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
this.handleDataChange(data); // 處理數據變更
});
} catch (error) {
console.error('KV Store初始化失敗:', error);
}
}
}
二、完整代碼實現
2.1 權限配置
首先在配置文件中聲明必要的分佈式權限:
// 文件:src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:permission_reason_distributed_datasync",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.ACCESS_SERVICE_DM",
"reason": "$string:permission_reason_access_service_dm",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
在字符串資源中定義權限説明:
// 文件:src/main/resources/base/element/string.json
{
"string": [
{
"name": "permission_reason_distributed_datasync",
"value": "用於在不同設備間同步您的購物車數據,提供無縫體驗"
},
{
"name": "permission_reason_access_service_dm",
"value": "用於發現和連接附近的信任設備,以實現分佈式功能"
}
]
}
2.2 數據模型定義
定義商品數據模型,使用@Observed裝飾器實現深度觀察:
// 文件:src/main/ets/model/Product.ts
import { Observed } from '@arkts.observer';
@Observed
export class Product {
id: string;
name: string;
price: number;
count: number;
imageUrl: string;
deviceId: string; // 添加設備的ID,用於衝突解決
constructor(id: string, name: string, price: number, imageUrl: string = '') {
this.id = id;
this.name = name;
this.price = price;
this.count = 1;
this.imageUrl = imageUrl;
this.deviceId = '';
}
// 商品總價計算
get totalPrice(): number {
return this.price * this.count;
}
}
2.3 購物車服務實現
實現核心的分佈式購物車服務:
// 文件:src/main/ets/service/DistributedCartService.ts
import { BusinessError } from '@ohos.base';
import distributedData from '@ohos.data.distributedData';
import distributedDevice from '@ohos.distributedDevice';
import { Product } from '../model/Product';
export class DistributedCartService {
private static instance: DistributedCartService = new DistributedCartService();
private kvStore: distributedData.SingleKVStore | null = null;
public cartItems: Map<string, Product> = new Map();
private changeCallbacks: Array<(items: Map<string, Product>) => void> = [];
public static getInstance(): DistributedCartService {
return this.instance;
}
// 添加商品到購物車
async addProduct(product: Product): Promise<void> {
if (!this.kvStore) {
throw new Error('KV Store未初始化');
}
try {
// 設置設備ID,用於衝突解決
product.deviceId = await this.getLocalDeviceId();
// 將商品數據序列化並存儲
const productKey = `product_${product.id}`;
const productData = JSON.stringify(product);
await this.kvStore.put(productKey, productData);
console.info('商品添加成功:', product.name);
} catch (error) {
console.error('添加商品失敗:', error);
throw error;
}
}
// 從購物車移除商品
async removeProduct(productId: string): Promise<void> {
if (!this.kvStore) {
throw new Error('KV Store未初始化');
}
try {
const productKey = `product_${productId}`;
await this.kvStore.delete(productKey);
console.info('商品移除成功:', productId);
} catch (error) {
console.error('移除商品失敗:', error);
throw error;
}
}
// 更新商品數量
async updateProductCount(productId: string, newCount: number): Promise<void> {
if (!this.kvStore) {
throw new Error('KV Store未初始化');
}
try {
const productKey = `product_${productId}`;
const existingProduct = this.cartItems.get(productId);
if (existingProduct) {
existingProduct.count = newCount;
const productData = JSON.stringify(existingProduct);
await this.kvStore.put(productKey, productData);
}
} catch (error) {
console.error('更新商品數量失敗:', error);
throw error;
}
}
// 加載購物車數據
private async loadCartData(): Promise<void> {
if (!this.kvStore) return;
try {
const entries = await this.kvStore.getEntries('product_');
this.cartItems.clear();
entries.forEach(entry => {
try {
const product = JSON.parse(entry.value.toString()) as Product;
this.cartItems.set(product.id, product);
} catch (parseError) {
console.error('解析商品數據失敗:', parseError);
}
});
// 通知所有訂閲者數據已更新
this.notifyChange();
} catch (error) {
console.error('加載購物車數據失敗:', error);
}
}
// 處理數據變更
private handleDataChange(data: distributedData.ChangeData): Promise<void> {
console.info('接收到數據變更:', JSON.stringify(data));
return this.loadCartData();
}
// 獲取本地設備ID
private async getLocalDeviceId(): Promise<string> {
// 實際實現中應調用分佈式設備管理API
return 'local_device_id';
}
// 註冊數據變更回調
registerChangeCallback(callback: (items: Map<string, Product>) => void): void {
this.changeCallbacks.push(callback);
}
// 通知數據變更
private notifyChange(): void {
this.changeCallbacks.forEach(callback => {
callback(new Map(this.cartItems));
});
}
}
2.4 購物車UI實現
實現跨設備適配的購物車界面:
// 文件:src/main/ets/pages/ShoppingCartPage.ts
import { DistributedCartService } from '../service/DistributedCartService';
import { Product } from '../model/Product';
@Entry
@Component
struct ShoppingCartPage {
@State cartItems: Map<string, Product> = new Map();
@State totalPrice: number = 0;
@State isConnected: boolean = false;
private cartService: DistributedCartService = DistributedCartService.getInstance();
aboutToAppear(): void {
// 註冊數據變更回調
this.cartService.registerChangeCallback((items: Map<string, Product>) => {
this.cartItems = new Map(items);
this.calculateTotalPrice();
});
// 初始化購物車服務
this.initCartService();
}
// 初始化購物車服務
async initCartService(): Promise<void> {
try {
// 在實際實現中應傳遞正確的Context
await this.cartService.initKVStore(getContext(this));
this.isConnected = true;
} catch (error) {
console.error('購物車服務初始化失敗:', error);
}
}
// 計算總價
calculateTotalPrice(): void {
this.totalPrice = Array.from(this.cartItems.values()).reduce(
(total, product) => total + product.totalPrice, 0
);
}
// 增加商品數量
increaseQuantity(productId: string): void {
const product = this.cartItems.get(productId);
if (product) {
this.cartService.updateProductCount(productId, product.count + 1);
}
}
// 減少商品數量
decreaseQuantity(productId: string): void {
const product = this.cartItems.get(productId);
if (product && product.count > 1) {
this.cartService.updateProductCount(productId, product.count - 1);
}
}
// 結算操作
checkout(): void {
if (this.cartItems.size === 0) {
alert('購物車為空,請先添加商品');
return;
}
// 在實際實現中應跳轉到結算頁面
console.info('開始結算,總金額:', this.totalPrice);
}
build() {
Column({ space: 20 }) {
// 標題欄
Text('分佈式購物車')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 20, bottom: 10 })
// 連接狀態指示器
Row() {
Text(this.isConnected ? '設備已連接' : '設備未連接')
.fontSize(14)
.fontColor(this.isConnected ? '#00FF00' : '#FF0000')
Image(this.isConnected ? 'connected.png' : 'disconnected.png')
.width(20)
.height(20)
.margin({ left: 10 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.padding({ left: 20, right: 20 })
// 商品列表
List({ space: 15 }) {
ForEach(Array.from(this.cartItems.entries()), ([id, product]) => {
ListItem() {
this.buildProductItem(product);
}
})
}
.layoutWeight(1)
.width('100%')
.padding(10)
// 底部結算欄
this.buildCheckoutBar()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 構建商品項組件
@Builder
buildProductItem(product: Product) {
Row({ space: 15 }) {
// 商品圖片
Image(product.imageUrl || 'default_product.png')
.width(80)
.height(80)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// 商品信息
Column({ space: 5 }) {
Text(product.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
Text(`¥${product.price.toFixed(2)}`)
.fontSize(14)
.fontColor('#FF6B00')
.width('100%')
.textAlign(TextAlign.Start)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 數量控制器
Row({ space: 10 }) {
Button('-')
.width(30)
.height(30)
.fontSize(14)
.onClick(() => this.decreaseQuantity(product.id))
Text(product.count.toString())
.fontSize(16)
.width(40)
.textAlign(TextAlign.Center)
Button('+')
.width(30)
.height(30)
.fontSize(14)
.onClick(() => this.increaseQuantity(product.id))
}
// 移除按鈕
Button('刪除')
.width(60)
.height(30)
.fontSize(12)
.backgroundColor('#FF4757')
.fontColor('#FFFFFF')
.onClick(() => this.cartService.removeProduct(product.id))
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 2, color: '#000000', offsetX: 0, offsetY: 1 })
}
// 構建結算欄組件
@Builder
buildCheckoutBar() {
Column({ space: 10 }) {
// 總價信息
Row() {
Text('合計:')
.fontSize(16)
Text(`¥${this.totalPrice.toFixed(2)}`)
.fontSize(20)
.fontColor('#FF6B00')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })
// 結算按鈕
Button('去結算')
.width('90%')
.height(45)
.fontSize(18)
.backgroundColor('#07C160')
.fontColor('#FFFFFF')
.onClick(() => this.checkout())
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
}
}
三、關鍵API參數説明
|
API接口
|
參數説明
|
返回值
|
使用場景
|
|
|
|
|
創建分佈式數據管理器
|
|
|
|
|
獲取分佈式數據庫實例
|
|
|
|
|
存儲或更新數據
|
|
|
|
|
刪除指定數據
|
|
|
|
|
訂閲數據變更事件
|
|
|
|
|
獲取匹配前綴的所有數據項
|
四、多設備UI適配策略
4.1 設備類型檢測與適配
// 文件:src/main/ets/utils/DeviceAdapter.ts
import display from '@ohos.display';
export class DeviceAdapter {
// 檢測設備類型
static getDeviceType(): DeviceType {
const displayInfo = display.getDefaultDisplaySync();
const width = displayInfo.width;
const height = displayInfo.height;
const minSize = Math.min(width, height);
if (minSize < 600) {
return DeviceType.WEARABLE; // 手錶
} else if (minSize >= 600 && minSize < 1200) {
return DeviceType.PHONE; // 手機
} else {
return DeviceType.TABLET; // 平板
}
}
// 獲取佈局配置
static getLayoutConfig(): LayoutConfig {
const deviceType = this.getDeviceType();
switch (deviceType) {
case DeviceType.WEARABLE:
return {
columns: 1,
itemHeight: 80,
fontSize: 14,
imageSize: 60
};
case DeviceType.PHONE:
return {
columns: 1,
itemHeight: 100,
fontSize: 16,
imageSize: 80
};
case DeviceType.TABLET:
return {
columns: 2,
itemHeight: 120,
fontSize: 18,
imageSize: 100
};
default:
return {
columns: 1,
itemHeight: 100,
fontSize: 16,
imageSize: 80
};
}
}
}
export enum DeviceType {
WEARABLE = 'wearable',
PHONE = 'phone',
TABLET = 'tablet'
}
export interface LayoutConfig {
columns: number;
itemHeight: number;
fontSize: number;
imageSize: number;
}
五、注意事項與最佳實踐
5.1 數據同步衝突解決
最後寫入優先策略:當多個設備同時修改同一商品時,採用時間戳機制,最後修改的操作覆蓋之前的操作。
設備優先級:手機作為主要操作設備,其修改優先級高於手錶等輔助設備。
// 衝突解決示例
async resolveConflict(productId: string, localProduct: Product, remoteProduct: Product): Promise<Product> {
const localTimestamp = localProduct.timestamp || 0;
const remoteTimestamp = remoteProduct.timestamp || 0;
// 時間戳更新的優先
if (remoteTimestamp > localTimestamp) {
return remoteProduct;
} else {
return localProduct;
}
}
5.2 性能優化建議
- 數據分頁加載:購物車商品過多時,採用分頁加載策略。
- 圖片懶加載:商品圖片在進入可視區域時再加載。
- 防抖處理:商品數量修改操作添加防抖,避免頻繁同步。
- 本地緩存:在分佈式存儲基礎上添加本地緩存,提升讀取速度。
5.3 常見問題及解決方案
問題1:設備連接不穩定
- 解決方案:實現重連機制,在網絡恢復時自動重新同步。
問題2:同步延遲
- 解決方案:添加本地狀態提示,明確告知用户數據同步狀態。
問題3:權限申請失敗
- 解決方案:提供友好的權限引導界面,指導用户手動開啓權限。
第三部分:總結
核心要點回顧
- 分佈式架構是基礎:通過KV Store實現多設備數據實時同步,採用發佈-訂閲模式確保數據一致性。
- 權限配置是關鍵:必須正確聲明
DISTRIBUTED_DATASYNC和ACCESS_SERVICE_DM權限,並在運行時動態申請。 - 設備適配是體驗保障:根據設備類型採用不同的UI佈局和交互方式,確保在各設備上都有良好體驗。
- 衝突解決是穩定性核心:實現合理的數據衝突解決策略,確保在多設備同時操作時的數據正確性。
行動建議
- 開發階段:使用DevEco Studio的分佈式模擬器進行多設備聯調,重點測試網絡切換、數據衝突等邊界場景。
- 測試階段:覆蓋單設備異常、網絡波動、權限拒絕等異常情況,確保應用健壯性。
- 上線前:在多款真機設備上進行完整流程測試,驗證不同設備型號的兼容性。
下篇預告
下一篇我們將深入探討渲染性能優化——讓應用如絲般順滑。你將學習到HarmonyOS應用渲染原理、常見性能瓶頸識別方法、以及列表渲染優化、內存管理、動畫優化等實用技巧。通過性能優化,你的應用將實現更流暢的用户體驗,為後續的綜合實戰項目打下堅實基礎。