目录

  1. 权限声明
  2. 同步检测:获取当前网络状态
  3. 异步监听:实时感知网络变化
  4. 请求试探法:绕过 OS API 不可靠问题
  5. 离线缓存机制
  6. 网络恢复后自动上传
  7. 完整方案整合
  8. 错误排查实录
  9. 最佳实践总结

一、权限声明

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 核心规则

  1. OS API 不拦请求isNetworkAvailable() 若判断有误,可以加条件来判断是否发 HTTP 请求
  2. 判空是强制要求networkCapnetCap 都可能为 undefined,调用 .includes() 前须要 != null 判空
  3. 创建实例再监听connection.createNetConnection().on().register(),三步缺一不可
  4. 缓存+自动上传是标配:任何需要网络的操作都应有离线兜底和恢复重试机制
  5. 类型名对齐官方文档NetCapabilityInfo(不是 NetCapabilityChangeInfo

9.3 检查清单

检查项 位置
module.json5INTERNET + GET_NETWORK_INFO 已声明 权限配置
netCapabilities.networkCap != null 判空存在 所有使用处
.on() 后跟了 .register() 所有监听注册处
类型名是 NetCapabilityInfo 回调参数类型
提交失败有缓存兜底 业务代码
网络恢复有自动上传 Index 或 App 入口
页面销毁时调了 .unregister() 生命周期
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐