HarmonyOS开发中离线优先策略:本地数据优先
【摘要】 离线优先策略:本地数据优先网络不可怕,没网才可怕——离线优先让应用永不掉线 一、背景与动机:为什么需要离线优先?想象一下这个场景:用户在地铁上看你的 App,正刷得开心,突然进隧道了——没网了。传统做法:页面显示"网络错误",用户只能干瞪眼。离线优先做法:页面继续显示内容,只是右上角有个小图标提示"离线模式"。等出了隧道有网了,数据自动更新。这就是**离线优先(Offline-First)...
离线优先策略:本地数据优先
网络不可怕,没网才可怕——离线优先让应用永不掉线
一、背景与动机:为什么需要离线优先?
想象一下这个场景:
用户在地铁上看你的 App,正刷得开心,突然进隧道了——没网了。
传统做法:页面显示"网络错误",用户只能干瞪眼。
离线优先做法:页面继续显示内容,只是右上角有个小图标提示"离线模式"。等出了隧道有网了,数据自动更新。
这就是**离线优先(Offline-First)**的核心理念:把离线当作正常状态,而不是异常状态。
离线优先要解决什么问题?
- 数据可用性:没网也能看数据
- 操作连续性:没网也能操作,有网后同步
- 体验一致性:在线离线体验无缝切换
- 数据一致性:本地数据与服务器最终一致
离线优先 vs 传统模式
flowchart TB
subgraph 传统模式
A1[用户操作] --> B1{有网络?}
B1 -->|是| C1[请求服务器]
B1 -->|否| D1[显示错误]
C1 --> E1[显示数据]
end
subgraph 离线优先
A2[用户操作] --> B2[读取本地数据]
B2 --> C2[立即显示]
C2 --> D2{有网络?}
D2 -->|是| E2[请求服务器]
D2 -->|否| F2[标记离线模式]
E2 --> G2[更新本地数据]
G2 --> H2[刷新显示]
end
classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
classDef info fill:#7ED321,stroke:#5BA318,color:#fff
class A1,A2,B2,C2 primary
class B1,D2 info
class C1,E1,E2,G2,H2 warning
class D1,F2 error
二、核心原理:离线优先架构
离线优先不是"加个缓存"就完事,而是一套完整的架构:
flowchart TB
subgraph 数据层
A[本地数据库<br/>RDB/Preferences]
B[操作队列<br/>待同步操作]
C[数据版本<br/>冲突检测]
end
subgraph 同步层
D[同步管理器<br/>协调同步]
E[冲突解决器<br/>处理冲突]
F[网络检测器<br/>监听网络状态]
end
subgraph 业务层
G[数据访问接口<br/>统一读写]
H[离线操作队列<br/>暂存操作]
I[状态同步<br/>UI更新]
end
G --> A
G --> B
G --> C
D --> A
D --> E
D --> F
H --> B
I --> G
F --> D
D --> H
classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
classDef info fill:#7ED321,stroke:#5BA318,color:#fff
class A,B,C primary
class D,E,F warning
class G,H,I info
三层协作:
- 数据层:本地存储、版本控制、操作队列
- 同步层:网络检测、数据同步、冲突解决
- 业务层:数据访问、离线操作、UI更新
三、代码实战:完整离线优先实现
示例1:网络状态检测器
// network/offline/NetworkDetector.ets
import connection from '@ohos.net.connection';
/**
* 网络状态检测器
* 实时监听网络状态变化
*/
export class NetworkDetector {
private isConnected: boolean = false;
private connectionType: connection.NetConnectionType = connection.NetConnectionType.UNKNOWN;
private listeners: Array<(connected: boolean) => void> = [];
private netHandle: connection.NetHandle | null = null;
/**
* 初始化网络检测
*/
async init(): Promise<void> {
try {
// 获取当前网络状态
this.netHandle = await connection.getDefaultNet();
const capabilities = await connection.getNetCapabilities(this.netHandle);
this.isConnected = capabilities !== null;
this.connectionType = this.getConnectionType(capabilities);
console.info(`[NetworkDetector] 初始化完成, 已连接: ${this.isConnected}, 类型: ${this.connectionType}`);
// 注册网络状态变化监听
connection.on('netConnectionChange', (data) => {
this.handleNetworkChange(data);
});
} catch (error) {
console.error('[NetworkDetector] 初始化失败:', error);
this.isConnected = false;
}
}
/**
* 处理网络状态变化
* @private
*/
private handleNetworkChange(data: connection.NetConnectionChangeInfo): void {
const wasConnected = this.isConnected;
this.isConnected = data.netHandle !== null;
console.info(`[NetworkDetector] 网络状态变化: ${wasConnected} -> ${this.isConnected}`);
// 通知所有监听器
if (wasConnected !== this.isConnected) {
this.listeners.forEach(listener => {
try {
listener(this.isConnected);
} catch (error) {
console.error('[NetworkDetector] 监听器执行失败:', error);
}
});
}
}
/**
* 获取连接类型
* @private
*/
private getConnectionType(capabilities: connection.NetCapabilities): connection.NetConnectionType {
if (capabilities?.bearerTypes?.includes(connection.NetBearType.BEARER_WIFI)) {
return connection.NetConnectionType.WIFI;
}
if (capabilities?.bearerTypes?.includes(connection.NetBearType.BEARER_CELLULAR)) {
return connection.NetConnectionType.MOBILE;
}
return connection.NetConnectionType.UNKNOWN;
}
/**
* 添加网络状态监听器
*/
addListener(listener: (connected: boolean) => void): void {
this.listeners.push(listener);
}
/**
* 移除网络状态监听器
*/
removeListener(listener: (connected: boolean) => void): void {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
/**
* 当前是否在线
*/
get isOnline(): boolean {
return this.isConnected;
}
/**
* 当前是否离线
*/
get isOffline(): boolean {
return !this.isConnected;
}
/**
* 获取连接类型
*/
get type(): connection.NetConnectionType {
return this.connectionType;
}
/**
* 销毁检测器
*/
destroy(): void {
connection.off('netConnectionChange');
this.listeners = [];
}
}
示例2:离线操作队列
// network/offline/OperationQueue.ets
import { preferences } from '@kit.ArkData';
/**
* 操作类型枚举
*/
export enum OperationType {
CREATE = 'create',
UPDATE = 'update',
DELETE = 'delete'
}
/**
* 离线操作结构
*/
export interface OfflineOperation {
/** 操作ID */
id: string;
/** 操作类型 */
type: OperationType;
/** 资源类型(如 'product', 'order') */
resource: string;
/** 资源ID */
resourceId: string;
/** 操作数据 */
data: any;
/** 创建时间 */
timestamp: number;
/** 重试次数 */
retryCount: number;
/** 状态 */
status: 'pending' | 'syncing' | 'failed' | 'completed';
}
/**
* 离线操作队列
* 管理待同步的操作
*/
export class OperationQueue {
private store: preferences.Preferences | null = null;
private queue: OfflineOperation[] = [];
private maxRetry: number = 3;
/**
* 初始化操作队列
*/
async init(): Promise<void> {
this.store = await preferences.getPreferences('offline_queue');
// 加载持久化的操作
const json = await this.store.get('queue', '[]') as string;
this.queue = JSON.parse(json);
console.info(`[OperationQueue] 初始化完成, 待同步操作: ${this.queue.length}`);
}
/**
* 添加操作到队列
*/
async add(
type: OperationType,
resource: string,
resourceId: string,
data: any
): Promise<OfflineOperation> {
const operation: OfflineOperation = {
id: this.generateId(),
type,
resource,
resourceId,
data,
timestamp: Date.now(),
retryCount: 0,
status: 'pending'
};
this.queue.push(operation);
await this.persist();
console.info(`[OperationQueue] 添加操作: ${operation.id}, 类型: ${type}, 资源: ${resource}`);
return operation;
}
/**
* 获取待同步操作
*/
getPendingOperations(): OfflineOperation[] {
return this.queue.filter(op => op.status === 'pending');
}
/**
* 获取指定资源的所有待同步操作
*/
getOperationsByResource(resource: string): OfflineOperation[] {
return this.queue.filter(op => op.resource === resource && op.status === 'pending');
}
/**
* 标记操作为同步中
*/
async markSyncing(operationId: string): Promise<void> {
const operation = this.queue.find(op => op.id === operationId);
if (operation) {
operation.status = 'syncing';
await this.persist();
}
}
/**
* 标记操作为完成
*/
async markCompleted(operationId: string): Promise<void> {
const index = this.queue.findIndex(op => op.id === operationId);
if (index > -1) {
this.queue.splice(index, 1);
await this.persist();
console.info(`[OperationQueue] 操作完成: ${operationId}`);
}
}
/**
* 标记操作为失败
*/
async markFailed(operationId: string, error: any): Promise<void> {
const operation = this.queue.find(op => op.id === operationId);
if (operation) {
operation.retryCount++;
if (operation.retryCount >= this.maxRetry) {
// 达到最大重试次数,标记为失败
operation.status = 'failed';
console.error(`[OperationQueue] 操作失败(已达最大重试): ${operationId}`, error);
} else {
// 重置为待同步,等待下次重试
operation.status = 'pending';
console.warn(`[OperationQueue] 操作失败(将重试 ${operation.retryCount}/${this.maxRetry}): ${operationId}`);
}
await this.persist();
}
}
/**
* 清空队列
*/
async clear(): Promise<void> {
this.queue = [];
await this.persist();
}
/**
* 获取队列统计信息
*/
getStats() {
return {
total: this.queue.length,
pending: this.queue.filter(op => op.status === 'pending').length,
syncing: this.queue.filter(op => op.status === 'syncing').length,
failed: this.queue.filter(op => op.status === 'failed').length
};
}
/**
* 持久化队列
* @private
*/
private async persist(): Promise<void> {
if (this.store) {
const json = JSON.stringify(this.queue);
await this.store.put('queue', json);
await this.store.flush();
}
}
/**
* 生成操作ID
* @private
*/
private generateId(): string {
return `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
示例3:同步管理器
// network/offline/SyncManager.ets
import { NetworkDetector } from './NetworkDetector';
import { OperationQueue, OfflineOperation, OperationType } from './OperationQueue';
import { HttpClient } from '../HttpClient';
/**
* 同步配置
*/
export interface SyncConfig {
/** 自动同步间隔(毫秒) */
autoSyncInterval?: number;
/** 最大重试次数 */
maxRetry?: number;
/** 同步处理器映射 */
handlers: Map<string, SyncHandler>;
}
/**
* 同步处理器接口
*/
export interface SyncHandler {
/** 处理创建操作 */
handleCreate?(data: any): Promise<any>;
/** 处理更新操作 */
handleUpdate?(resourceId: string, data: any): Promise<any>;
/** 处理删除操作 */
handleDelete?(resourceId: string): Promise<void>;
}
/**
* 同步管理器
* 协调离线操作的同步
*/
export class SyncManager {
private networkDetector: NetworkDetector;
private operationQueue: OperationQueue;
private config: SyncConfig;
private syncing: boolean = false;
private syncTimer: number = -1;
constructor(
networkDetector: NetworkDetector,
operationQueue: OperationQueue,
config: SyncConfig
) {
this.networkDetector = networkDetector;
this.operationQueue = operationQueue;
this.config = config;
}
/**
* 初始化同步管理器
*/
async init(): Promise<void> {
// 监听网络状态变化
this.networkDetector.addListener((connected) => {
if (connected) {
console.info('[SyncManager] 网络恢复,开始同步');
this.sync();
}
});
// 启动自动同步
if (this.config.autoSyncInterval) {
this.startAutoSync(this.config.autoSyncInterval);
}
// 如果当前在线,立即同步
if (this.networkDetector.isOnline) {
await this.sync();
}
console.info('[SyncManager] 初始化完成');
}
/**
* 启动自动同步
*/
startAutoSync(interval: number): void {
if (this.syncTimer !== -1) {
clearInterval(this.syncTimer);
}
this.syncTimer = setInterval(() => {
if (this.networkDetector.isOnline && !this.syncing) {
this.sync();
}
}, interval);
console.info(`[SyncManager] 自动同步已启动,间隔: ${interval}ms`);
}
/**
* 停止自动同步
*/
stopAutoSync(): void {
if (this.syncTimer !== -1) {
clearInterval(this.syncTimer);
this.syncTimer = -1;
console.info('[SyncManager] 自动同步已停止');
}
}
/**
* 执行同步
*/
async sync(): Promise<void> {
// 检查是否在线
if (this.networkDetector.isOffline) {
console.warn('[SyncManager] 离线状态,跳过同步');
return;
}
// 检查是否正在同步
if (this.syncing) {
console.warn('[SyncManager] 正在同步中,跳过');
return;
}
this.syncing = true;
console.info('[SyncManager] 开始同步...');
try {
// 获取待同步操作
const operations = this.operationQueue.getPendingOperations();
if (operations.length === 0) {
console.info('[SyncManager] 无待同步操作');
return;
}
console.info(`[SyncManager] 待同步操作数: ${operations.length}`);
// 逐个执行同步
for (const operation of operations) {
await this.syncOperation(operation);
}
console.info('[SyncManager] 同步完成');
} catch (error) {
console.error('[SyncManager] 同步失败:', error);
} finally {
this.syncing = false;
}
}
/**
* 同步单个操作
* @private
*/
private async syncOperation(operation: OfflineOperation): Promise<void> {
// 标记为同步中
await this.operationQueue.markSyncing(operation.id);
try {
// 获取对应的处理器
const handler = this.config.handlers.get(operation.resource);
if (!handler) {
console.error(`[SyncManager] 未找到处理器: ${operation.resource}`);
await this.operationQueue.markFailed(operation.id, 'Handler not found');
return;
}
// 执行同步
let result: any;
switch (operation.type) {
case OperationType.CREATE:
result = await handler.handleCreate?.(operation.data);
break;
case OperationType.UPDATE:
result = await handler.handleUpdate?.(operation.resourceId, operation.data);
break;
case OperationType.DELETE:
await handler.handleDelete?.(operation.resourceId);
break;
}
// 标记为完成
await this.operationQueue.markCompleted(operation.id);
console.info(`[SyncManager] 操作同步成功: ${operation.id}`);
} catch (error) {
// 标记为失败
await this.operationQueue.markFailed(operation.id, error);
}
}
/**
* 获取同步状态
*/
getStatus() {
return {
isOnline: this.networkDetector.isOnline,
syncing: this.syncing,
queueStats: this.operationQueue.getStats()
};
}
}
示例4:离线数据访问层
// network/offline/OfflineDataAccess.ets
import relationalStore from '@ohos.data.relationalStore';
import { NetworkDetector } from './NetworkDetector';
import { OperationQueue, OperationType } from './OperationQueue';
/**
* 离线数据访问层
* 提供统一的数据读写接口,自动处理离线场景
*/
export class OfflineDataAccess<T extends { id: string }> {
private rdbStore: relationalStore.RdbStore | null = null;
private tableName: string;
private networkDetector: NetworkDetector;
private operationQueue: OperationQueue;
private apiEndpoint: string;
constructor(
tableName: string,
apiEndpoint: string,
networkDetector: NetworkDetector,
operationQueue: OperationQueue
) {
this.tableName = tableName;
this.apiEndpoint = apiEndpoint;
this.networkDetector = networkDetector;
this.operationQueue = operationQueue;
}
/**
* 初始化数据访问层
*/
async init(): Promise<void> {
// 创建RDB存储
const config: relationalStore.StoreConfig = {
name: 'offline_data.db',
securityLevel: relationalStore.SecurityLevel.S1
};
this.rdbStore = await relationalStore.getRdbStore(config);
// 创建表
const sql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id TEXT PRIMARY KEY,
data TEXT NOT NULL,
version INTEGER DEFAULT 0,
updated_at INTEGER NOT NULL,
is_local INTEGER DEFAULT 0
)
`;
await this.rdbStore.executeSql(sql);
console.info(`[OfflineDataAccess] 初始化完成: ${this.tableName}`);
}
/**
* 查询单条数据
* 离线优先:先查本地,有网时后台更新
*/
async get(id: string): Promise<T | null> {
// 1. 查询本地数据
const local = await this.getLocal(id);
// 2. 如果在线,后台更新
if (this.networkDetector.isOnline) {
this.fetchFromServer(id).then(serverData => {
if (serverData) {
this.saveLocal(serverData, false);
}
}).catch(error => {
console.error('[OfflineDataAccess] 后台更新失败:', error);
});
}
// 3. 返回本地数据
return local;
}
/**
* 查询所有数据
*/
async getAll(): Promise<T[]> {
// 查询本地数据
const locals = await this.getAllLocal();
// 如果在线,后台更新
if (this.networkDetector.isOnline) {
this.fetchAllFromServer().then(serverData => {
this.saveAllLocal(serverData, false);
}).catch(error => {
console.error('[OfflineDataAccess] 后台更新失败:', error);
});
}
return locals;
}
/**
* 创建数据
* 离线优先:先存本地,有网时同步
*/
async create(data: T): Promise<T> {
// 生成临时ID(如果是离线创建)
if (!data.id || this.networkDetector.isOffline) {
data.id = this.generateTempId();
}
// 保存到本地
await this.saveLocal(data, true);
// 添加到操作队列
await this.operationQueue.add(
OperationType.CREATE,
this.tableName,
data.id,
data
);
// 如果在线,立即同步
if (this.networkDetector.isOnline) {
// 同步会在SyncManager中自动执行
}
return data;
}
/**
* 更新数据
*/
async update(id: string, data: Partial<T>): Promise<T | null> {
// 获取现有数据
const existing = await this.getLocal(id);
if (!existing) {
return null;
}
// 合并数据
const updated = { ...existing, ...data, updatedAt: Date.now() };
// 保存到本地
await this.saveLocal(updated, true);
// 添加到操作队列
await this.operationQueue.add(
OperationType.UPDATE,
this.tableName,
id,
updated
);
return updated;
}
/**
* 删除数据
*/
async delete(id: string): Promise<void> {
// 从本地删除
await this.deleteLocal(id);
// 添加到操作队列
await this.operationQueue.add(
OperationType.DELETE,
this.tableName,
id,
null
);
}
/**
* 从本地获取数据
* @private
*/
private async getLocal(id: string): Promise<T | null> {
if (!this.rdbStore) return null;
const predicates = new relationalStore.RdbPredicates(this.tableName);
predicates.equalTo('id', id);
const resultSet = await this.rdbStore.query(predicates);
if (resultSet.goToFirstRow()) {
const json = resultSet.getString(resultSet.getColumnIndex('data'));
resultSet.close();
return JSON.parse(json);
}
resultSet.close();
return null;
}
/**
* 从本地获取所有数据
* @private
*/
private async getAllLocal(): Promise<T[]> {
if (!this.rdbStore) return [];
const predicates = new relationalStore.RdbPredicates(this.tableName);
const resultSet = await this.rdbStore.query(predicates);
const items: T[] = [];
while (resultSet.goToNextRow()) {
const json = resultSet.getString(resultSet.getColumnIndex('data'));
items.push(JSON.parse(json));
}
resultSet.close();
return items;
}
/**
* 保存到本地
* @private
*/
private async saveLocal(data: T, isLocal: boolean): Promise<void> {
if (!this.rdbStore) return;
const valueBucket: relationalStore.ValuesBucket = {
id: data.id,
data: JSON.stringify(data),
version: 0,
updated_at: Date.now(),
is_local: isLocal ? 1 : 0
};
// 使用REPLACE语义(存在则更新,不存在则插入)
await this.rdbStore.insert(this.tableName, valueBucket, relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
}
/**
* 批量保存到本地
* @private
*/
private async saveAllLocal(data: T[], isLocal: boolean): Promise<void> {
for (const item of data) {
await this.saveLocal(item, isLocal);
}
}
/**
* 从本地删除
* @private
*/
private async deleteLocal(id: string): Promise<void> {
if (!this.rdbStore) return;
const predicates = new relationalStore.RdbPredicates(this.tableName);
predicates.equalTo('id', id);
await this.rdbStore.delete(predicates);
}
/**
* 从服务器获取数据
* @private
*/
private async fetchFromServer(id: string): Promise<T | null> {
// 这里应该调用实际的API
// 简化示例,返回null
return null;
}
/**
* 从服务器获取所有数据
* @private
*/
private async fetchAllFromServer(): Promise<T[]> {
// 这里应该调用实际的API
// 简化示例,返回空数组
return [];
}
/**
* 生成临时ID
* @private
*/
private generateTempId(): string {
return `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
示例5:页面中的离线优先实践
// pages/ProductListPage.ets
import { NetworkDetector } from '../network/offline/NetworkDetector';
import { OfflineDataAccess } from '../network/offline/OfflineDataAccess';
interface Product {
id: string;
name: string;
price: number;
image: string;
}
@Entry
@Component
struct ProductListPage {
@State products: Product[] = [];
@State loading: boolean = true;
@State isOffline: boolean = false;
@State pendingCount: number = 0;
private networkDetector: NetworkDetector = new NetworkDetector();
private productAccess: OfflineDataAccess<Product>;
aboutToAppear() {
this.initOffline();
}
/**
* 初始化离线功能
*/
async initOffline() {
// 初始化网络检测
await this.networkDetector.init();
// 监听网络状态
this.networkDetector.addListener((connected) => {
this.isOffline = !connected;
if (connected) {
// 网络恢复,刷新数据
this.loadProducts();
}
});
// 初始化离线数据访问
this.productAccess = new OfflineDataAccess(
'products',
'/api/products',
this.networkDetector,
new OperationQueue()
);
await this.productAccess.init();
// 加载数据
this.isOffline = this.networkDetector.isOffline;
await this.loadProducts();
}
/**
* 加载商品列表
*/
async loadProducts() {
this.loading = true;
try {
// 离线优先:从本地读取
const products = await this.productAccess.getAll();
this.products = products;
} catch (error) {
console.error('加载失败:', error);
} finally {
this.loading = false;
}
}
/**
* 添加商品(支持离线)
*/
async addProduct() {
const newProduct: Product = {
id: '',
name: '新商品',
price: 99.9,
image: ''
};
try {
// 离线优先:先存本地
const created = await this.productAccess.create(newProduct);
// 更新UI
this.products = [...this.products, created];
// 提示用户
if (this.isOffline) {
promptAction.showToast({
message: '已保存到本地,联网后自动同步',
duration: 2000
});
}
} catch (error) {
console.error('添加失败:', error);
}
}
build() {
Column() {
// 顶部状态栏
this.statusBar();
// 商品列表
if (this.loading) {
LoadingProgress()
.width(48)
.height(48)
} else if (this.products.length === 0) {
this.emptyState();
} else {
this.productList();
}
// 添加按钮
Button('添加商品')
.type(ButtonType.Capsule)
.backgroundColor('#4A90E2')
.fontColor(Color.White)
.width('90%')
.margin({ top: 16, bottom: 16 })
.onClick(() => {
this.addProduct();
})
}
.width('100%')
.height('100%')
}
/**
* 状态栏
*/
@Builder
statusBar() {
Row() {
Text('商品列表')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
// 离线指示器
if (this.isOffline) {
Row() {
Image($r('app.media.ic_offline'))
.width(16)
.height(16)
Text('离线')
.fontSize(12)
.fontColor('#E74C3C')
.margin({ left: 4 })
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#FFEBEE')
.borderRadius(12)
}
// 待同步指示器
if (this.pendingCount > 0) {
Row() {
Text(`${this.pendingCount}个待同步`)
.fontSize(12)
.fontColor('#F5A623')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#FFF8E1')
.borderRadius(12)
.margin({ left: 8 })
}
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
}
/**
* 空状态
*/
@Builder
emptyState() {
Column() {
Image($r('app.media.ic_empty'))
.width(100)
.height(100)
Text('暂无商品')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
if (this.isOffline) {
Text('联网后可同步数据')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
/**
* 商品列表
*/
@Builder
productList() {
List() {
ForEach(this.products, (product: Product) => {
ListItem() {
Row() {
Image(product.image || $r('app.media.ic_product'))
.width(60)
.height(60)
.borderRadius(8)
Column() {
Text(product.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`¥${product.price}`)
.fontSize(14)
.fontColor('#E74C3C')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
}
.margin({ bottom: 8 })
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
}
四、踩坑与注意事项
坑1:临时ID冲突
问题:离线创建的数据用临时ID,同步后服务器返回真实ID,如何处理?
解决:维护ID映射表:
// ID映射表
const idMapping: Map<string, string> = new Map();
// 同步成功后更新映射
function onSyncSuccess(tempId: string, realId: string) {
idMapping.set(tempId, realId);
// 更新所有引用了tempId的数据
}
坑2:数据冲突
问题:离线修改了数据A,别人在线也修改了数据A,同步时冲突。
解决:使用版本号 + 冲突解决策略:
// 版本号检测
if (local.version < server.version) {
// 服务器版本更新,需要冲突解决
const resolved = await conflictResolver.resolve(local, server);
return resolved;
}
坑3:大量数据同步卡顿
问题:离线积累了很多操作,一联网就全部同步,卡死。
解决:分批同步 + 限流:
// 每次最多同步10个操作
const batchSize = 10;
const operations = this.queue.slice(0, batchSize);
// 同步间隔
await delay(100); // 每个操作间隔100ms
坑4:网络状态误判
问题:网络检测不准确,明明有网却判断为离线。
解决:心跳检测 + 实际请求验证:
// 定期心跳检测
setInterval(async () => {
try {
await fetch('https://api.example.com/ping', { timeout: 5000 });
this.isConnected = true;
} catch {
this.isConnected = false;
}
}, 30000); // 每30秒检测一次
五、HarmonyOS 6 适配要点
1. RDB API 增强
// HarmonyOS 6 支持更强大的数据库操作
import { relationalStore } from '@kit.ArkData';
// 支持事务
await rdbStore.beginTransaction();
try {
await rdbStore.insert('table1', data1);
await rdbStore.insert('table2', data2);
await rdbStore.commit();
} catch (error) {
await rdbStore.rollBack();
}
// 支持批量操作
await rdbStore.batchInsert('table', [data1, data2, data3]);
2. 网络检测增强
// HarmonyOS 6 提供更详细的网络信息
const netConnection = connection.createNetConnection();
// 监听网络能力变化
netConnection.on('netCapabilitiesChange', (data) => {
console.log('网络能力:', data.netCapabilities);
// 包含:带宽、延迟、是否计费等信息
});
3. 后台任务支持
// HarmonyOS 6 支持后台同步
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
// 申请后台任务
const id = await backgroundTaskManager.requestSuspendDelay('sync', () => {
console.log('后台同步任务结束');
});
// 执行同步
await syncManager.sync();
// 取消后台任务
backgroundTaskManager.cancelSuspendDelay(id);
六、总结
离线优先是现代应用的"标配",让用户无感知地享受在线离线切换:
| 层次 | 职责 | 关键技术 |
|---|---|---|
| 数据层 | 本地存储 | RDB、Preferences |
| 同步层 | 数据同步 | 操作队列、冲突解决 |
| 业务层 | 数据访问 | 统一接口、状态管理 |
记住几个原则:
- ✅ 本地优先:先读本地,后台更新
- ✅ 操作队列:离线操作持久化
- ✅ 网络检测:实时监听网络状态
- ✅ 冲突解决:版本号 + 策略
- ✅ 用户提示:离线状态要告知用户
下一篇我们深入网络模拟与Mock,看看开发调试的利器。
💡 最佳实践提示:建议在应用启动时初始化离线系统,并在全局状态中维护网络状态,让所有页面都能感知在线/离线切换。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)