首页
/ Electron USBDevice 对象详解:WebUSB 设备信息结构、字段语义与浏览器进程生成机制

Electron USBDevice 对象详解:WebUSB 设备信息结构、字段语义与浏览器进程生成机制

2026-09-06 17:23:00作者:鲍丁臣Ursa

本文围绕 Electron 的 USBDevice 结构体展开:先给出该对象的全部属性定义与嵌套层级,再结合浏览器进程源码(shell/browser/usb/)解析这个对象是如何从 Chromium 的 UsbDeviceInfo Mojom 结构逐字段映射生成的,最后结合 session 的 USB 事件与测试用例,说明它在 WebUSB 权限与设备选择流程中的实际用法。读完你可以准确理解 USBDevice 每个字段的含义、哪些字段可能为空,以及它在主进程中的完整使用链路。

USBDevice 对象是什么

USBDevice 是 Electron 在 docs/api/session.md 中定义的 Session USB 事件所使用的数据结构。当渲染进程通过 WebUSB API(navigator.usb)请求设备、或主进程通过 sessionselect-usb-deviceusb-device-addedusb-device-removedusb-device-revoked 等事件处理 USB 权限时,回调参数中携带的设备信息就是 USBDevice 对象。它对齐了 WebUSB 规范中 USBDevice 接口在设备枚举阶段暴露的静态信息:厂商/产品标识、设备版本、USB 协议版本,以及完整的配置(configuration)—接口(interface)—端点(endpoint)描述符树。

USBDevice 属性完整参考

顶层属性

属性 类型 说明
configuration Object(可选) 当前已选中配置的信息,结构同 configurations 数组元素;无活动配置时为 null
configurations Object[] 设备全部可切换配置的数组,每个元素为 USBConfiguration
deviceClass Integer 设备类代码,对应设备描述符中的 class code
deviceId string 设备的唯一标识符(GUID)
deviceProtocol Integer 设备描述符中的协议代码
deviceSubclass Integer 设备描述符中的子类代码
deviceVersionMajor Integer 设备厂商定义的设备版本号主版本
deviceVersionMinor Integer 设备版本号次版本
deviceVersionSubminor Integer 设备版本号子版本
manufacturerName string(可选) 厂商名称
productId Integer USB 产品 ID
productName string(可选) 设备名称
serialNumber string(可选) USB 设备序列号
usbVersionMajor Integer 设备支持的 USB 协议主版本
usbVersionMinor Integer USB 协议次版本
usbVersionSubminor Integer USB 协议子版本
vendorId Integer USB 厂商 ID

注意几个在源码中可确认的可选性约束(详见后文实现分析):manufacturerNameproductNameserialNumber 只有在设备描述符中实际提供了对应字符串时才会出现在对象中;configuration 在无活动配置时为 null 而非缺省。

USBConfiguration 嵌套结构

configurations 数组中每个元素(以及顶层的 configuration)包含:

  • configurationValue Integer — 该配置的配置值;
  • configurationName string — 设备提供用于描述该配置的名称;
  • interfaces Object[] — 该配置下提供的接口数组,每个接口为 USBInterface
    • interfaceNumber Integer — 接口号;
    • alternate Object — 当前选中的替代设置(结构同 USBAlternateInterface);
    • alternates Object[] — 该接口所有可能的替代设置数组。

USBAlternateInterface 嵌套结构

alternatealternates 数组元素包含:

  • alternateSetting Integer — 替代设置号;
  • interfaceClass Integer — 接口类代码,含义参见 USB.org 定义的设备类代码(class codes);
  • interfaceSubclass Integer — 接口子类;
  • interfaceProtocol Integer — 接口支持的协议;
  • interfaceName string(可选)— 接口名称,仅当设备提供时存在;
  • endpoints Object[] — 属于该接口的端点数组,每个端点为 USBEndpoint
    • endpointNumber Integer — 端点号,取值 1~15;
    • direction string — 数据传输方向,'in''out'
    • type string — 端点类型,'bulk''interrupt''isochronous' 三者之一;
    • packetSize Integer — 通过该端点传输的数据被切分成的包大小。

源码解析:USBDevice 对象如何生成

USBDevice 不是手写 JS 对象,而是浏览器进程在枚举设备后由 C++ 层统一转换出来的。核心实现在 DeviceInfoToValue,它把 Chromium 设备服务的 device::mojom::UsbDeviceInfo 逐字段填充进 base::DictValue,与上述字段表一一对应:

  • 顶层字段productName(取 product_name,缺省为空字符串)、vendorIdproductId 总是写入;manufacturerNameserialNumber 仅在 Mojom 中对应字段非空时写入——这正解释了文档中标注的"可选"属性为何可能缺失(见 CanStorePersistentEntry:序列号非空才可作为持久化条目)。deviceId 取的是设备的 guid
  • 端点映射direction 由 Mojom 枚举 UsbTransferDirection::INBOUND 映射为字符串 "in",否则为 "out"usb_chooser_context.cc);typeUsbTransferType 枚举映射为 isochronous / bulk / interrupt 三个字符串,遇到未知类型会触发 NOTREACHED 断言(usb_chooser_context.cc)。
  • 活动配置/活动替代设置:只有当 interface->alternates 中某项的 alternateSetting == 0 时,才会被克隆为接口的 alternate 字段(usb_chooser_context.cc);同理,configuration 字段只有在 device_info.active_configuration 与某个 configurationValue 相等时才会写入,否则在末尾显式置为 nullusb_chooser_context.cc)。
  • 设备列表configurations 由遍历所有配置生成(usb_chooser_context.cc)。

转换结果如何到达 JS 层:usb_device_info_converter.h 中定义了 gin 的 Converter<device::mojom::UsbDeviceInfoPtr>,其 ToV8 直接调用 UsbChooserContext::DeviceInfoToValue 再转成 v8::Value。因此凡是把 UsbDeviceInfo 作为参数传给 Session 事件的回调(例如 select-usb-devicedetails.deviceList),主进程拿到的就是符合本文字段的 USBDevice 对象。

设备过滤:哪些设备根本不会出现在 USBDevice 列表中

在设备进入 deviceList 之前,源码中有两层过滤值得了解:

  1. 大容量存储(Mass Storage)屏蔽ShouldExposeDevice 检查设备的所有配置,若某个配置下所有接口都命中 Mass Storage 类(class_code == 0x08,见 kUsbClassMassStorage),该设备将被整体排除在枚举列表外;注释说明这是为了与 blink::USBDevice::claimInterface() 禁止 claim Mass Storage 接口的行为保持一致。
  2. 黑名单与过滤器UsbChooserController::DisplayDevice 依次用渲染进程传入的 filtersexclusionFilters 以及 Chrome 的 UsbBlocklist 过滤设备;UsbChooserContext::HasDevicePermission 同样会在黑名单未禁用时拒绝授权。

这意味着你写 details.deviceList.find(...) 时,某些物理上存在的设备可能因上述策略而不出现,属于预期行为。

实战:在 Session 事件中使用 USBDevice

USBDevice 主要出现在 docs/api/session.md 描述的四个事件里:

  • select-usb-devicenavigator.usb.requestDevice 被调用且需要选择设备时触发,details.deviceListUSBDevice[],回调传入的 deviceId 即对应对象的 deviceId(GUID)字段;
  • usb-device-added / usb-device-removed:选择流程期间设备热插拔时触发,参数即 USBDevice,用于实时更新选择 UI;
  • usb-device-revoked:页面调用 USBDevice.forget() 后触发,details.device 为被撤销的 USBDevice,可用于维护持久化权限存储。

典型的完整用法(摘自 docs/api/session.md 的官方示例):

const { app, BrowserWindow } = require('electron')

let win = null

app.whenReady().then(() => {
  win = new BrowserWindow()

  win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
    if (permission === 'usb') {
      // Add logic here to determine if permission should be given to allow USB selection
      return true
    }
    return false
  })

  // Optionally, retrieve previously persisted devices from a persistent store
  const grantedDevices = fetchGrantedDevices()

  win.webContents.session.setDevicePermissionHandler((details) => {
    if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'usb') {
      if (details.device.vendorId === 123 && details.device.productId === 345) {
        // Always allow this type of device
        return true
      }
      // Search through the list of devices that have previously been granted permission
      return grantedDevices.some((grantedDevice) => {
        return grantedDevice.vendorId === details.device.vendorId &&
              grantedDevice.productId === details.device.productId &&
              grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
      })
    }
    return false
  })

  win.webContents.session.on('select-usb-device', (event, details, callback) => {
    event.preventDefault()
    const selectedDevice = details.deviceList.find((device) => {
      return device.vendorId === 9025 && device.productId === 67
    })
    if (selectedDevice) {
      grantedDevices.push(selectedDevice)
      updateGrantedDevices(grantedDevices)
    }
    callback(selectedDevice?.deviceId)
  })
})

从这段示例可以看到 USBDevice 字段的实战价值:用 vendorId / productId 做设备类型匹配,用 serialNumber 区分同型号设备(注意源码层面 serialNumber 可能缺失,匹配逻辑必须先判空),最后通过 deviceId 完成选择回调。

另外,主进程还可用 ses.setUSBProtectedClassesHandler(handler) 覆写"受保护 USB 类"列表(如 videoaudiohid 等),返回空数组即放开所有类、原样返回入参数组则维持默认保护策略,详见 docs/api/session.md 对应小节;其字符串类代码与 USBDevicedeviceClass / interfaceClass 的数值类代码是同一套 USB 类定义。

测试用例中的行为验证

spec/chromium-spec.ts 中的 WebUSB 测试对上述机制做了回归验证:

  • 未监听 select-usb-device 时,navigator.usb.requestDevice({filters: []}) 会解析为空并抛出类似 NotFoundError: ... No device selected. 的结果(对应 OnDeviceChosen 中不选设备即回传 nullptr 的逻辑);
  • 定义了 select-usb-device 监听器并回调 deviceId 后,渲染进程收到的对象字符串化结果包含 [object USBDevice],验证了 Mojom → USBDevice → Blink 对象的完整链路;
  • 测试还覆盖了通过 executeJavaScript 调用 device.forget() 等场景,对应 usb-device-revoked 事件(其实现即 RevokeObjectPermissionInternal:持久化设备走 ElectronPermissionManager 撤销,临时设备从 ephemeral_devices_ 移除,并向 Session 发出 usb-device-revoked)。

小结

USBDevice 是 Electron 把 WebUSB 设备描述符暴露给主进程的统一结构:顶层携带厂商/产品/版本等静态标识,configurationsconfiguration 承载"配置—接口—替代设置—端点"的完整描述符树。从 DeviceInfoToValue 的源码可以确认各字段的可选性与 null 语义,理解 Mass Storage 屏蔽、黑名单与过滤器后,你在 select-usb-device 中匹配 vendorId / productId / serialNumber、并用 deviceId 完成授权回调的整套主进程权限管理就能稳定落地。

登录后查看全文
热门项目推荐
相关项目推荐