1. 为什么你的uniapp项目需要WiFi扫描功能?

最近好几个做智能硬件配套App的朋友都来问我同一个问题:“我的设备需要配网,用户得先连上家里的WiFi,这第一步怎么在App里优雅地实现?” 我一看,嘿,这不就是典型的WiFi扫描和列表获取需求嘛。在物联网、智能家居、打印机配网、甚至是一些需要获取网络环境的工具类App里,这个功能太常见了。用户打开你的App,点击“添加设备”或“连接网络”,一个清晰、快速的WiFi列表弹出来,体验一下子就上去了。

你可能之前用过原生开发,觉得调用系统API挺麻烦,权限一堆,兼容性还要操心。现在用uniapp,一套代码跑多个端,是不是更头大了?别担心,我刚开始也这么想。但实测下来,在uniapp里集成WiFi扫描,尤其是面向微信小程序平台,其实有现成的“捷径”可走,核心逻辑甚至比原生还清晰。这个功能的核心价值在于提升用户体验和操作效率。想象一下,让用户手动输入一长串晦涩的WiFi名称(SSID)有多反人类;而自动弹出列表让用户点选,这才是现代App该有的样子。

当然,这里有个非常重要的前提需要跟你强调:出于安全和隐私考虑,这个功能目前主要、且最稳定可靠的实现平台是微信小程序。支付宝小程序等平台可能有类似能力,但接口和限制各有不同。而App端(iOS和Android)由于系统级别的严格限制,uniapp默认的API是无法直接实现系统级WiFi扫描的。在App端,你通常需要对接原生插件或使用其他技术方案(比如引导用户跳转到系统设置页面),这完全是另一个话题了。所以,今天我们聚焦在微信小程序这个最主流、最可行的场景下,聊聊怎么用uniapp的语法,快速把WiFi列表“搬”到你的页面上。只要你项目的目标平台包含微信小程序,那这套方法就是你的“速效救心丸”。

2. 动手之前:理清权限与运行环境

好,咱们决定开干了。但在敲代码之前,有几步准备工作绝对不能跳过,这些“坑”我早期项目都踩过,现在帮你提前标出来。

### 2.1 配置小程序权限

uniapp编译到微信小程序,最终跑的是小程序代码。所以,微信小程序平台的要求就是我们的“圣旨”。WiFi相关接口属于敏感接口,需要用户授权。你不需要在uniapp的manifest.json里配,但必须在微信小程序的项目配置文件app.json中声明。

怎么操作呢?在HBuilderX里,找到你项目根目录下的 manifest.json 文件,切换到“微信小程序配置”视图。或者,你也可以直接去项目根目录的 unpackage/dist/dev/mp-weixin 目录下(编译后生成),找到 app.json。你需要确保其中有这样一段声明:

{
  "requiredPrivateInfos": [
    "getWifiList",
    "onWifiConnected",
    "startWifi"
  ]
}

注意,requiredPrivateInfos 这个字段是微信小程序要求声明隐私接口的。从2023年下半年开始,微信加强了隐私合规,这里没声明,你调用相关API可能直接失败。通常,我们至少需要声明 getWifiList 和 startWifi。我建议把可能用到的都写上,避免后续扩展时忘记。

### 2.2 理解API的“平台差异”与条件编译

这是uniapp开发的核心思维。虽然我们用的是uniapp的语法,但uni对象下并没有一个叫做uni.scanWifi的万能API。我们需要使用条件编译,在微信小程序环境下,调用微信原生的API。

在页面脚本中,你会看到这样的结构:

// #ifdef MP-WEIXIN
// 这里写微信小程序专用的WiFi API代码
wx.startWifi({...});
// #endif

// #ifdef APP-PLUS
// 这里写App端的处理方案,比如提示用户或调用插件
uni.showModal({
  title: '提示',
  content: 'App端需要跳转系统设置连接WiFi,或使用原生插件。'
});
// #endif

// #ifdef MP-WEIXIN 和 // #endif 就是条件编译指令。这意味着,这段代码只会在编译到微信小程序平台时被包含进去。编译到App或其他平台时,这段代码就像不存在一样。这样做保证了代码的平台兼容性,不会在非微信环境报错。你可能会问,那其他小程序平台呢?是的,支付宝、百度等小程序也有自己的WiFi API,但接口名和参数可能不同,你需要查阅对应平台的文档,并添加类似的条件编译块。今天我们主攻微信小程序这个最大公约数。

### 2.3 用户的手机系统与权限弹窗

即便你配置都对了,代码也写了,在真机上运行时,用户还会遇到两道“关卡”。

第一关是微信小程序基础库版本。WiFi接口不是最古老的API,但也不算新。为了保险起见,你可以设置一个较低的基础库版本要求。不过,现在绝大多数用户的微信版本都足够新,这个问题不大。

第二关,也是体验关键的一关,是用户授权弹窗。当你的代码第一次调用wx.startWifi()时,微信会向用户弹出一个授权窗口,询问“是否允许使用WiFi”。用户必须点击“允许”,后续所有操作才能正常进行。如果用户点了“拒绝”或“取消”,你的代码就会走入fail回调。作为开发者,我们必须优雅地处理这个拒绝情况:通常需要提示用户“功能需要WiFi权限”,并提供一个按钮,引导用户手动点击进入小程序设置页去打开权限(可以通过uni.openSetting接口)。这个流程设计的好坏,直接关系到你的功能开通率。

3. 核心三步曲:从初始化到列表展示

铺垫了这么多,终于到了最核心的代码环节。我把整个过程提炼成三个清晰的步骤,你跟着做,十分钟就能跑通。

### 3.1 第一步:初始化WiFi模块

你可以把WiFi模块想象成一个特殊的硬件设备,在使用前,需要先给它通电、启动。wx.startWifi() 就是那个“开机键”。这个操作通常是异步的,所以我们需要在它的回调函数里知道是否成功。

我习惯在页面的onLoad生命周期里,或者在一个明显的用户操作(比如点击一个“搜索WiFi”按钮)后调用它。代码如下:

// 在Page的methods中定义一个方法
initWifiModule() {
  const that = this;
  wx.startWifi({
    success(res) {
      console.log('WiFi模块初始化成功', res);
      that.isWifiModuleReady = true; // 用一个状态变量标记
      uni.showToast({ title: 'WiFi模块已就绪', icon: 'success' });
      // 初始化成功,紧接着就可以获取列表了
      that.getWifiList();
    },
    fail(err) {
      console.error('WiFi模块初始化失败', err);
      uni.showModal({
        title: '初始化失败',
        content: `失败原因:${err.errMsg || '未知错误'}。请检查手机WiFi是否开启,或是否授权小程序使用WiFi功能。`,
        showCancel: false
      });
      that.isWifiModuleReady = false;
    }
  });
}

注意几个细节:第一,我在成功回调里设置了一个标记isWifiModuleReady,这样页面其他部分可以知道当前状态。第二,失败回调里我用了uni.showModal给用户明确的错误提示,并把微信返回的err.errMsg展示出来,这对于调试和用户理解问题非常有帮助。第三,成功之后,我直接链式调用了that.getWifiList(),让流程自动进行下去。

### 3.2 第二步:发起获取WiFi列表的请求

模块准备好了,我们就可以命令系统去扫描周边的WiFi热点。这就是wx.getWifiList()干的事。它像一个侦察兵,派出去搜集情报。

getWifiList() {
  if (!this.isWifiModuleReady) {
    uni.showToast({ title: '请先初始化WiFi模块', icon: 'none' });
    return;
  }

  wx.getWifiList({
    success(res) {
      console.log('已发出获取WiFi列表指令', res);
      // 注意:这里success仅仅表示指令发送成功,不代表拿到数据了!
      // 真正的列表数据在另一个监听回调里。
    },
    fail(err) {
      console.error('获取WiFi列表指令发送失败', err);
      uni.showToast({ title: '扫描指令失败', icon: 'none' });
    }
  });
}

这里有一个超级重要的坑点,我当年就在这里卡了半天:wx.getWifiList()的success回调,只代表“系统收到了你的扫描请求”,并不代表扫描完成并把数据给你了!真正的WiFi列表数据,是通过一个事件监听器来获取的。所以,你光调用这个方法,页面上是等不来数据的。很多人做到这一步就懵了,觉得代码没错怎么没数据?原因就在这儿。

### 3.3 第三步:监听并处理返回的WiFi列表数据

上一步派出了侦察兵,这一步我们要设置一个“通讯员”,专门等待侦察兵带回情报。这个“通讯员”就是 wx.onGetWifiList 监听函数。这个监听只需要注册一次,通常放在页面初始化的时候(比如onLoad里),或者紧接在startWifi成功之后、第一次调用getWifiList之前。

// 在onLoad或初始化方法中注册监听
onLoad() {
  this.initWifiListener();
},
methods: {
  initWifiListener() {
    wx.onGetWifiList((res) => {
      console.log('监听到WiFi列表数据', res);
      const wifiList = res.wifiList || [];
      
      if (wifiList.length === 0) {
        uni.showToast({ title: '未扫描到任何WiFi网络', icon: 'none' });
        this.wifiList = [];
        return;
      }

      // 对列表进行简单处理:按信号强度排序
      const sortedList = wifiList.sort((a, b) => b.signalStrength - a.signalStrength);
      
      // 处理数据,方便页面渲染
      const processedList = sortedList.map(item => {
        return {
          ssid: item.SSID, // WiFi名称
          bssid: item.BSSID, // MAC地址
          secure: item.secure, // 是否加密
          signalStrength: item.signalStrength, // 信号强度
          // 信号强度转成图标或等级,比如0-4格
          signalLevel: this.calcSignalLevel(item.signalStrength)
        };
      });

      // 更新页面数据,触发视图渲染
      this.wifiList = processedList;
      uni.showToast({ title: `找到${processedList.length}个网络`, icon: 'success' });
    });
  },
  
  // 一个简单的信号强度计算函数
  calcSignalLevel(rssi) {
    if (rssi >= -50) return 4; // 信号极好
    else if (rssi >= -60) return 3; // 信号好
    else if (rssi >= -70) return 2; // 信号一般
    else if (rssi >= -80) return 1; // 信号弱
    else return 0; // 信号极弱
  }
}

现在,整个流程就串起来了:初始化模块 -> 发送扫描指令 -> 监听器自动接收数据 -> 处理并渲染数据。你可能会注意到,res.wifiList里的每个WiFi对象包含了很多字段,比如SSID(名称)、BSSID(路由器MAC地址)、secure(是否加密)、signalStrength(信号强度,单位dBm,负值,越大越好)。我通常会对列表按信号强度排个序,把最强的放在最前面,方便用户选择。同时,将信号强度这个数值转换成更直观的“信号格数”,用户体验会更好。

4. 打造一个健壮好用的WiFi列表组件

光有数据还不够,我们得把它漂亮、稳定地展示给用户,并且能处理用户交互。这一步才是功能价值的最终体现。

### 4.1 页面数据绑定与渲染

首先,我们在页面的data中定义好存放列表的变量。

data() {
  return {
    wifiList: [], // 处理后的WiFi列表数据
    isScanning: false, // 是否正在扫描
    currentConnectedSSID: '' // 当前手机已连接的WiFi名(可选)
  };
}

然后,在模板中用一个v-for循环来渲染列表。这里我用一个简单的view列表展示,你可以根据自己项目的UI库(如uView、uni-ui)来美化。

<template>
  <view class="container">
    <view class="scan-btn">
      <button @tap="handleScanWifi" :loading="isScanning">
        {{ isScanning ? '扫描中...' : '点击扫描周边WiFi' }}
      </button>
    </view>

    <view class="wifi-list" v-if="wifiList.length > 0">
      <view class="list-header">
        <text>选择可用的WiFi网络</text>
        <text>共 {{ wifiList.length }} 个</text>
      </view>
      <view class="wifi-item" v-for="(item, index) in wifiList" :key="index" @tap="handleSelectWifi(item)">
        <view class="wifi-info">
          <view class="ssid">{{ item.ssid }}</view>
          <view class="details">
            <text v-if="item.secure" class="secure-lock">🔒</text>
            <text class="bssid">{{ item.bssid }}</text>
          </view>
        </view>
        <view class="wifi-signal">
          <!-- 根据signalLevel显示信号格 -->
          <view class="signal-bars">
            <view v-for="n in 4" :key="n" 
                  :class="['signal-bar', n <= item.signalLevel ? 'active' : '']">
            </view>
          </view>
          <text class="strength">{{ item.signalStrength }} dBm</text>
        </view>
      </view>
    </view>

    <view class="empty-tip" v-else>
      <text>暂无WiFi列表,请点击上方按钮扫描</text>
    </view>
  </view>
</template>

对应的CSS样式(<style>部分)你需要自己设计,比如设置.wifi-item的边框、内边距、.signal-bar的宽度颜色等,让列表看起来清晰美观。信号格可以用几个小长条view,通过active类控制高亮数量来模拟。

### 4.2 处理用户选择与连接

用户点击列表中的某个WiFi项后,我们通常需要做两件事:第一,展示该WiFi的详情(比如密码输入框);第二,调用连接API。这里我们先处理第一件事。

handleSelectWifi(wifiItem) {
  console.log('用户选择了WiFi:', wifiItem);
  this.selectedWifi = wifiItem; // 存储当前选中的WiFi
  
  // 弹出底部弹窗或跳转新页面,让用户输入密码
  uni.showModal({
    title: `连接至 ${wifiItem.ssid}`,
    content: '请输入WiFi密码',
    editable: true, // 允许输入
    placeholderText: '请输入密码',
    success: (modalRes) => {
      if (modalRes.confirm) {
        const password = modalRes.content;
        this.connectToWifi(wifiItem, password);
      }
    }
  });
},

### 4.3 实现WiFi连接功能

用户输入密码后,我们调用 wx.connectWifi API 进行连接。这是一个关键操作,需要处理好各种状态。

connectToWifi(wifiItem, password) {
  uni.showLoading({ title: '连接中...', mask: true });
  
  wx.connectWifi({
    SSID: wifiItem.ssid,
    BSSID: wifiItem.bssid, // BSSID可选,但指定后更精确
    password: password,
    success: (res) => {
      console.log('连接指令发送成功', res);
      uni.hideLoading();
      // 连接指令成功,同样不代表已连上,需要监听连接状态变化
      uni.showToast({ title: '正在连接,请稍候...', icon: 'none' });
    },
    fail: (err) => {
      console.error('连接指令失败', err);
      uni.hideLoading();
      let errMsg = '连接失败';
      // 根据错误码给出友好提示
      switch(err.errCode) {
        case 12002: errMsg = '网络超时,请重试'; break;
        case 12003: errMsg = '密码错误,请重新输入'; break;
        case 12004: errMsg = '连接被拒绝'; break;
        case 12005: errMsg = '未找到指定WiFi'; break;
        default: errMsg = `连接失败: ${err.errMsg}`;
      }
      uni.showModal({ title: '提示', content: errMsg, showCancel: false });
    }
  });

  // 监听WiFi连接成功事件
  wx.onWifiConnected((res) => {
    console.log('WiFi已连接', res);
    const connectedWifi = res.wifi;
    uni.showToast({ title: `已成功连接到 ${connectedWifi.SSID}`, icon: 'success' });
    // 更新状态,比如清空列表或标记已连接项
    this.currentConnectedSSID = connectedWifi.SSID;
    // 可以在这里进行下一步业务逻辑,比如设备配网
  });
}

注意,wx.connectWifi 和之前的 getWifiList 类似,它的success也只代表连接请求已发出。真正的连接结果,需要通过监听 wx.onWifiConnected 事件来获取。同时,你也应该监听 wx.onWifiConnectedFail 来处理连接失败的事件,给用户更精准的反馈。为了代码清晰,我这里只展示了成功监听。

5. 避坑指南与高级技巧

功能跑通了,但想做得更专业、更稳定,下面这些我踩过的坑和总结的技巧,你一定要看看。

### 5.1 常见错误码与排查方法

在实际使用中,你肯定会遇到各种fail回调。看懂错误码是快速解决问题的关键。我整理了几个最常见的:

错误码含义可能原因与解决方案
12000未先调用 startWifi调用顺序错了。确保先startWifi成功,再调其他API。
12001系统WiFi开关未打开提示用户去手机设置打开WiFi。
12002连接超时网络环境复杂或信号太弱。提示用户重试,或靠近路由器。
12003密码错误用户输入错误。引导用户重新输入,并确保显示密码功能可用。
12004连接请求被拒绝路由器端拒绝了请求(如MAC过滤)。提示用户检查路由器设置。
12005未找到指定SSID的网络可能WiFi已隐藏,或设备已远离。重新扫描。
12006系统操作失败系统底层错误。尝试重启小程序或手机WiFi。

在fail回调里,通过err.errCode判断类型,给用户友好的提示,而不是直接抛出一串代码,体验会好很多。

### 5.2 性能优化与体验提升

  1. 防抖与加载状态:那个“扫描”按钮,用户可能会疯狂点击。不做处理的话会重复发送请求。我通常会用个变量isScanning锁住按钮,并在点击后设置为true,等到列表返回或超时后再设为false。同时按钮显示“扫描中...”的加载状态。
  2. 定时刷新与缓存:在一些需要实时显示网络环境的场景,你可以设置一个定时器(比如每10秒自动扫描一次)。但要注意功耗和用户体验,不宜太频繁。对于静态场景,扫描一次后缓存列表,除非用户手动刷新。
  3. 处理隐藏网络:wx.getWifiList获取不到隐藏网络(不广播SSID的网络)。如果需要连接隐藏网络,你需要让用户手动输入SSID,然后直接调用wx.connectWifi,并确保SSID参数准确。
  4. Android/iOS的细微差异:虽然都是微信小程序,但不同手机系统在权限提示、返回数据细节上可能有微小差别。真机测试时务必覆盖主流机型。

### 5.3 封装成可复用的组件或模块

如果你的项目里多个页面都需要WiFi功能,强烈建议把核心逻辑封装起来。我习惯创建一个wifiManager.js的工具模块。

// utils/wifiManager.js
class WifiManager {
  constructor() {
    this.isModuleReady = false;
    this._initListener();
  }

  // 初始化监听
  _initListener() {
    wx.onGetWifiList((res) => {
      if (this.onWifiListUpdate) {
        this.onWifiListUpdate(res.wifiList);
      }
    });
    wx.onWifiConnected((res) => {
      if (this.onWifiConnected) {
        this.onWifiConnected(res.wifi);
      }
    });
  }

  // 公开方法:开始扫描
  scan() {
    return new Promise((resolve, reject) => {
      if (!this.isModuleReady) {
        wx.startWifi({
          success: () => {
            this.isModuleReady = true;
            this._doGetWifiList(resolve, reject);
          },
          fail: reject
        });
      } else {
        this._doGetWifiList(resolve, reject);
      }
    });
  }

  _doGetWifiList(resolve, reject) {
    wx.getWifiList({
      success: resolve,
      fail: reject
    });
  }

  // 公开方法:连接WiFi
  connect({ SSID, BSSID, password }) {
    return new Promise((resolve, reject) => {
      wx.connectWifi({
        SSID,
        BSSID,
        password,
        success: resolve,
        fail: reject
      });
    });
  }
}

export default new WifiManager();

然后在页面中,你可以这样优雅地使用:

import wifiManager from '@/utils/wifiManager.js';

export default {
  methods: {
    async handleScan() {
      try {
        this.isScanning = true;
        await wifiManager.scan();
        // 监听回调会通过 onWifiListUpdate 触发
      } catch (err) {
        // 处理错误
      } finally {
        this.isScanning = false;
      }
    }
  },
  onLoad() {
    // 设置回调
    wifiManager.onWifiListUpdate = (list) => {
      this.wifiList = this.processList(list);
    };
    wifiManager.onWifiConnected = (wifi) => {
      console.log('连接成功回调', wifi);
    };
  }
}

这样封装后,代码清晰多了,业务页面只关心数据和UI,所有WiFi相关的底层调用和状态管理都交给了wifiManager。

更多推荐