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:current
BatteryManagerAPI 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>: <span>supported</span> <br /> <strong>Battery state</strong>: <span>fetched</span> <br /> <strong>Charge level</strong>: <span>{ (batteryState.level * 100).toFixed(0) }%</span> <br /> <strong>Charging</strong>: <span>{ batteryState.charging ? 'yes' : 'no' }</span> <br /> <strong>Charging time</strong>: <span>{ batteryState.chargingTime ? batteryState.chargingTime : 'finished' }</span> <br /> <strong>Discharging time</strong>: <span>{ batteryState.dischargingTime }</span> </div> ); };这个示例清晰地展示了useBattery的三阶段渲染模型,这也是实际项目中最推荐的使用范式:
- 不支持(
isSupported === false):浏览器没有getBattery能力,直接展示降级提示; - 获取中(
isSupported === true && fetched === false):API 已确认支持,但异步的电池快照尚未返回,展示"fetching"加载态; - 已获取(
fetched === true):电池数据就绪,此时level、charging、chargingTime、dischargingTime均可安全读取。
在第三步的展示中,有几个值得注意的细节:
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.0到1.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),通过isSupported与fetched两个判别字段将三种状态严格区分开:
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还是空实现useBatteryMock。useBatteryMock永远返回{ 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,这正是fetched从false变为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 中fetched为false,而第一次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} 分钟`; }需要提醒的是,chargingTime与dischargingTime的值由浏览器实现决定,某些平台可能返回Infinity或0(表示未知/已满/未在充电),因此上例用formatTime对0做了兜底处理,这也是文档示例中用batteryState.chargingTime ? ... : 'finished'判断的原因——直接渲染这些原始值很容易出现"0 秒"或"Infinity 秒"这类对用户不友好的展示。
六、兼容性说明与注意事项
结合文档警告与源码行为,使用useBattery时有以下几点需要牢记:
- API 已废弃:Battery Status API 处于 obsolete 状态,随时可能从浏览器中移除。
useBattery的isSupported分支就是为此设计的降级通道,任何依赖电池数据的业务都必须给出不支持时的替代方案; - 环境检测是静态的:特性检测在模块加载时完成(
typeof nav.getBattery === 'function'),如果浏览器后续动态改变能力,本次加载周期内不会重新探测; - 仅适用于浏览器环境:源码通过
isNavigator(即typeof navigator !== 'undefined')规避了 SSR / Node 环境下navigator不存在的问题,服务端渲染时useBattery会直接走useBatteryMock分支,返回{ isSupported: false }; - 数据是异步就绪的:
getBattery()返回 Promise,因此组件必然先经历fetched: false的加载态,编写 UI 时不要假定挂载后立刻能读到电池数据; - 事件驱动的实时性:电量、充电状态等变化通过
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),仅供参考