简介:这份资源面向使用uni-app进行跨平台开发、需要与Android原生深度交互的前端与移动端开发者,聚焦通过原生插件实现JS与Android通信以及控制WiFi开关这一常见硬件操作需求。压缩包共38个文件,约11.71MB,以js脚本、vue组件、json配置为主,辅以png图标、html页面、scss与css样式,并包含aar原生插件包与apk安装包,便于直接运行验证。资源围绕uni.$invokeMethod调用原生方法、BroadcastReceiver广播通信、WifiManager的setWifiEnabled控制、Android 6.0以上动态申请ACCESS_WIFI_STATE与CHANGE_WIFI_STATE权限等知识点展开,示例工程中可看到按钮触发调用、原生插件接收并执行开关WiFi的完整链路。已有2488人学习,适合想掌握原生插件封装、真机调试与跨平台兼容处理的开发者参考借鉴。
1. 从一次真机联调说起:uni-app 怎么和 Android 原生打通
去年做一个工业巡检 App,前端用 uni-app 写页面,结果卡在一个很具体的问题上:设备列表页要显示当前连接的 WiFi 名称,还要能一键跳到系统 WiFi 设置页让用户手动切换网络。纯 JS 层拿不到这些信息,只能往原生走。当时我以为要重写整个项目,后来发现 uni-app 的plus.android桥接能力足够用,几十行原生调用代码就解决了。这个项目就是围绕「uni-app 调用 Android 原生实现通信、打开 WiFi 设置」这条链路展开的,适合已经会用 uni-app 写页面、但一碰到原生能力就发怵的开发者。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆,代码可以直接抄进你的工程里试。
2. 技术选型与工程结构:为什么走 plus.android 而不是原生插件
2.1 三种桥接方式的取舍
uni-app 调 Android 原生,常见有三条路:原生插件、UTS 插件、plus.android反射调用。原生插件能力最强但要单独建 Android 工程、写 module 配置、打包自定义基座,对只想调几个系统 API 的场景太重;UTS 插件是较新的方案,写起来接近 TypeScript,但生态和调试工具还在完善,团队里没人踩过坑的话容易翻车;plus.android是 HBuilderX 内置的桥接对象,不需要额外工程,直接在 JS 里importClass拿 Java 类、newObject建实例、invoke调方法,适合「调系统服务、拿系统信息、跳系统页面」这类轻量需求。
这个项目选的是第三条路,理由很实际:需求只有「读 WiFi 状态」和「打开 WiFi 设置页」两个动作,没有复杂 UI 要嵌原生 View,也没有高频通信的性能要求。用plus.android的代价是只能在 App 端跑(H5 和小程序不支持),但换来的是零原生工程、改一行 JS 就能真机验证。
提示:
plus.android只在 5+ App(Android/iOS)环境存在,H5、各家小程序里plus是 undefined,调用前必须做环境判断,否则页面一进就白屏。
2.2 目录结构与关键文件
工程是标准的 uni-app CLI 或 HBuilderX 项目,和原生相关的代码集中在一个工具模块里,不散落在页面中:
project-root/ ├── pages/ │ └── wifi/ │ └── wifi.vue # WiFi 状态展示与操作页 ├── utils/ │ └── android-bridge.js # 原生调用封装,统一出口 ├── static/ ├── manifest.json # 需确认 App 模块与权限配置 ├── pages.json └── main.jsutils/android-bridge.js是核心,所有plus.android调用都收在这里,页面只 import 函数、不直接碰plus。这样做的好处是:以后要换成 UTS 插件或原生插件,只改这一个文件;调试时也只需要在一个地方打日志。manifest.json里要确认「App 模块配置」勾选了必要的模块,Android 权限按需在manifest.json的app-plus.distribute.android.permissions里声明,WiFi 状态读取在部分系统版本上需要定位权限配合,这点后面避坑章节会细说。
2.3 运行环境与最小验证步骤
先把环境跑通再谈功能。需要 HBuilderX(或 CLI 工程配好 Android 打包环境)、一台开了 USB 调试的 Android 真机、以及一个能装上的自定义基座或标准基座。标准基座就能验证plus.android的大部分能力,不必一上来就打自定义包。
# CLI 工程示例:先跑起来,用真机调试 npm install npm run dev:app-plus # HBuilderX 用户:运行 -> 运行到手机或模拟器 -> 选择设备跑起来后,在任意页面的onReady里加一行探针,确认桥接对象可用:
// 探针:确认当前环境支持 plus.android onReady() { // #ifdef APP-PLUS console.log('plus 存在:', typeof plus !== 'undefined') console.log('android 桥存在:', typeof plus.android !== 'undefined') // #endif }#ifdef APP-PLUS是 uni-app 的条件编译注释,只在 App 端保留这段代码,H5 打包时会被剔除。如果真机控制台打出两个true,说明桥接链路是通的,可以往下写业务调用;如果plus是 undefined,先检查是不是跑在了浏览器预览里,而不是真机基座。
3. 核心模块拆解:从 JS 到 Java 的调用链路
3.1 importClass 与 newObject:拿到系统服务
Android 的 WiFi 相关能力挂在Context的系统服务上,JS 侧要先拿到当前 Activity 的 Context,再通过getSystemService取WifiManager。plus.android提供importClass导入 Java 类、runtimeMainActivity拿主 Activity、invoke调实例方法。
// utils/android-bridge.js // 获取 WifiManager 实例,失败返回 null export function getWifiManager() { // #ifdef APP-PLUS try { // 导入 Android 的 Context 类,用于取系统服务常量 const Context = plus.android.importClass('android.content.Context') // 拿到当前主 Activity 作为 Context const main = plus.android.runtimeMainActivity() // 调 getSystemService 取 WiFi 管理服务 const wifiManager = main.getSystemService(Context.WIFI_SERVICE) return wifiManager || null } catch (e) { console.error('获取 WifiManager 失败:', e) return null } // #endif // #ifndef APP-PLUS return null // #endif }这段的关键点有三个。importClass的参数是完整类名,写错一个包名就抛异常,所以整段包在 try/catch 里,失败时返回 null 让上层降级。runtimeMainActivity()返回的是主 Activity 对象,它本身是 Context 的子类,直接调getSystemService即可,不需要再newObject。Context.WIFI_SERVICE是字符串常量"wifi",通过 importClass 拿到的类对象上可以直接访问静态字段,这是plus.android的一个便利之处。
3.2 读取 WiFi 状态:isWifiEnabled 与连接信息
拿到WifiManager后,开关状态用isWifiEnabled(),当前连接的 SSID 用getConnectionInfo()返回的WifiInfo对象再取getSSID()。注意getSSID()返回的字符串带双引号,需要自己剥掉。
// 读取 WiFi 开关与当前 SSID export function getWifiState() { const wifiManager = getWifiManager() if (!wifiManager) return { enabled: false, ssid: '' } // #ifdef APP-PLUS try { // 开关状态:布尔值 const enabled = wifiManager.isWifiEnabled() // 连接信息对象,可能为 null(未连接时) const info = wifiManager.getConnectionInfo() let ssid = '' if (info) { // getSSID 返回形如 "MyWiFi" 的带引号字符串 ssid = String(info.getSSID()).replace(/^"|"$/g, '') } return { enabled, ssid } } catch (e) { console.error('读取 WiFi 状态失败:', e) return { enabled: false, ssid: '' } } // #endif }isWifiEnabled()无参数,返回 boolean,直接对应系统 WiFi 开关。getConnectionInfo()在未连接任何网络时可能返回一个 SSID 为<unknown ssid>的对象,所以剥引号之后还要在 UI 层判断这个占位值,别直接显示给用户。replace(/^"|"$/g, '')同时去掉首尾引号,比slice(1, -1)安全,因为某些定制 ROM 返回的字符串可能不带引号,硬切会切掉真实字符。
3.3 打开 WiFi 设置页:Intent 跳转的两种写法
「打开 WiFi」有两种语义:一是把系统 WiFi 开关打开,二是跳到系统 WiFi 设置页让用户自己选网络。前者用setWifiEnabled(true),但在 Android 10 之后对普通应用限制很严,基本调不动;后者用Intent跳Settings.ACTION_WIFI_SETTINGS,各版本都稳。项目里主推第二种。
// 跳转系统 WiFi 设置页 export function openWifiSettings() { // #ifdef APP-PLUS try { const Intent = plus.android.importClass('android.content.Intent') const Settings = plus.android.importClass('android.provider.Settings') const main = plus.android.runtimeMainActivity() // 构造跳转 WiFi 设置页的 Intent const intent = new Intent(Settings.ACTION_WIFI_SETTINGS) // 以新任务栈启动,避免和当前页面耦合 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) main.startActivity(intent) return true } catch (e) { console.error('打开 WiFi 设置失败:', e) return false } // #endif // #ifndef APP-PLUS return false // #endif }new Intent(action)用 action 字符串构造,Settings.ACTION_WIFI_SETTINGS的值是"android.settings.WIFI_SETTINGS",通过 importClass 拿静态常量比手写字符串可靠。FLAG_ACTIVITY_NEW_TASK必须加,因为从非 Activity 上下文启动 Activity 时系统会要求新任务栈,不加会抛AndroidRuntimeException。startActivity是 Activity 的方法,所以调用方是runtimeMainActivity()返回的对象,不是plus.android本身。
3.4 页面层调用:状态刷新与生命周期配合
页面层只做三件事:进页面读一次状态、点按钮跳设置、从设置页回来再读一次。第三点最容易漏——用户去设置页改了 WiFi 再返回,页面不会自动刷新,得在onShow里重新读。
// pages/wifi/wifi.vue 的 script 部分 import { getWifiState, openWifiSettings } from '@/utils/android-bridge.js' export default { data() { return { enabled: false, ssid: '未连接' } }, onShow() { // 每次页面显示都刷新,覆盖从设置页返回的场景 this.refresh() }, methods: { refresh() { const state = getWifiState() this.enabled = state.enabled this.ssid = state.ssid && state.ssid !== '<unknown ssid>' ? state.ssid : '未连接' }, goSettings() { const ok = openWifiSettings() if (!ok) { uni.showToast({ title: '当前环境不支持', icon: 'none' }) } } } }onShow而不是onLoad是关键,onLoad只在页面首次加载时触发,从系统设置页返回走的是onShow。refresh里对<unknown ssid>做了兜底,避免把系统占位符显示成网络名。goSettings根据返回值给 toast,H5 环境下openWifiSettings返回 false,用户能看到明确提示而不是点了没反应。
4. 避坑与排查:真机上最容易翻车的五个点
4.1 现象:plus是 undefined,页面直接报错
原因通常是代码跑在了 H5 预览或小程序模拟器里,plus对象根本不存在。解决是把所有原生调用包进#ifdef APP-PLUS条件编译,并在函数入口做typeof plus === 'undefined'的防御性判断,返回降级值。别指望 try/catch 兜住,plus未定义时是 ReferenceError,在部分构建配置下会中断整个模块加载。
4.2 现象:getSSID()返回<unknown ssid>
原因有两个层面。一是确实没连 WiFi,系统返回占位符;二是 Android 8.1 之后读取 SSID 需要定位权限(ACCESS_FINE_LOCATION)且定位开关打开,权限没给时同样返回占位符而不是报错。解决是在manifest.json声明定位权限,运行时用plus.android检查并申请,同时在 UI 上把占位符映射成「未连接或权限不足」,别让用户以为是网络坏了。
4.3 现象:startActivity抛AndroidRuntimeException
报错信息类似Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag。原因是 Intent 少了新任务栈标志。解决是构造 Intent 后立刻intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),顺序上要在startActivity之前。这个坑在模拟器上不一定复现,真机上必现,属于典型的「本地好好的、一上真机就炸」。
4.4 现象:importClass报类找不到
包名拼错、类名大小写不对、或者该类在目标 Android 版本上不存在,都会走到这里。plus.android.importClass对不存在的类抛异常而不是返回 null,所以必须 try/catch。排查时先把类名单独打日志,用plus.android.importClass('android.provider.Settings')这种最简形式验证桥接本身没问题,再逐步加参数。另外Settings.ACTION_WIFI_SETTINGS这类常量在低版本 SDK 上可能缺失,做版本判断或用字符串字面量兜底。
4.5 现象:从设置页返回后状态不更新
原因是刷新逻辑写在了onLoad里。系统设置页是另一个 Activity,返回时当前页面不会重新加载,只触发onShow。解决是把读取状态的逻辑抽成refresh(),在onShow里调用。如果页面层级深、onShow触发时机不确定,还可以监听uni.onAppShow做补充,但优先用onShow,简单可靠。
5. 二次开发:把桥接层做成可替换的能力模块
这套代码真正值钱的地方不是那几十行调用,而是「桥接层收口」的结构。我后来在另一个项目里把utils/android-bridge.js升级成了带适配器的能力模块:对外暴露getWifiState、openWifiSettings这类语义化函数,内部按运行环境分派——App 端走plus.android,未来接 UTS 插件时只加一个分支,页面代码一行不改。
具体做法是定义一个能力接口,用简单对象做注册表:
// utils/capabilities/index.js const providers = {} // 注册某环境下的实现,key 为环境标识 export function registerProvider(env, impl) { providers[env] = impl } // 按当前环境取实现,找不到返回空实现 export function getProvider() { // #ifdef APP-PLUS return providers['app'] || emptyProvider // #endif // #ifdef H5 return providers['h5'] || emptyProvider // #endif return emptyProvider } const emptyProvider = { getWifiState: () => ({ enabled: false, ssid: '' }), openWifiSettings: () => false }页面只依赖getProvider().getWifiState(),不关心底层是plus.android还是别的。验证方法也简单:在 H5 下跑一遍,确认空实现让页面正常降级不报错;在真机上跑一遍,确认 App 实现生效。两套环境都过,说明收口是干净的。
改造时还有两个习惯值得坚持。一是所有原生返回值都做「类型归一」——getSSID()的引号、isWifiEnabled()的布尔、getConnectionInfo()的 null,在桥接层统一处理完再往上抛,页面层拿到的永远是干净数据。二是每个原生函数都留一条console.error带上下文,真机调试时用 HBuilderX 的日志面板能直接定位是哪个类、哪个方法出的问题,比盲猜快得多。
从那以后我每次接原生能力,都强制先把桥接层单独跑通、打一条探针日志确认环境,再往页面里接。这个顺序看着慢,实际省掉了大量「页面白屏但不知道哪一层挂了」的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取