# 鸿蒙应用开发实践:网络数据列表构建完整方案
在网络应用开发中,数据列表展示是基础且关键的场景。基于鸿蒙平台的网络数据列表应用开发,需要综合考虑网络请求、数据处理、性能优化等多个方面。本文将提供完整的实战解决方案。
## 架构设计与网络层实现
### 网络请求管理层
```jsx
import { http } from '@ohos/net.http';
import { Preferences } from '@ohos/data.preferences';
import { BusinessError } from '@ohos.base';
// 网络请求配置类
class NetworkConfig {
static BASE_URL = 'https://api.example.com';
static TIMEOUT = 30000;
static RETRY_COUNT = 3;
static RETRY_DELAY = 1000;
}
// 请求拦截器
class RequestInterceptor {
static async intercept(request) {
// 添加公共请求头
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'HarmonyOS-App/1.0',
...request.headers
};
// 添加认证令牌
const token = await this.getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return { ...request, headers };
}
static async getAuthToken() {
try {
const prefs = await Preferences.getPreferences('auth_store');
return await prefs.get('access_token', '');
} catch (error) {
console.warn('获取认证令牌失败:', error);
return null;
}
}
}
// 响应拦截器
class ResponseInterceptor {
static async intercept(response) {
if (response.responseCode === 401) {
// Token过期,尝试刷新
const refreshed = await this.refreshToken();
if (refreshed) {
throw new Error('RETRY_WITH_NEW_TOKEN');
}
}
if (response.responseCode >= 400) {
const error = new Error(this.getErrorMessage(response.responseCode));
error.code = response.responseCode;
error.data = response.result;
throw error;
}
return this.parseResponse(response);
}
static parseResponse(response) {
try {
const result = JSON.parse(response.result);
return {
data: result.data,
pagination: result.pagination,
code: result.code,
message: result.message
};
} catch (error) {
return { data: response.result };
}
}
}
// 网络请求核心类
class NetworkService {
constructor() {
this.http = http.createHttp();
this.cache = new Map();
this.pendingRequests = new Map();
}
async request(config) {
const {
url,
method = 'GET',
data = null,
params = {},
headers = {},
cacheKey = null,
useCache = true,
retry = NetworkConfig.RETRY_COUNT
} = config;
// 构造完整URL
const fullUrl = this.buildUrl(url, params);
const requestKey = `${method}:${fullUrl}`;
// 检查缓存
if (useCache && cacheKey) {
const cached = this.cache.get(cacheKey);
if (cached && !this.isCacheExpired(cached.timestamp)) {
return cached.data;
}
}
// 合并重复请求
if (this.pendingRequests.has(requestKey)) {
return this.pendingRequests.get(requestKey);
}
const requestPromise = this.executeRequest({
url: fullUrl,
method,
data,
headers,
retry
});
this.pendingRequests.set(requestKey, requestPromise);
try {
// 拦截请求
const interceptedRequest = await RequestInterceptor.intercept({
url: fullUrl,
method,
data,
headers
});
const options = {
method: interceptedRequest.method,
header: interceptedRequest.headers,
connectTimeout: NetworkConfig.TIMEOUT,
readTimeout: NetworkConfig.TIMEOUT
};
if (interceptedRequest.data) {
options.extraData = JSON.stringify(interceptedRequest.data);
}
const response = await this.http.request(fullUrl, options);
// 拦截响应
const parsedResponse = await ResponseInterceptor.intercept(response);
// 缓存响应
if (useCache && cacheKey) {
this.cache.set(cacheKey, {
data: parsedResponse,
timestamp: Date.now()
});
}
return parsedResponse;
} catch (error) {
if (error.message === 'RETRY_WITH_NEW_TOKEN') {
// 使用新token重试请求
return this.request({ ...config, retry: retry - 1 });
}
throw error;
} finally {
this.pendingRequests.delete(requestKey);
}
}
async executeRequest(requestConfig, retryCount = 0) {
try {
return await this.makeHttpRequest(requestConfig);
} catch (error) {
if (retryCount < requestConfig.retry) {
await this.delay(NetworkConfig.RETRY_DELAY * Math.pow(2, retryCount));
return this.executeRequest(requestConfig, retryCount + 1);
}
throw error;
}
}
buildUrl(path, params) {
let url = path.startsWith('http') ? path : `${NetworkConfig.BASE_URL}${path}`;
if (Object.keys(params).length > 0) {
const queryString = new URLSearchParams(params).toString();
url += `?${queryString}`;
}
return url;
}
isCacheExpired(timestamp, ttl = 5 * 60 * 1000) {
return Date.now() - timestamp > ttl;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
clearCache(key = null) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
// 使用示例
const networkService = new NetworkService();
export const fetchProductList = async (page = 1, limit = 20) => {
return networkService.request({
url: '/api/products',
method: 'GET',
params: { page, limit },
cacheKey: `products_${page}_${limit}`,
useCache: page === 1 // 仅缓存第一页
});
};
```
## 数据列表组件实现
### 核心列表组件
```jsx
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, FlatList, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
import { NetworkService, fetchProductList } from '../services/NetworkService';
const DataListApp = () => {
const [state, setState] = useState({
data: [],
loading: false,
refreshing: false,
loadingMore: false,
error: null,
page: 1,
hasMore: true,
total: 0
});
const networkServiceRef = useRef(new NetworkService());
const flatListRef = useRef(null);
// 初始化加载
useEffect(() => {
loadInitialData();
}, []);
// 监听网络状态变化
useEffect(() => {
const handleNetworkChange = (status) => {
if (!status.isConnected && state.data.length === 0) {
setState(prev => ({ ...prev, error: '网络连接不可用' }));
}
};
// 鸿蒙网络状态监听
if (typeof ohos !== 'undefined' && ohos.net.connection) {
ohos.net.connection.on('change', handleNetworkChange);
}
return () => {
if (typeof ohos !== 'undefined' && ohos.net.connection) {
ohos.net.connection.off('change', handleNetworkChange);
}
};
}, [state.data.length]);
// 加载初始数据
const loadInitialData = useCallback(async () => {
if (state.loading) return;
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const result = await fetchProductList(1, 20);
setState(prev => ({
...prev,
data: result.data || [],
total: result.pagination?.total || 0,
page: 1,
hasMore: result.pagination?.hasMore || false,
loading: false
}));
// 鸿蒙平台触感反馈
if (typeof ohos !== 'undefined' && ohos.vibrator) {
ohos.vibrator.vibrate({ duration: 10 });
}
} catch (error) {
console.error('加载数据失败:', error);
setState(prev => ({
...prev,
error: error.message || '加载失败',
loading: false
}));
}
}, [state.loading]);
// 下拉刷新
const handleRefresh = useCallback(async () => {
if (state.refreshing) return;
setState(prev => ({ ...prev, refreshing: true, error: null }));
try {
const result = await fetchProductList(1, 20);
setState(prev => ({
...prev,
data: result.data || [],
page: 1,
hasMore: result.pagination?.hasMore || false,
refreshing: false
}));
} catch (error) {
setState(prev => ({
...prev,
error: error.message,
refreshing: false
}));
}
}, [state.refreshing]);
// 加载更多
const handleLoadMore = useCallback(async () => {
if (!state.hasMore || state.loadingMore || state.refreshing) return;
setState(prev => ({ ...prev, loadingMore: true }));
try {
const nextPage = state.page + 1;
const result = await fetchProductList(nextPage, 20);
setState(prev => ({
...prev,
data: [...prev.data, ...(result.data || [])],
page: nextPage,
hasMore: result.pagination?.hasMore || false,
loadingMore: false
}));
} catch (error) {
setState(prev => ({
...prev,
error: error.message,
loadingMore: false
}));
}
}, [state.hasMore, state.loadingMore, state.refreshing, state.page]);
// 渲染列表项
const renderItem = useCallback(({ item, index }) => (
item={item}
index={index}
=> handleItemPress(item)}
/>
), []);
// 空状态
const renderEmpty = useCallback(() => (
{state.error ? (
<>
style={styles.retryButton}
>
>
>
) : (
)}
), [state.error, loadInitialData]);
<"p5.j9k5.org.cn"><"m1.j9k5.org.cn"><"a8.j9k5.org.cn">
// 底部加载更多
const renderFooter = useCallback(() => {
if (!state.hasMore && state.data.length > 0) {
return (
);
}
if (state.loadingMore) {
return (
);
}
return null;
}, [state.hasMore, state.loadingMore, state.data.length]);
// 错误边界
if (state.error && state.data.length === 0) {
return (
style={styles.errorButton}
>
>
);
}
return (
{/* 顶部统计信息 */}
{state.total > 0 && (
共 {state.total} 条数据
)}
{/* 主列表 */}
ref={flatListRef}
data={state.data}
renderItem={renderItem}
keyExtractor={item => `item_${item.id}`}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
refreshing={state.refreshing}
>
>
>
scrollEventThrottle={16}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
contentContainerStyle={state.data.length === 0 ? styles.emptyContent : null}
/>
{/* 全局加载指示器 */}
{state.loading && (
)}
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5'
},
header: {
padding: 12,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0'
},
headerText: {
fontSize: 14,
color: '#666666'
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60
},
emptyText: {
fontSize: 16,
color: '#999999',
textAlign: 'center',
marginBottom: 12
},
retryButton: {
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: '#007AFF',
borderRadius: 6
},
retryButtonText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '500'
},
footer: {
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row'
},
footerText: {
fontSize: 14,
color: '#666666',
marginLeft: 8
},
errorContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F5F5F5'
},
errorText: {
fontSize: 18,
color: '#FF3B30',
marginBottom: 16
},
errorButton: {
paddingHorizontal: 24,
paddingVertical: 12,
backgroundColor: '#007AFF',
borderRadius: 8
},
errorButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600'
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
},
emptyContent: {
flexGrow: 1
}
});
export default DataListApp;
```
## 数据缓存与离线支持
### 本地数据存储
```jsx
import { Preferences } from '@ohos/data.preferences';
// 数据存储管理器
class DataStorage {
static instance = null;
static getInstance() {
if (!DataStorage.instance) {
DataStorage.instance = new DataStorage();
}
return DataStorage.instance;
}
constructor() {
this.stores = new Map();
}
async getStore(name) {
if (!this.stores.has(name)) {
try {
const store = await Preferences.getPreferences(name);
this.stores.set(name, store);
return store;
} catch (error) {
console.error(`创建存储 ${name} 失败:`, error);
throw error;
}
}
return this.stores.get(name);
}
// 存储列表数据
async saveListData(key, data, options = {}) {
try {
const store = await this.getStore('list_cache');
const cacheData = {
data,
timestamp: Date.now(),
...options
};
await store.put(key, JSON.stringify(cacheData));
await store.flush();
console.log(`数据已缓存: ${key}`);
} catch (error) {
console.error(`保存缓存失败 ${key}:`, error);
}
}
// 获取缓存的列表数据
async getListData(key, maxAge = 5 * 60 * 1000) {
try {
const store = await this.getStore('list_cache');
const cached = await store.get(key, '');
if (!cached) return null;
const cacheData = JSON.parse(cached);
const isExpired = Date.now() - cacheData.timestamp > maxAge;
if (isExpired) {
await this.remove(key);
return null;
}
return cacheData.data;
} catch (error) {
console.error(`读取缓存失败 ${key}:`, error);
return null;
}
}
// 清理过期数据
async cleanupExpiredData() {
try {
const store = await this.getStore('list_cache');
const allKeys = await store.getAllKeys();
const now = Date.now();
for (const key of allKeys) {
const value = await store.get(key, '');
if (value) {
const cacheData = JSON.parse(value);
if (now - cacheData.timestamp > 24 * 60 * 60 * 1000) {
await store.delete(key);
}
}
}
await store.flush();
} catch (error) {
console.error('清理过期数据失败:', error);
}
}
}
// 离线支持的数据获取器
class OfflineDataFetcher {
constructor(storage) {
this.storage = storage || DataStorage.getInstance();
}
async fetchWithOfflineSupport(url, params, options = {}) {
const cacheKey = this.generateCacheKey(url, params);
// 首先尝试从缓存获取
if (options.useCache !== false) {
const cachedData = await this.storage.getListData(cacheKey);
if (cachedData) {
console.log('使用缓存数据:', cacheKey);
return {
data: cachedData,
fromCache: true,
timestamp: Date.now()
};
}
}
try {
// 从网络获取
const networkData = await this.fetchFromNetwork(url, params);
// 存储到缓存
if (options.shouldCache !== false) {
await this.storage.saveListData(cacheKey, networkData, {
url,
params,
cachedAt: new Date().toISOString()
});
}
return {
data: networkData,
fromCache: false,
timestamp: Date.now()
};
} catch (error) {
// 网络失败时,如果允许使用过期的缓存,则返回缓存
if (options.allowStaleCache) {
const staleData = await this.storage.getListData(cacheKey, 24 * 60 * 60 * 1000);
if (staleData) {
console.warn('网络失败,使用过期缓存:', cacheKey);
return {
data: staleData,
fromCache: true,
isStale: true,
timestamp: Date.now()
};
}
}
<"d0.j9k5.org.cn"><"h4.j9k5.org.cn"><"v6.j9k5.org.cn">
throw error;
}
}
generateCacheKey(url, params) {
const paramString = JSON.stringify(params);
return `${url}_${paramString}`.replace(/[^a-zA-Z0-9]/g, '_');
}
}
// 在列表组件中使用
const OfflineSupportedList = () => {
const [data, setData] = useState([]);
const [isOffline, setIsOffline] = useState(false);
const fetcherRef = useRef(new OfflineDataFetcher());
const loadData = useCallback(async () => {
try {
const result = await fetcherRef.current.fetchWithOfflineSupport(
'/api/products',
{ page: 1, limit: 20 },
{
useCache: true,
shouldCache: true,
allowStaleCache: true
}
);
setData(result.data);
setIsOffline(result.fromCache);
if (result.fromCache) {
// 在后台更新数据
updateDataInBackground();
}
} catch (error) {
console.error('加载数据失败:', error);
}
}, []);
const updateDataInBackground = async () => {
try {
const freshData = await fetchProductList(1, 20);
await fetcherRef.current.storage.saveListData(
fetcherRef.current.generateCacheKey('/api/products', { page: 1, limit: 20 }),
freshData
);
// 如果当前正在显示缓存数据,则静默更新
setData(freshData);
setIsOffline(false);
} catch (error) {
console.warn('后台更新失败:', error);
}
};
return (
{isOffline && (
)}
data={data}
// ... 其他属性
/>
);
};
```
## 性能优化与监控
### 虚拟化列表优化
```jsx
// 高性能列表项渲染
const OptimizedListItem = React.memo(({ item, onPress }) => {
const [imageLoaded, setImageLoaded] = useState(false);
const handlePress = useCallback(() => {
onPress(item);
}, [item, onPress]);
return (
style={styles.listItem}
>
activeOpacity={0.7}
>
{/* 异步图片加载 */}
{!imageLoaded && (
)}
source={{ uri: item.image }}
style={[
styles.image,
{ opacity: imageLoaded ? 1 : 0 }
]}
=> setImageLoaded(true)}
=> setImageLoaded(false)}
resizeMode="cover"
/>
style={styles.title}
numberOfLines={2}
ellipsizeMode="tail"
>
{item.name}
style={styles.description}
numberOfLines={3}
ellipsizeMode="tail"
>
{item.description}
库存: {item.stock}件
);
}, (prevProps, nextProps) => {
// 自定义比较函数,避免不必要的重新渲染
return prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name &&
prevProps.item.price === nextProps.item.price &&
prevProps.item.stock === nextProps.item.stock;
});
// 性能监控组件
const PerformanceMonitor = ({ componentName }) => {
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const currentTime = performance.now();
const renderTime = currentTime - lastRenderTime.current;
if (renderTime > 16) { // 超过一帧时间
console.warn(`组件 ${componentName} 渲染耗时: ${renderTime.toFixed(2)}ms`);
}
lastRenderTime.current = currentTime;
// 鸿蒙性能监控上报
if (typeof ohos !== 'undefined' && ohos.hiviewdfx.hiAppEvent) {
ohos.hiviewdfx.hiAppEvent.write({
domain: 'performance',
name: 'component_render',
params: {
component: componentName,
renderTime,
renderCount: renderCount.current
}
});
}
});
return null; // 这是一个无UI的监控组件
};
// 在列表项中使用
const MonitoredListItem = (props) => {
return (
<>
>
);
};
```
## 总结与最佳实践
基于鸿蒙平台的网络数据列表应用开发,总结以下关键实践:
1. **网络层优化**:
- 实现请求拦截器和响应拦截器
- 支持请求重试和缓存策略
- 处理网络状态变化
2. **数据管理策略**:
- 实现本地数据缓存
- 支持离线模式运行
- 智能数据同步机制
3. **性能优化重点**:
- 使用虚拟化列表提升性能
- 优化图片加载策略
- 减少不必要的重新渲染
4. **用户体验提升**:
- 提供清晰的状态反馈
- 实现平滑的加载动画
- 支持下拉刷新和上拉加载
5. **鸿蒙特性集成**:
- 利用鸿蒙存储能力
- 集成平台网络API
- 支持分布式数据
通过系统化的架构设计和细致的优化措施,可以在鸿蒙平台上构建出既高效又稳定的网络数据列表应用。建议在实际开发中根据具体业务需求调整和扩展这些实现方案。