【开发日志】HarmonyOS ArkTS 网络状态检测
目录
一、权限声明
在 entry/src/main/module.json5 中声明:
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_NETWORK_INFO" }
]
}
}
| 权限 | 用途 |
|---|---|
ohos.permission.INTERNET |
发起 HTTP/HTTPS 请求 |
ohos.permission.GET_NETWORK_INFO |
调用 connection 模块获取网络状态 |
GET_NETWORK_INFO是 normal 级别权限,声明即生效,无需用户手动授权。
二、同步检测:获取当前网络状态
2.1 基础实现
import { connection } from '@kit.NetworkKit';
/**
* 同步检测当前是否有可用互联网连接
* @returns true=有网, false=无网
*/
function isNetworkAvailable(): boolean {
try {
// 1. 获取默认数据网络句柄
const netHandle = connection.getDefaultNetSync();
// 2. netId 为 0 表示没有激活的网络连接
if (netHandle.netId === 0) {
return false;
}
// 3. 获取网络能力描述
const netCapabilities = connection.getNetCapabilitiesSync(netHandle);
if (!netCapabilities) {
return false;
}
// 4. 必须同时满足:能力数组非空 + 包含 INTERNET 标志
return netCapabilities.networkCap != null
&& netCapabilities.networkCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET);
} catch (e) {
return false;
}
}
2.2 注意
为什么 netId === 0 不可靠?
WiFi 已连接但 DHCP 未完成、IPv6 路由未就绪、VPN 协商中、热点刚开启等场景下,netId 可能为 0,但 HTTP 请求实际上可达。仅凭 netId 判断会误杀。
为什么需要校验 NET_CAPABILITY_INTERNET?
netId !== 0 仅表示有网络接口激活(如连上了路由器),但该网络未必能访问互联网(如路由器未拨号、内网隔离)。NET_CAPABILITY_INTERNET 是系统验证互联网可达的标志。
判空守卫是强制要求:
// ✅ 正确
netCapabilities.networkCap != null
&& netCapabilities.networkCap.includes(...)
// ❌ 错误:networkCap 可能为 undefined,直接调用 .includes() 触发 ArkTS 编译错误
netCapabilities.networkCap.includes(...)
2.3 获取更详细的网络信息
function getNetworkInfo(): {
isAvailable: boolean;
type: string; // 'wifi' | 'cellular' | 'ethernet' | 'unknown'
isMetered: boolean; // 是否按流量计费
} {
try {
const netHandle = connection.getDefaultNetSync();
if (netHandle.netId === 0) {
return { isAvailable: false, type: 'unknown', isMetered: false };
}
const caps = connection.getNetCapabilitiesSync(netHandle);
if (!caps || caps.networkCap == null) {
return { isAvailable: false, type: 'unknown', isMetered: false };
}
const netCap = caps.networkCap;
// 判断网络类型
let type = 'unknown';
if (netCap.includes(connection.NetCap.NET_CAPABILITY_WIFI)) {
type = 'wifi';
} else if (netCap.includes(connection.NetCap.NET_CAPABILITY_CELLULAR)) {
type = 'cellular';
} else if (netCap.includes(connection.NetCap.NET_CAPABILITY_ETHERNET)) {
type = 'ethernet';
}
return {
isAvailable: netCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET),
type: type,
isMetered: !netCap.includes(connection.NetCap.NET_CAPABILITY_NOT_METERED),
};
} catch (e) {
return { isAvailable: false, type: 'unknown', isMetered: false };
}
}
判断网络类型的地方可以提取出来成为枚举类型,避免魔法值
2.4 辅助工具:判断是否为 WiFi
function isWifiConnected(): boolean {
try {
const netHandle = connection.getDefaultNetSync();
if (netHandle.netId === 0) return false;
const caps = connection.getNetCapabilitiesSync(netHandle);
return caps?.networkCap != null
&& caps.networkCap.includes(connection.NetCap.NET_CAPABILITY_WIFI);
} catch (e) {
return false;
}
}
function isCellularConnected(): boolean {
try {
const netHandle = connection.getDefaultNetSync();
if (netHandle.netId === 0) return false;
const caps = connection.getNetCapabilitiesSync(netHandle);
return caps?.networkCap != null
&& caps.networkCap.includes(connection.NetCap.NET_CAPABILITY_CELLULAR);
} catch (e) {
return false;
}
}
三、异步监听:实时感知网络变化
3.1 创建监听实例
import { connection } from '@kit.NetworkKit';
// 创建 NetConnection 实例
const netConnection = connection.createNetConnection();
// 注册网络能力变化回调
netConnection.on('netCapabilitiesChange', (data: connection.NetCapabilityInfo) => {
// data.netHandle: 变化的网络句柄
// data.netCap: 变更后的网络能力(可能为 undefined)
});
// 注册网络丢失回调
netConnection.on('netLost', (data: connection.NetHandle) => {
// data: 丢失的网络句柄
});
// 激活监听(必须调用)
netConnection.register((error: BusinessError) => {
if (error) {
console.error('网络监听注册失败:', JSON.stringify(error));
} else {
console.info('网络监听已激活');
}
});
3.2 完整实用的监听实现
import { connection } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
class NetworkMonitor {
private netConnection: connection.NetConnection | null = null;
private isOnline: boolean = false;
private onOnlineCallback?: () => void;
private onOfflineCallback?: () => void;
/**
* 启动网络监听
*/
start(
onOnline?: () => void,
onOffline?: () => void
): void {
this.onOnlineCallback = onOnline;
this.onOfflineCallback = onOffline;
this.netConnection = connection.createNetConnection();
// 网络能力变化 → 检查 Internet 是否可用
this.netConnection.on('netCapabilitiesChange',
(data: connection.NetCapabilityInfo) => {
const wasOnline = this.isOnline;
const nowOnline = this.checkInternetAvailable(data);
if (nowOnline && !wasOnline) {
// 从无网恢复到有网
this.isOnline = true;
hilog.info(0x0000, 'NetworkMonitor', '网络已恢复');
this.onOnlineCallback?.();
} else if (!nowOnline && wasOnline) {
// 从有网掉到无网
this.isOnline = false;
hilog.warn(0x0000, 'NetworkMonitor', '网络已断开');
this.onOfflineCallback?.();
} else {
this.isOnline = nowOnline;
}
});
// 网络丢失(WiFi 断开 / 飞行模式)
this.netConnection.on('netLost', () => {
if (this.isOnline) {
this.isOnline = false;
hilog.warn(0x0000, 'NetworkMonitor', '网络连接丢失');
this.onOfflineCallback?.();
}
});
// 激活
this.netConnection.register((error: BusinessError) => {
if (error) {
hilog.error(0x0000, 'NetworkMonitor',
`注册失败: ${JSON.stringify(error)}`);
}
});
}
/**
* 停止监听(页面销毁时调用)
*/
stop(): void {
if (this.netConnection) {
this.netConnection.unregister(() => {
hilog.info(0x0000, 'NetworkMonitor', '已取消注册');
});
this.netConnection = null;
}
}
/**
* 检查网络能力变更事件中的 Internet 能力
*/
private checkInternetAvailable(
data: connection.NetCapabilityInfo
): boolean {
if (!data.netCap || data.netHandle.netId === 0) {
return false;
}
const netCap = data.netCap as connection.NetCapabilities;
return netCap.networkCap != null
&& netCap.networkCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET);
}
}
3.3 在页面中使用
@Component
export struct MyPage {
private networkMonitor = new NetworkMonitor();
aboutToAppear(): void {
this.networkMonitor.start(
() => {
// 网络恢复回调
this.uploadCachedData();
},
() => {
// 网络断开回调
this.showOfflineBanner();
}
);
}
aboutToDisappear(): void {
this.networkMonitor.stop();
}
}
3.4 常见踩坑
坑1:静态方法 connection.on() 不存在
// ❌ 错误:connection 没有静态 on 方法
connection.on('netCapabilitiesChange', callback);
// ✅ 正确:必须 createNetConnection() 创建实例
const nc = connection.createNetConnection();
nc.on('netCapabilitiesChange', callback);
nc.register();
坑2:类型名 NetCapabilityChangeInfo 不存在
// ❌ 错误:这个类型不存在
(data: connection.NetCapabilityChangeInfo) => {}
// ✅ 正确:类型名是 NetCapabilityInfo
(data: connection.NetCapabilityInfo) => {}
坑3:忘记调用 register()——回调不会触发
// ❌ 只注册回调但不激活
nc.on('netCapabilitiesChange', cb);
// 缺少 nc.register() —— 永远不会收到事件
// ✅ 必须三步:创建 → 注册回调 → 激活
const nc = connection.createNetConnection();
nc.on('netCapabilitiesChange', cb);
nc.register();
坑4:networkCap 可能为 undefined
NetCapabilityInfo 中的 netCap 字段和 NetCapabilities 中的 networkCap 数组都可能为 undefined,调用 .includes() 前必须判空:
// ✅ 安全写法
if (data.netCap && data.netHandle.netId !== 0) {
const netCap = data.netCap as connection.NetCapabilities;
if (netCap.networkCap != null
&& netCap.networkCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET)) {
// 网络可用
}
}
四、请求试探法:绕过 OS API 不可靠问题
4.1 为什么需要请求试探法
connection API 在不同设备、不同 HarmonyOS 版本、不同网络环境(校园网/公司网/热点/VPN/代理)下的行为不完全一致。以下场景中 OS API 可能误判:
| 场景 | OS API 行为 | 实际 HTTP 连通性 |
|---|---|---|
| WiFi 已连但 DHCP 未完成 | netId=0 |
可能可达(静态 IP / IPv6) |
| 需要网页认证的公共 WiFi | 已连接但无 INTERNET 标志 | 网关可达,API 服务器可能在内网 |
| VPN 隧道未完全建立 | netId=0 |
直连可达 |
| 部分鸿蒙设备 | 行为差异 | 不确定 |
4.2 核心原则
OS 网络状态 API 若判断出错,需要考虑真实连通性。
4.3 实现方式
/**
* 尝试提交数据,失败则缓存到本地
* 不依赖 isNetworkAvailable() 做前置拦截
*/
private async submitWithFallback(): Promise<void> {
try {
const result = await this.api.submit(data);
if (result) {
// 成功:正常流程
this.onSuccess(result);
return;
}
// 返回空:请求发出去了但业务失败 → 降级缓存
this.cacheLocally(data);
this.showToast('提交失败,已保存到本地,网络恢复后自动重试');
} catch (err) {
// 网络异常 / 超时 → 降级缓存
this.cacheLocally(data);
this.showToast('提交失败,已保存到本地,网络恢复后自动重试');
}
}
4.4 与 OS API 的结合使用
/**
* 分层策略:OS API 做 UI 弱提示,实际连通性用请求试探
*/
private async smartSubmit(data: SubmitData): Promise<void> {
// 层1:OS API → 仅用于 UI 提示,不拦截请求
if (!this.isNetworkAvailable()) {
this.showWeakBanner('当前网络可能较差');
}
// 层2:直接发请求 → 以实际结果为准
try {
const result = await this.api.submit(data);
if (result) {
this.onSuccess(result);
return;
}
} catch (err) {
// 不做额外处理,继续走降级
}
// 层3:降级缓存
this.cacheLocally(data);
this.showToast('已保存到本地,网络恢复后自动提交');
}
五、离线缓存机制
5.1 使用 Preferences 存储离线数据
import { preferences } from '@kit.ArkData';
const STORE_NAME = 'offline_cache';
interface CachedItem {
id: string;
payload: string; // JSON 序列化的业务数据
timestamp: number; // 缓存时间
retryCount: number; // 已重试次数
}
class OfflineCache {
private store?: preferences.Preferences;
async init(context: Context): Promise<void> {
this.store = await preferences.getPreferences(context, STORE_NAME);
}
/**
* 追加一条缓存
*/
async add(item: CachedItem): Promise<void> {
const list = await this.getAll();
list.push(item);
await this.store!.put('items', JSON.stringify(list));
await this.store!.flush();
}
/**
* 获取所有缓存条目
*/
async getAll(): Promise<CachedItem[]> {
const raw = await this.store!.get('items', '[]') as string;
return JSON.parse(raw);
}
/**
* 删除指定条目(上传成功后调用)
*/
async remove(id: string): Promise<void> {
const list = await this.getAll();
const filtered = list.filter(item => item.id !== id);
await this.store!.put('items', JSON.stringify(filtered));
await this.store!.flush();
}
/**
* 清空所有缓存
*/
async clearAll(): Promise<void> {
await this.store!.put('items', '[]');
await this.store!.flush();
}
/**
* 是否有待上传的缓存
*/
async hasPending(): Promise<boolean> {
const list = await this.getAll();
return list.length > 0;
}
}
5.2 MatDash 项目中的实际实现
// Preferences.ets — CachedGameResult 接口
export interface CachedGameResult {
sessionId: string;
score: number;
totalQuestions: number;
correctQuestions: number;
difficulty: string;
timestamp: number;
}
// 保存
static async saveCachedResult(context: Context, result: CachedGameResult): Promise<void>
// 读取全部
static async getCachedResults(context: Context): Promise<CachedGameResult[]>
// 单条删除
static async removeCachedResult(context: Context, sessionId: string): Promise<void>
// 全部清空
static async clearCachedResults(context: Context): Promise<void>
六、网络恢复后自动上传
6.1 合并监听 + 上传
@Component
export struct Index {
private networkMonitor = new NetworkMonitor();
aboutToAppear(): void {
this.networkMonitor.start(
() => this.onNetworkRecovered(), // 网络恢复
() => this.onNetworkLost() // 网络断开
);
}
aboutToDisappear(): void {
this.networkMonitor.stop();
}
/**
* 网络恢复后:逐条上传缓存数据
*/
private async onNetworkRecovered(): Promise<void> {
// 二次确认(用同步检测做兜底,防止回调误触发)
const netHandle = connection.getDefaultNetSync();
if (netHandle.netId === 0) {
return;
}
const cachedItems = await Preferences.getCachedResults(getContext());
if (cachedItems.length === 0) {
return;
}
hilog.info(DOMAIN, TAG,
`网络恢复,开始上传 ${cachedItems.length} 条缓存数据`);
// 倒序遍历,方便删除
for (let i = cachedItems.length - 1; i >= 0; i--) {
const item = cachedItems[i];
try {
const success = await this.uploadSingle(item);
if (success) {
await Preferences.removeCachedResult(getContext(), item.sessionId);
hilog.info(DOMAIN, TAG,
`缓存上传成功: sessionId=${item.sessionId}, 剩余 ${i} 条`);
} else {
hilog.warn(DOMAIN, TAG,
`缓存上传失败,保留: sessionId=${item.sessionId}`);
break; // 失败则停止,等待下次网络恢复
}
} catch (err) {
hilog.error(DOMAIN, TAG,
`缓存上传异常: ${JSON.stringify(err)}`);
break;
}
}
}
private onNetworkLost(): void {
hilog.warn(DOMAIN, TAG, '网络已断开');
// 可选:显示离线状态 UI
}
private async uploadSingle(item: CachedGameResult): Promise<boolean> {
// 调用对应的 API 上传
return true;
}
}
6.2 完整链路
应用启动 → Index.aboutToAppear()
→ NetworkMonitor.start()
→ 注册 netCapabilitiesChange / netLost 回调
→ 激活监听
用户完成游戏 → submitGameResultAsync()
→ 直接调 finishGame()
├─ 成功 → 正常流程
└─ 失败 → cacheGameResultLocally() 写入 Preferences
WiFi 断开 → onNetworkLost()
→ 仅打日志 / 显示离线 UI
WiFi 恢复 → on('netCapabilitiesChange')
→ 校验 NET_CAPABILITY_INTERNET
→ uploadCachedResults()
→ 逐条上传 → 成功后 remove
七、完整方案整合
7.1 MentalChallenge.ets 最终代码
import { connection } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0001;
const TAG = 'MentalChallenge';
// ... 组件定义 ...
/**
* 异步提交游戏成绩(入库操作 + 排名查询)
*
* 策略:
* 不依赖 OS 网络状态 API 做前置判断(行为不一致),
* 直接尝试提交请求,以实际结果为准,失败则降级缓存。
*/
private async submitGameResultAsync(): Promise<void> {
try {
const finishData = await this.viewModel.finishGame();
if (finishData) {
// 提交成功
if ((finishData.rank ?? 0) <= 0) {
await this.viewModel.getMyRank();
}
hilog.info(DOMAIN, TAG,
`异步入库完成: isNewBest=${finishData.isNewBest}, rank=${finishData.rank}`);
return;
}
// 提交失败:降级到本地缓存
hilog.warn(DOMAIN, TAG,
'finishGame 返回空,成绩降级缓存到本地,等待网络恢复后自动上传');
this.cacheGameResultLocally();
this.getUIContext().getPromptAction().showToast({
message: '成绩上传失败,请检查网络',
duration: 2000
});
} catch (err) {
hilog.error(DOMAIN, TAG,
`submitGameResultAsync 异常: ${JSON.stringify(err)}`);
this.cacheGameResultLocally();
}
}
/**
* 将当前对局成绩缓存到本地 Preferences
*/
private cacheGameResultLocally(): void {
const sessionId = this.viewModel.getSessionId();
if (!sessionId) {
hilog.warn(DOMAIN, TAG, 'cacheGameResultLocally 跳过: sessionId 为空');
return;
}
const result: CachedGameResult = {
sessionId: sessionId,
score: this.viewModel.score,
totalQuestions: this.viewModel.totalQuestions,
correctQuestions: this.viewModel.correctQuestions,
difficulty: this.viewModel.currentDifficulty,
timestamp: Date.now()
};
Preferences.saveCachedResult(getContext(this), result);
hilog.info(DOMAIN, TAG, `成绩已缓存: ${sessionId}`);
}
/**
* 同步检测网络状态(保留用于 UI 弱提示,不用做请求闸门)
*/
private isNetworkAvailable(): boolean {
try {
const netHandle = connection.getDefaultNetSync();
if (netHandle.netId === 0) return false;
const netCapabilities = connection.getNetCapabilitiesSync(netHandle);
if (!netCapabilities) return false;
return netCapabilities.networkCap != null
&& netCapabilities.networkCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET);
} catch (e) {
return false;
}
}
7.2 Index.ets 完整性检查
// Index.ets — 网络恢复监听(兜底机制)
// 必须确认以下代码存在且正确:
import { connection } from '@kit.NetworkKit';
// 在 aboutToAppear 或 init 中:
const netConnection = connection.createNetConnection();
netConnection.on('netCapabilitiesChange', (data: connection.NetCapabilityInfo) => {
if (data.netCap && data.netHandle.netId !== 0) {
const netCap = data.netCap as connection.NetCapabilities;
if (netCap.networkCap != null
&& netCap.networkCap.includes(connection.NetCap.NET_CAPABILITY_INTERNET)) {
this.uploadCachedResults();
}
}
});
netConnection.register(() => {});
八、错误排查实录
8.1 案例:有网但被误判为无网
现象
07-31 19:36:21.890 A0a001/MentalChallenge W 无网络,60秒挑战成绩已缓存到本地,等待网络恢复后自动上传
但游戏成绩实际上能上传到后台(说明网络真实可用)
排查过程
| 步骤 | 假设 | 验证方式 | 结论 |
|---|---|---|---|
| 1 | 缺少 GET_NETWORK_INFO 权限 |
成绩能上传 → HTTP 可达 → 权限正常 | ❌ 排除 |
| 2 | 排行榜 API 接口问题 | 用户指出报错来自 MentalChallenge | ❌ 方向错误 |
| 3 | isNetworkAvailable() 误判 |
对比 connection API 返回值与实际 HTTP 连通性 |
✅ 命中 |
根因
connection.getDefaultNetSync() 在某些 HarmonyOS 设备/网络环境(WiFi 刚连未完成 DHCP、VPN、校园网认证等场景)下 netId 返回 0 或 NET_CAPABILITY_INTERNET 未置位,但 HTTP 请求实际可达。
修复
将"先判断再请求"改为"先请求,失败再降级"(见第四章请求试探法)。
8.2 案例:networkCap 为 undefined 导致崩溃
现象
// 运行时崩溃:Cannot read properties of undefined (reading 'includes')
if (netCap.networkCap.includes(...)) { }
修复
if (netCap.networkCap != null
&& netCap.networkCap.includes(...)) { }
九、最佳实践总结
9.1 分层使用原则
高可靠性
│
├─ HTTP 请求试探 ← 决定业务走向(提交/缓存)
│
├─ WebSocket 心跳 ← 长连接存活检测
│
└─ connection API ← 仅做 UI 弱提示(信号图标、弱网提醒)
低可靠性
9.2 核心规则
- OS API 不拦请求:
isNetworkAvailable()若判断有误,可以加条件来判断是否发 HTTP 请求 - 判空是强制要求:
networkCap和netCap都可能为undefined,调用.includes()前须要!= null判空 - 创建实例再监听:
connection.createNetConnection()→.on()→.register(),三步缺一不可 - 缓存+自动上传是标配:任何需要网络的操作都应有离线兜底和恢复重试机制
- 类型名对齐官方文档:
NetCapabilityInfo(不是NetCapabilityChangeInfo)
9.3 检查清单
| 检查项 | 位置 |
|---|---|
module.json5 中 INTERNET + GET_NETWORK_INFO 已声明 |
权限配置 |
netCapabilities.networkCap != null 判空存在 |
所有使用处 |
.on() 后跟了 .register() |
所有监听注册处 |
类型名是 NetCapabilityInfo |
回调参数类型 |
| 提交失败有缓存兜底 | 业务代码 |
| 网络恢复有自动上传 | Index 或 App 入口 |
页面销毁时调了 .unregister() |
生命周期 |
更多推荐



所有评论(0)