浏览器访问USB设备曾经是安全策略的禁区,但随着物联网与硬件交互需求的增长,Chrome率先在桌面端提供了WebUSB API,允许网页在用户授权后直接与USB外设通信。对于仍大量使用jQuery的存量项目而言,直接操作Promise链会显得繁琐,将WebUSB封装成jQuery插件能够显著简化调用逻辑。本文将介绍如何利用jQuery封装WebUSB API,实现连接设备、读取数据、写入指令的完整交互流程。

WebUSB API基础与浏览器支持情况
WebUSB是W3C标准草案,定义了网页与USB设备进行安全通信的接口。它基于USB协议栈,允许网页声明特定接口、发送控制传输和批量传输。核心对象包括navigator.usb、USBDevice、USBConfiguration、USBInterface、USBEndpoint等。调用流程通常为:调用navigator.usb.requestDevice弹出设备选择器,获取用户授权的USBDevice实例,然后打开设备、选择配置、声明接口,最后通过transferOut和transferIn方法进行数据传输。
需要注意的是,WebUSB目前仅在基于Chromium的浏览器中得到支持,Firefox和Safari暂未实现。此外,页面必须运行在HTTPS环境或localhost下,且requestDevice调用必须由用户手势触发(如点击按钮)。这些限制意味着封装层需要处理不支持WebUSB时的降级提示,以及手势触发的约束。
理解这些底层细节后,我们才能设计出健壮的jQuery封装。jQuery的核心优势在于其简洁的API风格和事件系统,我们可以将WebUSB的异步流程包装成类似$.ajax的调用方式,同时利用Deferred对象管理Promise链。
封装jQuery插件的设计思路与基本结构
封装的目标是将常用的USB操作抽象为几个易用的方法,例如:$.webusb.connect(filter)用于连接设备,$.webusb.write(data)用于发送数据,$.webusb.read(length)用于接收数据。由于WebUSB的操作依赖设备实例,我们可以在插件内部维护一个当前连接的设备对象,并提供设置过滤规则的方法。
为了避免全局状态污染,可以使用闭包保存私有变量,并通过jQuery的$.extend暴露公共方法。以下是一个基本的封装骨架:
(function($) {
var currentDevice = null;
var currentInterface = null;
var currentEndpointOut = null;
var currentEndpointIn = null;
var webusb = {
isSupported: function() {
return 'usb' in navigator;
},
connect: function(filter) {
var deferred = $.Deferred();
if (!this.isSupported()) {
deferred.reject('WebUSB not supported');
return deferred.promise();
}
navigator.usb.requestDevice({ filters: [filter] })
.then(function(device) {
currentDevice = device;
return device.open();
})
.then(function() {
if (currentDevice.configuration === null) {
return currentDevice.selectConfiguration(1);
}
})
.then(function() {
return currentDevice.claimInterface(0);
})
.then(function(usbInterface) {
currentInterface = usbInterface;
// 查找批量传输端点
usbInterface.alternate.endpoints.forEach(function(endpoint) {
if (endpoint.direction === 'out') {
currentEndpointOut = endpoint;
} else if (endpoint.direction === 'in') {
currentEndpointIn = endpoint;
}
});
deferred.resolve(currentDevice);
})
.catch(function(error) {
deferred.reject(error);
});
return deferred.promise();
},
write: function(data) {
var deferred = $.Deferred();
if (!currentDevice || !currentEndpointOut) {
deferred.reject('Device not connected');
return deferred.promise();
}
currentDevice.transferOut(currentEndpointOut.endpointNumber, data)
.then(function(result) {
deferred.resolve(result);
})
.catch(function(error) {
deferred.reject(error);
});
return deferred.promise();
},
read: function(length) {
var deferred = $.Deferred();
if (!currentDevice || !currentEndpointIn) {
deferred.reject('Device not connected');
return deferred.promise();
}
currentDevice.transferIn(currentEndpointIn.endpointNumber, length)
.then(function(result) {
deferred.resolve(result);
})
.catch(function(error) {
deferred.reject(error);
});
return deferred.promise();
},
close: function() {
if (currentDevice) {
currentDevice.close();
currentDevice = null;
currentInterface = null;
currentEndpointOut = null;
currentEndpointIn = null;
}
}
};
$.webusb = webusb;
})(jQuery);
上述代码展示了基本的封装思路。connect方法接收一个过滤器对象,该对象用于匹配特定厂商ID或产品ID。在成功连接后,我们查找接口0的批量端点并保存引用,之后write和read方法便可以直接调用。使用$.Deferred可以保持异步操作的jQuery风格,方便在调用时使用done和fail回调。
处理设备过滤、批量传输与错误捕获
在实际应用中,设备过滤规则非常关键。WebUSB的requestDevice支持两种过滤方式:按厂商ID和产品ID过滤,或者按设备类别过滤。过滤器对象的形式如{ vendorId: 0x1234, productId: 0x5678 }。如果设备没有特定产品ID,可以使用{ vendorId: 0x1234 }。封装时应当允许调用者传入自定义过滤规则,并保留扩展性。
批量传输是USB通信中最常用的方式,适用于数据量较大且对实时性要求不高的场景。transferOut用于向设备发送数据,第一个参数是端点号,第二个参数是BufferSource类型的数据(如Uint8Array)。transferIn用于从设备读取数据,需要指定期望读取的字节数。需要注意的是,端点号从USB描述符中获取,通常在claimInterface后通过端点列表查找。如果设备有多个接口或多个端点,封装时需要增加参数指定接口号和端点方向。
错误处理也是封装必须考虑的部分。原生Promise的catch只能捕获异步操作中的错误,但用户取消设备选择器时会抛出NotFoundError,设备拔出时会触发disconnect事件。我们可以在插件中增加对navigator.usb的connect和disconnect事件监听,当设备断开时自动清理当前状态,并触发自定义事件通知页面。以下代码展示了如何处理设备断开:
navigator.usb.addEventListener('disconnect', function(event) {
if (currentDevice && event.device === currentDevice) {
currentDevice = null;
currentInterface = null;
currentEndpointOut = null;
currentEndpointIn = null;
$(document).trigger('webusb:disconnect');
}
});
通过监听disconnect事件,我们可以及时更新UI状态,提示用户设备已拔出。同时,在connect方法中应当增加设备已连接时的检查,避免重复打开已关闭的设备。
实际案例:读写自定义USB HID设备
假设我们有一个自定义USB HID设备,厂商ID为0x1234,产品ID为0x5678,接口0包含一个输出端点(端点号1)和一个输入端点(端点号1,方向相反)。设备协议规定,发送3字节指令[0x01, 0x02, 0x03]后,设备会返回4字节状态数据。使用我们封装的jQuery插件,页面代码可以非常简洁:
<button id="connectBtn">连接设备</button>
<button id="sendBtn" disabled>发送指令</button>
<div id="status"></div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="jquery.webusb.js"></script>
<script>
$(function() {
var filter = { vendorId: 0x1234, productId: 0x5678 };
$('#connectBtn').on('click', function() {
$.webusb.connect(filter).done(function(device) {
$('#status').text('设备已连接:' + device.productName);
$('#sendBtn').prop('disabled', false);
}).fail(function(error) {
$('#status').text('连接失败:' + error);
});
});
$('#sendBtn').on('click', function() {
var command = new Uint8Array([0x01, 0x02, 0x03]);
$.webusb.write(command).done(function(result) {
console.log('发送成功,已写入', result.bytesWritten, '字节');
// 等待设备响应
$.webusb.read(4).done(function(readResult) {
var response = new Uint8Array(readResult.data.buffer);
$('#status').text('收到响应:' + Array.from(response).join(','));
}).fail(function(error) {
$('#status').text('读取失败:' + error);
});
}).fail(function(error) {
$('#status').text('发送失败:' + error);
});
});
$(document).on('webusb:disconnect', function() {
$('#status').text('设备已断开');
$('#sendBtn').prop('disabled', true);
});
});
</script>
这个示例展示了完整的连接、写入、读取流程。我们把设备过滤规则作为参数传入connect方法,成功回调中拿到设备信息,然后启用发送按钮。发送指令使用Uint8Array构造二进制数据,写入后立即调用read方法读取响应。整个过程基于jQuery的Deferred和事件机制,符合传统jQuery开发者的使用习惯。
需要注意的是,read方法中readResult.data是DataView类型,需要通过其buffer属性获取底层ArrayBuffer,再转换为Uint8Array以便处理。如果设备返回的数据长度不固定,可以循环读取直到满足协议要求。
安全限制与生产环境注意事项
WebUSB的安全模型非常严格,浏览器只允许在安全上下文(HTTPS或localhost)中访问,并且requestDevice必须由用户手势触发。这意味着你不能在页面加载时自动弹出设备选择器,必须绑定到点击、触摸等事件上。此外,用户每次重新加载页面后都需要重新授权,浏览器不会记住设备权限(除非用户勾选“允许此站点访问”)。
在封装时,应当检测环境是否支持WebUSB,如果不支持则提供优雅降级,例如提示用户使用Chrome浏览器或改用其他通信方式(如串口API)。同时,封装层不应缓存设备实例到全局窗口对象上,以免页面刷新后引用失效。除了设备断开事件,还需要处理连接超时、传输中断等异常情况。
另一个实际问题是设备驱动的冲突。如果操作系统已经为USB设备安装了专用驱动,浏览器可能无法声明接口。此时需要在设备固件中配置为无驱模式或使用WinUSB驱动。对于HID设备,由于操作系统默认会占用HID接口,WebUSB无法直接访问,需要设备固件提供额外的非HID接口。这些都是硬件设计阶段需要考虑的因素。
最后,虽然jQuery封装降低了使用门槛,但开发者仍需理解WebUSB的底层概念,特别是端点、接口和配置的关系。只有在明确设备通信协议的前提下,封装才能发挥最大作用。
通过本文的封装实践,我们看到了jQuery与WebUSB结合的可能性。在存量项目中使用这种封装方式,可以以较小的改动成本实现浏览器直接读写USB设备的功能,为工业调试、硬件配置工具等场景提供便利。

