导读:本期聚焦于印尼程序员创作的《如何在TypeScript中为WebHID Collection API定义用途页数据类型?》,敬请观看详情。你在处理WebHID设备集合时,是否碰到usagePage和usage全是number,无法一眼看出设备类型?TypeScript默认为HIDCollectionInfo提供的类型过于宽泛,导致代码里充满了魔法数字。本文从HID用途页结构出发,给出一种强类型定义方案,把常见用途页如通用桌面、消费控制、按钮等映射为可读枚举,再结合联合类型和类型守卫,让浏览器获取的集合信息变得类型安全。文中涉及的用途页数值均来自HID Usage Tables规范。文章会展示完整的枚举定义、接口设计和消费集合的实践代码,帮助你在读写报告前准确识别设备功能,减少魔法数字带来的维护成本。

在基于浏览器的 HID 设备交互场景里,WebHID 协议把设备内部的报告描述符解析成集合树,而 TypeScript 内置的 HIDCollectionInfo 类型仅仅把 usagePage 和 usage 标注为 number。这种宽泛处理让开发者面对键盘、鼠标、游戏手柄等不同设备时,不得不手动记住 0x01 配 0x06 代表键盘,0x01 配 0x02 代表鼠标,代码里散落着大量魔法数字。更合适的方式是用枚举、字面量类型和联合类型将这些十六进制数值建模成语义化数据类型,在读取集合信息时直接获得类型提示和自动补全。本文将围绕 WebHID Collection API 的用途页展开,给出可落地的 TypeScript 定义方案。

如何在TypeScript中为WebHID Collection API定义用途页数据类型?

先厘清用途页与集合类型

HID 报告描述符通过用途页(Usage Page)和用途(Usage)共同确定一个控制项的功能。用途页是个 16 位值,高字节通常用作页标识,低字节描述具体用途。比如用途页 0x01 是通用桌面页(Generic Desktop Page),在该页下 0x06 表示键盘、0x02 表示鼠标、0x04 表示摇杆;用途页 0x09 是按钮页,页内任意非零用途都代表一个按钮;用途页 0x0C 是消费控制页,包含音量、播放、浏览器等功能键。WebHID 在 HIDCollectionInfo 中直接暴露了 usagePage 和 usage,但并未提供这些数值的语义映射。

集合本身也有类型约束,浏览器使用的 HIDCollectionType 枚举区分了物理集合、应用集合、逻辑集合、报告集合等。一个设备可能有多个集合,例如带键盘和鼠标的复合设备会同时包含应用集合(Application Collection)和物理集合(Physical Collection)。在写 TypeScript 类型时,除了约束用途页的数值范围,还应该保留集合层级和子集合关系,否则过滤输入报告和输出报告时会丢失上下文。理解这些原始字段是定义数据类型的前提,下一步就是把常用页转成可读枚举。

为常见用途页建立枚举与联合类型

先定义用途页枚举,使用普通 enum 即可保证运行时值和类型名共存。下面列出最常用的几个页面,数值来自 HID Usage Tables 规范。

enum UsagePage {
  GenericDesktop = 0x01,
  Keyboard = 0x07,
  LED = 0x08,
  Button = 0x09,
  Consumer = 0x0C,
  Digitizer = 0x0D,
  VendorDefinedStart = 0xFF00,
  VendorDefinedEnd = 0xFFFF,
}

接着给每个页面下常用的用途编号命名。通用桌面页可以这样写:

enum GenericDesktopUsage {
  Pointer = 0x01,
  Mouse = 0x02,
  Joystick = 0x04,
  GamePad = 0x05,
  Keyboard = 0x06,
  Keypad = 0x07,
  MultiAxisController = 0x08,
  SystemControl = 0x80,
}

消费控制页里的用途数量较多,可以只挑选业务需要用到的部分:

enum ConsumerUsage {
  Volume = 0xE0,
  Mute = 0xE2,
  PlayPause = 0xCD,
  ScanNextTrack = 0xB5,
  ScanPreviousTrack = 0xB6,
  BrowserHome = 0x223,
}

此时可以把 HIDCollectionInfo 包装成一个更精确的类型。下面使用可辨识联合区分不同用途页,键名 usagePage 是判别字段,usage 的类型随页面收窄,这样在 switch 分支里就能拿到具体用途的字面量。

type GenericDesktopCollection = {
  usagePage: UsagePage.GenericDesktop;
  usage: GenericDesktopUsage;
  type: HIDCollectionType;
  children: HIDCollectionInfo[];
  inputReports: HIDReportInfo[];
  outputReports: HIDReportInfo[];
  featureReports: HIDReportInfo[];
};

type ConsumerCollection = {
  usagePage: UsagePage.Consumer;
  usage: ConsumerUsage;
  type: HIDCollectionType;
  children: HIDCollectionInfo[];
  inputReports: HIDReportInfo[];
  outputReports: HIDReportInfo[];
  featureReports: HIDReportInfo[];
};

type KnownHIDCollection = GenericDesktopCollection | ConsumerCollection;

上面代码中的 HIDCollectionType 和 HIDReportInfo 来自浏览器内置的 DOM 类型声明。如果项目使用较旧版本的 TypeScript,可能缺失部分 WebHID 类型,需要手动声明 interface HIDCollectionInfo 和 interface HIDDevice,或者安装最新版 @types/w3c-web-hid。建议优先升级 TypeScript 版本,让内置类型与新规范保持一致。

在读取设备集合时应用类型守卫

浏览器返回的 device.collections 类型是 HIDCollectionInfo[],其中 usagePage 是 number,不会自动识别成前面定义的 KnownHIDCollection。因此需要编写类型守卫函数,对原始数值做运行时检查,只有落在枚举范围内才收窄为自定义类型。下面是一个针对通用桌面页的守卫:

function isGenericDesktopCollection(
  collection: HIDCollectionInfo
): collection is GenericDesktopCollection {
  return collection.usagePage === UsagePage.GenericDesktop
    && Object.values(GenericDesktopUsage).includes(
      collection.usage as GenericDesktopUsage
    );
}

这里用 Object.values 检查 collection.usage 是否属于枚举值,从而避免直接断言导致的误判。如果设备厂商使用自定义用途,不属于任何已知枚举,守卫会返回 false,不会破坏后续逻辑。遍历设备集合时,可以先用守卫过滤已知类型,再按具体功能分发:

async function handleHIDDevice(device: HIDDevice) {
  const collections = device.collections;
  if (!collections) return;

  for (const collection of collections) {
    if (isGenericDesktopCollection(collection)) {
      switch (collection.usage) {
        case GenericDesktopUsage.Keyboard:
          console.log('检测到键盘集合,准备读取按键报告');
          break;
        case GenericDesktopUsage.Mouse:
          console.log('检测到鼠标集合,准备读取位移报告');
          break;
        case GenericDesktopUsage.GamePad:
          console.log('检测到游戏手柄集合,准备读取摇杆报告');
          break;
        default:
          console.log('其他通用桌面用途', collection.usage);
      }
    } else if (collection.usagePage === UsagePage.Consumer) {
      // 可以继续扩展消费者控制集合的处理
      console.log('发现消费控制集合,usage =', collection.usage);
    } else {
      console.log('未识别的集合,usagePage =', collection.usagePage);
    }
  }
}

这段代码展示了类型守卫和分派逻辑的组合。由于 isGenericDesktopCollection 已经将 collection 收窄为 GenericDesktopCollection,在 switch 中 collection.usage 会获得对应的字面量联合类型,编辑器可以给出补全提示,也避免了把鼠标用途和键盘用途混用的错误。对于消费控制页,如果业务里需要精确的 usage 类型,可以按照同样模式补一个 isConsumerCollection 守卫。

处理厂商自定义用途与可维护性

HID 规范把 0xFF00 到 0xFFFF 之间的用途页保留给厂商自定义,很多条形码扫描器、专业测量设备、工业控制器都会使用这个区间。面对这类设备,强类型枚举无法穷举所有可能,但可以保留一个通用的厂商拓展类型,把 usagePage 约束在 UsagePage.VendorDefinedStart 到 UsagePage.VendorDefinedEnd 范围内,usage 继续用 number 并配合注释说明。这样既不会滥用 as any,又给未知设备留出了扩展口。

另外,如果你在多处重复 inputReports、outputReports 等字段,会显得啰嗦。可以用泛型基类提取公共结构,再让具体集合类型继承:

interface BaseHIDCollectionInfo {
  type: HIDCollectionType;
  children: HIDCollectionInfo[];
  inputReports: HIDReportInfo[];
  outputReports: HIDReportInfo[];
  featureReports: HIDReportInfo[];
}

type GenericDesktopCollection = BaseHIDCollectionInfo & {
  usagePage: UsagePage.GenericDesktop;
  usage: GenericDesktopUsage;
};

这种方式比复制粘贴字段更易维护,也适用于新增用途页。定义好类型后,建议在团队中把 KnownHIDCollection 作为统一出口,所有与集合相关的函数签名都使用它,而不是原生的 HIDCollectionInfo。长期来看,当 WebHID 规范更新或 TypeScript 内置类型变化时,你只需要调整自己的联合类型和守卫函数,业务代码几乎不用改动。

WebHID 目前在 Chromium 内核浏览器中可用,实际项目中还应该处理用户授权、设备断开重连、报告读写失败等情况。本文聚焦于用途页数据类型,为这些后续交互提供了清晰的类型基础。只要把集合识别逻辑封装好,后续读取输入报告时就能根据用途页直接知道数据是按键盘按键解析还是按鼠标位移解析,从根源上减少类型不匹配带来的调试成本。

WebHID Collection APITypeScript类型定义用途页数据类型修改时间:2026-09-21 16:29:35

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0921/60124.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。