Generic Sensor API是W3C提出的一套统一传感器访问规范,它把Accelerometer、Gyroscope、Gyroscope、Magnetometer等传感器抽象成统一的基类接口,开发者只需要面对一致的事件模型和生命周期方法。不过原生API上手并不轻松:创建传感器实例、监听error事件、处理权限申请、读取reading回调,每一步都要写不少样板代码。如果把这套逻辑封装成一个jQuery插件,调用时只要几行代码就能拿到实时数据,再配合Canvas绘制曲线,就能快速做出流畅的传感器数据可视化页面。

Generic Sensor API基础与浏览器兼容性
Generic Sensor API的核心是Sensor基类,各类传感器继承自它。以加速度计为例,创建实例后调用start()开始采样,通过onreading事件获取数据,数据存放在实例的x、y、z属性上。需要注意的是,这个API要求页面运行在HTTPS环境下,否则传感器构造函数会直接抛出异常。
兼容性方面,Chrome和Edge对Generic Sensor API支持较好,Firefox和Safari的支持则相对滞后。因此在封装之前必须做特性检测,可以用typeof判断构造函数是否存在:
function sensorSupported(name) {
return typeof window[name] === 'function';
}
// 使用示例
if (sensorSupported('Accelerometer')) {
console.log('当前浏览器支持加速度计');
} else {
console.log('请使用支持Generic Sensor API的浏览器');
}另外要注意权限问题。加速度计和陀螺仪属于需授权的传感器,部分浏览器(如Android Chrome)会弹出授权提示,被拒绝后会触发SecurityError。封装时应当把权限请求也纳入插件流程,而不是让使用者在业务代码里自己处理。
jQuery插件封装架构设计
封装的目标是隐藏传感器实例的创建、错误处理和生命周期管理,让使用者通过熟悉的jQuery事件机制订阅数据。整体架构分三层:底层是传感器实例管理器,负责创建具体的传感器并统一收集错误;中间层是数据分发器,把onreading回调转成jQuery自定义事件;上层是插件入口,暴露$.fn.sensorViz方法,支持链式调用。
插件主体实现如下,核心思路是把每个传感器包装成一个数据源,通过trigger向宿主元素派发sensors:update事件:
(function ($) {
var sensorClasses = {
accelerometer: 'Accelerometer',
gyroscope: 'Gyroscope'
};
function SensorWrapper(type, options) {
this.type = type;
this.options = $.extend({
frequency: 20,
onError: $.noop
}, options);
this.data = { x: 0, y: 0, z: 0 };
this.instance = null;
}
SensorWrapper.prototype.start = function ($host) {
var self = this;
var SensorClass = window[sensorClasses[this.type]];
if (typeof SensorClass !== 'function') {
this.options.onError(new Error('当前浏览器不支持 ' + this.type));
return this;
}
try {
// 传入采样频率,单位Hz,这里设为20次每秒
this.instance = new SensorClass({ frequency: this.options.frequency });
this.instance.addEventListener('error', function (event) {
self.options.onError(event.error);
});
this.instance.addEventListener('reading', function () {
self.data.x = self.instance.x || 0;
self.data.y = self.instance.y || 0;
self.data.z = self.instance.z || 0;
// 将最新数据以自定义事件的形式派发出去
$host.trigger('sensors:update', [self.type, $.extend({}, self.data)]);
});
this.instance.start();
} catch (e) {
this.options.onError(e);
}
return this;
};
SensorWrapper.prototype.stop = function () {
if (this.instance) {
this.instance.stop();
this.instance = null;
}
return this;
};
$.sensorWrapper = function (type, options) {
return new SensorWrapper(type, options);
};
})(jQuery);这段代码里有两个细节值得注意。一是读取数据时做了|| 0的兜底,因为部分传感器在首次触发前属性值可能是null;二是派发事件时用$.extend({}, this.data)复制了一份快照,避免外部拿到内部可变对象的引用造成数据污染。
有了包装器之后,再封装一个简单的插件方法,把传感器生命周期挂到某个DOM元素上:
(function ($) {
$.fn.sensorViz = function (types, options) {
return this.each(function () {
var $host = $(this);
var wrappers = {};
var typesArr = $.isArray(types) ? types : [types];
$.each(typesArr, function (i, type) {
wrappers[type] = $.sensorWrapper(type, {
frequency: (options && options.frequency) || 20,
onError: function (err) {
$host.trigger('sensors:error', [type, err]);
}
}).start($host);
});
// 把包装器存到元素数据上,方便后续停止
$host.data('sensorViz', wrappers);
});
};
$.fn.sensorVizStop = function () {
return this.each(function () {
var wrappers = $(this).data('sensorViz');
if (wrappers) {
$.each(wrappers, function (type, wrapper) {
wrapper.stop();
});
}
});
};
})(jQuery);使用时非常简洁,两行代码就能同时监听加速度计和陀螺仪,并保持jQuery的链式调用习惯:
$('#chart')
.on('sensors:update', function (e, type, data) {
console.log(type, data.x.toFixed(2), data.y.toFixed(2), data.z.toFixed(2));
})
.on('sensors:error', function (e, type, err) {
alert(type + ' 出错:' + err.message);
})
.sensorViz(['accelerometer', 'gyroscope'], { frequency: 30 });用Canvas实现实时曲线绘制
数据拿到之后,可视化部分建议用Canvas而不是操作DOM节点。传感器采样频率通常在20到60Hz,如果每秒往页面里插入几十个DOM元素,浏览器很快就会卡顿。Canvas只需要每帧重绘一次位图,性能开销稳定得多。
绘制策略采用环形缓冲区加滚动曲线:维护一个固定长度的数组,新数据从尾部压入,超出容量就从头部丢弃,画布每帧整体左移重绘。下面是一个支持多通道的简易绘图器:
function RollingChart(canvas, capacity) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.capacity = capacity || 200;
this.channels = {}; // 每个传感器一组曲线
this.colors = ['#e74c3c', '#2ecc71', '#3498db', '#9b59b6'];
this._raf = null;
}
RollingChart.prototype.push = function (seriesKey, value) {
var series = this.channels[seriesKey] || (this.channels[seriesKey] = []);
series.push(value);
if (series.length > this.capacity) {
series.shift(); // 超出容量时移除最早的数据
}
if (!this._raf) {
var self = this;
this._raf = requestAnimationFrame(function () {
self.draw();
self._raf = null;
});
}
};
RollingChart.prototype.draw = function () {
var ctx = this.ctx;
var w = this.canvas.width, h = this.canvas.height;
ctx.clearRect(0, 0, w, h);
// 画网格背景
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
for (var gy = 0; gy <= h; gy += h / 4) {
ctx.beginPath();
ctx.moveTo(0, gy);
ctx.lineTo(w, gy);
ctx.stroke();
}
var self = this;
var colorIndex = 0;
$.each(this.channels, function (key, series) {
if (series.length < 2) return;
var color = self.colors[colorIndex++ % self.colors.length];
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
for (var i = 0; i < series.length; i++) {
// y轴做归一化,取值范围按传感器类型调整
var x = (i / (self.capacity - 1)) * w;
var y = h / 2 - (series[i] / 15) * (h / 2);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
});
};这里有一个关键的性能优化点:push方法里没有直接重绘,而是通过requestAnimationFrame合并同一帧内的多次推送。即使加速度计和陀螺仪在同一帧各推送三个轴的数据,实际绘制也只发生一次。归一化的除数需要根据传感器调整,加速度计通常取15(覆盖正常重力范围加余量),陀螺仪角速度可以取3左右。
把绘图器和插件串起来,就得到一个完整可用的可视化页面:
$(function () {
var chart = new RollingChart(document.getElementById('chart'), 240);
$('#chart')
.on('sensors:update', function (e, type, data) {
chart.push(type + '-x', data.x);
chart.push(type + '-y', data.y);
chart.push(type + '-z', data.z);
})
.sensorViz(['accelerometer', 'gyroscope'], { frequency: 30 });
});常见坑与进阶建议
实际部署时会遇到几个典型问题。第一是屏幕锁定,手机息屏或切到后台后,传感器回调会暂停甚至传感器被系统回收,回到页面时要监听visibilitychange事件重新调用start()。第二是iOS的限制,Safari目前对Generic Sensor API支持不完整,如果需要兼容iOS,可以准备DeviceMotionEvent作为降级方案,在插件内部做自动切换。
第三是采样频率的选择。频率设得越高数据越平滑,但耗电越快,可视化场景一般30Hz就足够了,真机调试时可以观察timestamp属性来确认实际采样率是否符合预期。第四个坑是Canvas的高清屏适配,手机上devicePixelRatio普遍大于1,画布要按比例放大后再缩小绘制,否则曲线会发虚:
function setupHiDPI(canvas) {
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
var ctx = canvas.getContext('2d');
// 将坐标系缩放,绘制逻辑仍可按CSS像素写
ctx.scale(dpr, dpr);
return ctx;
}如果后续需要进一步扩展,可以在这个架构上加入数据录制与导出功能,把环形缓冲区换成可增长数组并提供CSV下载;或者接入WebSocket把传感器数据实时推到服务端做远程监控。整个封装的核心价值在于:把浏览器原生的命令式API转换成jQuery社区熟悉的事件驱动模型,业务代码只关心数据怎么用,不用操心传感器怎么管。
jQueryGeneric Sensor API传感器数据可视化修改时间:2026-09-04 11:37:01