news 2026/9/18 22:35:06

react-use 之 useBattery:用 React Hook 追踪设备电池状态(Battery Status API 实践指南)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react-use 之 useBattery:用 React Hook 追踪设备电池状态(Battery Status API 实践指南)

react-use 之 useBattery:用 React Hook 追踪设备电池状态(Battery Status API 实践指南)

【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use

useBattery 是 react-use 提供的一个传感器(Sensor)类 Hook,用于在 React 组件中读取并实时追踪设备的电池状态。本文以 docs/useBattery.md 为主体,结合 src/useBattery.ts 源码实现,完整讲解它的使用方法、返回值语义、底层原理(含事件订阅与深度比较优化)以及兼容性注意事项。读完本文,你将能够在一个 React 应用中快速实现电量、充电状态、预计充电/放电剩余时间等信息的展示,并理解在浏览器 Battery Status API 已进入废弃状态的前提下应如何安全使用它。

一、useBattery 是什么

useBattery是一个用于读取和追踪设备电池状态的 React Hook,它封装了浏览器的BatteryManagerAPI(又称 Battery Status API)。在支持该 API 的环境中,Hook 会主动拉取一次电池快照,并持续订阅电池相关事件,保证组件中的电量数据始终与系统实际状态同步。

需要特别注意的是,官方文档在 docs/useBattery.md 的开头给出了明确警告:

Note:currentBatteryManagerAPI state is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time.

即当前BatteryManagerAPI 已处于废弃状态,部分浏览器中可能仍然可用,但由于它随时可能被移除,官方并不鼓励在生产环境中依赖它。这正是useBattery把"是否支持"作为一个独立状态暴露出来的原因——应用必须对不支持的环境做降级处理。

从源码结构看,useBattery属于 react-use 的 Sensors(传感器)类别,与 useGeolocation、useNetworkState 等 Hook 同属一类,且在 src/index.ts 中被统一导出:

export { default as useBattery } from './useBattery';

二、基本用法

useBattery不接受任何参数,调用后返回一个描述电池状态的联合状态对象。官方文档给出的完整示例如下:

import {useBattery} from 'react-use'; const Demo = () => { const batteryState = useBattery(); if (!batteryState.isSupported) { return ( <div> <strong>Battery sensor</strong>: <span>not supported</span> </div> ); } if (!batteryState.fetched) { return ( <div> <strong>Battery sensor</strong>: <span>supported</span> <br /> <strong>Battery state</strong>: <span>fetching</span> </div> ); } return ( <div> <strong>Battery sensor</strong>:&nbsp;&nbsp; <span>supported</span> <br /> <strong>Battery state</strong>: <span>fetched</span> <br /> <strong>Charge level</strong>:&nbsp;&nbsp; <span>{ (batteryState.level * 100).toFixed(0) }%</span> <br /> <strong>Charging</strong>:&nbsp;&nbsp; <span>{ batteryState.charging ? 'yes' : 'no' }</span> <br /> <strong>Charging time</strong>:&nbsp;&nbsp; <span>{ batteryState.chargingTime ? batteryState.chargingTime : 'finished' }</span> <br /> <strong>Discharging time</strong>:&nbsp;&nbsp; <span>{ batteryState.dischargingTime }</span> </div> ); };

这个示例清晰地展示了useBattery三阶段渲染模型,这也是实际项目中最推荐的使用范式:

  1. 不支持(isSupported === false:浏览器没有getBattery能力,直接展示降级提示;
  2. 获取中(isSupported === true && fetched === false:API 已确认支持,但异步的电池快照尚未返回,展示"fetching"加载态;
  3. 已获取(fetched === true:电池数据就绪,此时levelchargingchargingTimedischargingTime均可安全读取。

在第三步的展示中,有几个值得注意的细节:

  • level的取值范围是0.0 ~ 1.0,因此展示百分比时需要乘以 100,示例中还用toFixed(0)做了取整;
  • chargingTime在电池已充满时为0,示例用三元表达式将其渲染为'finished'
  • charging用布尔值控制显示'yes'/'no'

同样的演示代码也存在于仓库的 Storybook 示例 stories/useBattery.story.tsx 中(注册在Sensors/useBattery分类下),可以通过 Storybook 直接预览该 Hook 在不同状态下的渲染效果。

三、返回值参考

官方文档在 Reference 一节给出了如下的类型签名:

const {isSupported, level, charging, dischargingTime, chargingTime} = useBattery();

各返回值字段的完整语义如下:

  • isSupported: boolean— 浏览器/设备是否支持BatteryManager
  • fetched: boolean— 电池状态是否已经获取成功;
  • level: number— 系统电池电量,取值范围为0.01.0之间的数值;
  • charging: boolean— 电池当前是否正在充电;
  • dischargingTime: number— 距离电池完全放电、系统进入挂起状态所剩余的秒数;
  • chargingTime: number— 距离电池充满所剩余的秒数;若电池已充满,则该值为0

对照 src/useBattery.ts 源码中的BatteryState接口,可以看到上述字段在实现层的精确类型定义:

export interface BatteryState { charging: boolean; chargingTime: number; dischargingTime: number; level: number; }

useBattery的返回值并不是一个普通对象,而是一个可辨识联合类型(Discriminated Union),通过isSupportedfetched两个判别字段将三种状态严格区分开:

type UseBatteryState = | { isSupported: false } // Battery API is not supported | { isSupported: true; fetched: false } // battery API supported but not fetched yet | (BatteryState & { isSupported: true; fetched: true }); // battery API supported and fetched

这意味着在 TypeScript 项目中,当你通过if (!batteryState.isSupported)if (!batteryState.fetched)完成分支收敛之后,TypeScript 会在后续代码块中自动推断出可用的字段,从而在编译期保证"只有数据就绪后才能访问level/charging等字段",从类型层面杜绝了空值访问错误。

四、源码实现原理

useBattery的核心实现位于 src/useBattery.ts,整体可以分为以下几个关键环节。

4.1 模块级的特性检测与降级导出

在模块加载阶段,Hook 会立即检测环境是否支持电池 API,而非等到组件渲染时才判断:

const nav: NavigatorWithPossibleBattery | undefined = isNavigator ? navigator : undefined; const isBatteryApiSupported = nav && typeof nav.getBattery === 'function'; function useBatteryMock(): UseBatteryState { return { isSupported: false }; } export default isBatteryApiSupported ? useBattery : useBatteryMock;

其中isNavigator来自 src/misc/util.ts:

export const isNavigator = typeof navigator !== 'undefined';

navigator.getBattery是异步的、返回Promise<BatteryManager>,因此这里先做同步的类型判定。判定结果直接决定默认导出的是真实实现useBattery还是空实现useBatteryMockuseBatteryMock永远返回{ isSupported: false },让不支持的环境天然落入第一节展示的"不支持"分支,无需额外的运行时判断。这保证了在 SSR、旧浏览器等场景下,Hook 可以安全挂载而不会抛错。

4.2 初始化与订阅生命周期

真实实现中,组件挂载后通过useEffect发起异步获取,并管理订阅的生命周期:

useEffect(() => { let isMounted = true; let battery: BatteryManager | null = null; const handleChange = () => { if (!isMounted || !battery) { return; } const newState: UseBatteryState = { isSupported: true, fetched: true, level: battery.level, charging: battery.charging, dischargingTime: battery.dischargingTime, chargingTime: battery.chargingTime, }; !isDeepEqual(state, newState) && setState(newState); }; nav!.getBattery!().then((bat: BatteryManager) => { if (!isMounted) { return; } battery = bat; on(battery, 'chargingchange', handleChange); on(battery, 'chargingtimechange', handleChange); on(battery, 'dischargingtimechange', handleChange); on(battery, 'levelchange', handleChange); handleChange(); }); return () => { isMounted = false; if (battery) { off(battery, 'chargingchange', handleChange); off(battery, 'chargingtimechange', handleChange); off(battery, 'dischargingtimechange', handleChange); off(battery, 'levelchange', handleChange); } }; }, []);

这一实现有几个值得称道的细节:

  • isMounted守卫:由于getBattery()是异步操作,Promise resolve 时组件可能已经卸载。源码在.then回调和handleChange中都检查isMounted,避免对已卸载组件调用setState造成的内存泄漏与 React 警告;
  • 四类电池事件订阅chargingchange(充电状态变化)、chargingtimechange(充电时间变化)、dischargingtimechange(放电时间变化)、levelchange(电量变化)覆盖了 Battery Status API 的全部事件类型。其中前三个事件被定义为BatteryManager接口上的回调属性(见源码第 12–17 行的BatteryManager接口定义),而实际监听统一收敛到同一个handleChange回调;
  • 首次快照:订阅完成后立即调用一次handleChange(),将 Promise 返回的初始电池状态写入 React state,这正是fetchedfalse变为true的时机;
  • 完整清理:Effect 的清理函数将isMounted置为false,并通过off移除四个事件监听,on/off均来自 src/misc/util.ts,内部对addEventListener/removeEventListener做了存在性判断与类型封装。

4.3 深度比较,避免无谓重渲染

handleChange中还有一个容易被忽略的优化点:

!isDeepEqual(state, newState) && setState(newState);

isDeepEqual来自 src/misc/isDeepEqual.ts,其实现是:

import isDeepEqualReact from 'fast-deep-equal/react'; export default isDeepEqualReact;

即底层使用fast-deep-equal的 React 版本做深度相等比较。当电池事件触发但状态值没有实质变化时(例如levelchange事件因精度问题重复派发),setState会被跳过,从而避免多余的重新渲染。这也解释了为什么初始 state 中fetchedfalse,而第一次handleChange之后状态才真正变成{ isSupported: true, fetched: true, level, charging, ... }的完整形态。

五、实战:构建一个电池状态指示器

结合前面三阶段渲染模型与各字段语义,可以构造一个稍完整的实战组件:在电量低于阈值时给出低电量提示,并在充电过程中展示预计充满时间。

import {useBattery} from 'react-use'; const BatteryIndicator = () => { const battery = useBattery(); if (!battery.isSupported) { return <p>当前浏览器不支持 Battery Status API,无法读取电池信息。</p>; } if (!battery.fetched) { return <p>正在获取电池状态…</p>; } const percent = Math.round(battery.level * 100); const isLow = percent <= 20 && !battery.charging; return ( <div> <p>电量:{percent}%({battery.charging ? '充电中' : '使用电池'})</p> {isLow && <p style={{color: 'red'}}>⚠️ 电量过低,请及时充电</p>} {battery.charging ? ( <p> 预计 {formatTime(battery.chargingTime)} 后充满 </p> ) : ( <p> 预计还可使用 {formatTime(battery.dischargingTime)} </p> )} </div> ); }; function formatTime(seconds) { // chargingTime 为 0 表示已充满,dischargingTime 为 0 表示正在放电中 if (!seconds) return '未知'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); return h > 0 ? `${h} 小时 ${m} 分钟` : `${m} 分钟`; }

需要提醒的是,chargingTimedischargingTime的值由浏览器实现决定,某些平台可能返回Infinity0(表示未知/已满/未在充电),因此上例用formatTime0做了兜底处理,这也是文档示例中用batteryState.chargingTime ? ... : 'finished'判断的原因——直接渲染这些原始值很容易出现"0 秒"或"Infinity 秒"这类对用户不友好的展示。

六、兼容性说明与注意事项

结合文档警告与源码行为,使用useBattery时有以下几点需要牢记:

  1. API 已废弃:Battery Status API 处于 obsolete 状态,随时可能从浏览器中移除。useBatteryisSupported分支就是为此设计的降级通道,任何依赖电池数据的业务都必须给出不支持时的替代方案;
  2. 环境检测是静态的:特性检测在模块加载时完成(typeof nav.getBattery === 'function'),如果浏览器后续动态改变能力,本次加载周期内不会重新探测;
  3. 仅适用于浏览器环境:源码通过isNavigator(即typeof navigator !== 'undefined')规避了 SSR / Node 环境下navigator不存在的问题,服务端渲染时useBattery会直接走useBatteryMock分支,返回{ isSupported: false }
  4. 数据是异步就绪的getBattery()返回 Promise,因此组件必然先经历fetched: false的加载态,编写 UI 时不要假定挂载后立刻能读到电池数据;
  5. 事件驱动的实时性:电量、充电状态等变化通过levelchange等事件推送,只要事件正常触发,组件状态就会自动同步,无需手动轮询。

结语

useBattery是 react-use 中典型的传感器类 Hook:它用约 80 行代码完成了特性检测、异步获取、事件订阅、深度比较防抖和卸载清理这一整套闭环。虽然底层 Battery Status API 已处于废弃状态,但在仍支持该 API 的浏览器环境中,它依然是读取与展示电池信息最便捷的 React 封装方案。若要进一步阅读源码或本地预览效果,可以分别查看 src/useBattery.ts 与 stories/useBattery.story.tsx,并通过文档同目录下的 docs/useBattery.md 对照学习。

【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 22:34:56

无网也能写 AI 会议纪要:anarlog 离线模式完整指南

无网也能写 AI 会议纪要&#xff1a;anarlog 离线模式完整指南 【免费下载链接】anarlog Open source Granola AI Alternative 项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog anarlog 的离线模式&#xff1a;一款开源 AI 会议笔记应用&#xff0c;监听你的…

作者头像 李华
网站建设 2026/9/18 22:29:29

代码审查实战:从原则到落地,构建高效Code Review流程

1. 代码审查到底在审什么&#xff1a;先想清楚这件事值不值得做代码审查&#xff08;Code Review&#xff09;这词儿&#xff0c;但凡是写代码的&#xff0c;基本都听过。有些人觉得它是形式主义&#xff0c;走个过场点个赞就完事&#xff1b;有些人觉得它是团队里最有价值的一…

作者头像 李华
网站建设 2026/9/18 22:27:09

Flutter OHOS 端内存与 GPU 问题定位:从原理到实战排查指南

用 Flutter 做 OHOS 端应用&#xff0c;你迟早会撞上内存和 GPU 这两堵墙。我见过太多团队&#xff0c;功能都跑通了&#xff0c;一到真机压测就露馅&#xff1a;内存曲线一路涨不回头&#xff0c;列表滑两页开始掉帧&#xff0c;GPU 占用高得离谱&#xff0c;翻来覆去不知道从…

作者头像 李华
网站建设 2026/9/18 22:25:17

IDEA插件精选指南:提升开发效率的实用组合与避坑技巧

1. 装插件之前&#xff0c;先说说我的筛选标准每次看到有人晒 IDEA 界面&#xff0c;密密麻麻全是插件图标&#xff0c;我就觉得挺有意思——装插件这事&#xff0c;跟买工具很像&#xff0c;看着什么都想要&#xff0c;真正天天用的其实就那么几个。我前后用过至少上百款 IDEA…

作者头像 李华