更新记录
1.0.0(2026-07-19)
首发。实时监控流图 / 示波器——每秒 10 万样本级实时滚动。
- 双模式摄取:
sampled(定频,时间由样本计数推进)/timed(自带毫秒时间戳);LE f32 二进制批推(pushSamples),坏字节/NaN/Inf/时间回退整批原子拒收 - 示波器渲染:每像素列 min/max 包络带 + 中线;增量列缓存(绝对时间锚定)让滚动帧摆脱全量重扫;断流缺口诚实留白(相邻间隔 >4× 中位判断流)
- 阈值告警:高/低双向虚线 + 越线段变色 + 边沿触发事件队列(1024 环形,
drainAlarms一次性取空) - 暂停 / 回看 / 缩放:
pause(冻结钟外供)/resume/setReviewOffset;原生视图暂停态单指平移回看 + 双指捏合缩放时间窗 - 快照 + 统计:
snapshot出 PNG data-URI;getStats(count/min/max/avg/last,窗内无样本诚实返 null);getValuesAt十字线近邻值;getMeta元信息 <uni-nex-stream-view>原生视图(App):时钟自驱滚动(running/pausedprop 控制,不满足即停排帧省电)+ 手势热环路零桥接开销;
平台兼容性
uni-app x(5.14)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| √ | √ | 5.0 | 12 | × | × |
nex-stream 实时监控流图 / 示波器
示波器搬进 uni-app——每秒 10 万样本实时滚动。JS 图表库
setData一秒刷几万点直接卡死的场景,本插件自研 环形缓冲 + 包络降采样原生渲染:现实配置(2 系列 × 1000Hz × 10s 窗)单帧 7.45ms,60fps 预算余量 2.2×(实测)。越线告警 + 暂停回看 + 快照留证——工业巡检 / 设备监控闭环三件套;断流诚实可见:时钟驱动滚动,数据停了图不撒谎(留白,不横线冒充)。
特性
- 10 万样本/秒摄取:8 系列 × 合计 600 万样本环形缓冲;
pushSamples二进制批推(LE f32 字节,避 JSON 序列化开销),单批 1 万样本实测 0.312ms - 双摄取模式:
sampled(定频,如 ADC/音频,时间由样本计数自动推进)/timed(自带毫秒时间戳,如传感器上报,断流缺口如实留白) - 示波器画法:每像素列 min/max 包络带 + 中线,百万级样本一屏尽收不糊成一坨;增量列缓存让滚动帧摆脱全量重扫(较无缓存 -71%)
- 阈值告警:高/低双向告警线虚线绘制 + 越线段变色 + 边沿触发事件(进入越线报一次,持续越线不刷屏);队列缓存 1024 条,组件每帧自动转发
@alarm - 暂停 / 回看 / 缩放:
pause()冻结画面,原生视图单指平移回看历史、双指捏合缩放时间窗,resume()跳回实时 - 快照留证:
snapshot()一次调用出 PNG data-URI,直接喂<内置图像编解码 :src>或上传存档 - 实时统计角标:右上角每系列 min/max/avg 随帧刷新(
getStats亦可单独取,缓存命中路径 0.04ms) <uni-nex-stream-view>原生视图(App):时钟自驱滚动 + 手势热环路全程零桥接开销(Android View / iOS UIView 直贴位图)- 三端同源:App(Android/iOS 原生绑定层) + H5 同一自研原生内核,15 个 API 三端全可用(无 App 专用阉割);H5 组件
<uni-nex-stream-canvas>rAF 帧循环 - 自研 依赖链:无任何 C 依赖
快速上手
1. 建引擎 + 批量推流(三端同一套 API)
import {
createStreamEngine, pushSamples, destroyStreamEngine,
pause, resume, snapshot, getStats, streamVersion
} from '@/uni_modules/nex-stream'
// 建引擎:2 系列(CH1 定频 1000Hz 带告警线;CH2 自带时间戳)
const engineId = createStreamEngine(JSON.stringify({
theme: 'dark',
windowSec: 10,
series: [
{ name: 'CH1', mode: 'sampled', sampleRate: 1000, thresholds: { high: 80, low: -10 } },
{ name: 'CH2', mode: 'timed', color: '#ee6666' }
]
}))
// ⚠️ 时间锚:建引擎时记一个 t0,此后一切时间入参都传「Date.now() − t0」(相对毫秒)。
// 引擎数据时间基是相对毫秒(sampled t 从 0 起步),直接传 Date.now() 壁钟原值 = 窗口与数据
// 永不相交 = 空图 / getStats count=0。这是本插件头号使用陷阱,务必照做(详见契约「时钟语义 ⚠️」)。
const t0 = Date.now()
// 推流姿势(推荐 ≥50ms 批推——见下方「推流最佳实践」)
const timer = setInterval(() => {
const n = 50 // 1000Hz × 50ms = 50 样本/批
const buf = new ArrayBuffer(n * 4) // sampled 系列:每样本 1 个 f32(纯 v)
const dv = new DataView(buf)
for (let i = 0; i < n; i++) {
dv.setFloat32(i * 4, readSensor(), true) // true = little-endian,必须
}
pushSamples(engineId, 0, new Uint8Array(buf))
}, 50)
// 暂停回看 / 快照 / 统计——nowMs 一律传相对锚时间(Date.now() − t0),与数据时间基同基
pause(engineId, Date.now() - t0) // 冻结画面(nowMs 外供——本插件 原生层不取系统时钟)
const png = snapshot(engineId, 750, 400, 2, Date.now() - t0) // data:内置图像编解码/png;base64,...
const stats = JSON.parse(getStats(engineId, 0, Date.now() - t0)) // {count,min,max,avg,last}
resume(engineId)
// 页面卸载:先停定时器,再销毁引擎(JS 侧持有所有权,用完必调)
clearInterval(timer)
destroyStreamEngine(engineId)
timed 系列的字节是 [t, v] 对(每样本 2 个 f32,t=毫秒、须单调不减):dv.setFloat32(i*8, t, true); dv.setFloat32(i*8+4, v, true)。⚠️ timed 的 t 也用相对锚时间(Date.now() − t0),绝不可传 Date.now() 原值——t 过桥是 f32,epoch 量级下 ulp≈131 秒,样本会全部坍缩(详见契约「时钟语义」timed 条款)。
2. App 端:<uni-nex-stream-view> 原生视图(时钟自驱 + 手势)
<template>
<uni-nex-stream-view
:engine-id="engineId" :running="running" :paused="paused" :gesture="true"
:clock-epoch-ms="t0"
style="width: 750rpx; height: 400rpx;"
@alarm="onAlarm" @crosshair="onCrosshair" @viewportchange="onViewport" />
<button @click="togglePause">{{ paused ? '恢复' : '暂停回看' }}</button>
</template>
<script>
import { pause, resume } from '@/uni_modules/nex-stream'
export default {
// ⚠️ t0 = 建引擎时记的 Date.now()(时间锚);:clock-epoch-ms 把它交给组件,组件供时 = 壁钟 − t0,
// 与数据时间基(相对毫秒)同基。不传锚 = 组件用壁钟 epoch 原值 = 空图(见「快速上手 §1」注释)。
data() { return { engineId: 0, t0: -1, running: true, paused: false } },
onHide() { this.running = false }, // 后台页必停自驱(省电,见 FAQ)
onShow() { this.running = true },
methods: {
togglePause() {
// pause 的 nowMs 同样传相对锚时间(Date.now() − t0),与组件 clockEpochMs 同锚
if (this.paused) { resume(this.engineId) } else { pause(this.engineId, Date.now() - this.t0) }
this.paused = !this.paused // ⚠️ pause/resume 后必须同步翻转 paused prop
},
onAlarm(json: string) {
// [{"seriesIndex":0,"kind":"high","t":1234.5,"v":85.2}, ...]——组件每帧自动转发
// ⚠️ 挂了本组件就别再自调 drainAlarms()(队列一次性取空,两个消费者互抢)
},
onCrosshair(json: string) { /* running 态单击:[{"seriesIndex","name","t","v"}...] */ },
onViewport(json: string) { /* 暂停态拖动/捏合结束:getMeta 元信息(含 windowSec/reviewOffsetMs)*/ }
}
}
</script>
手势(内置,gesture prop 可关):暂停态单指平移=回看历史、双指捏合=缩放时间窗;运行态单击=十字线取值(@crosshair)。运行态不支持平移/捏合(实时流不回看——先 pause())。
3. H5 端:<uni-nex-stream-canvas> 组件
<!-- H5 用 canvas 组件(rAF 帧循环);props/事件语义与原生视图对齐(无手势,简版) -->
<!-- :clock-epoch-ms 同原生视图(t0=建引擎时的 Date.now()),不传则组件 attach 时刻自记锚 -->
<uni-nex-stream-canvas :engine-id="engineId" :width-px="375" :height-px="200"
:running="running" :paused="paused" :clock-epoch-ms="t0" @alarm="onAlarm" />
// H5 端 H5 端引擎 异步初始化:建引擎前等一次
// #ifdef H5
import { ensureReady } from '@/uni_modules/nex-stream'
await ensureReady()
// #endif
App 端请直接用
<uni-nex-stream-view>原生视图(本 canvas 组件的 App 分支为占位,v1 不渲染)。H5 端回看/缩放走 API:pause/setReviewOffset/setWindow。
推流最佳实践(≥50ms 批推)
别逐样本推——每次 pushSamples 都要过一次 JS↔原生桥,逐样本推 1000Hz = 每秒 1000 次过桥,桥开销远超数据本身。推荐姿势:
- UTS/JS 定时器 ≥50ms 一批:1000Hz 传感器 = 每批 50 样本;10 万样本/秒满配 = 每批 5000 样本(实测该姿势单批 push 耗时 0.3ms 量级,10 万/s 稳定摄取)。
- bytes 单拷贝:
ArrayBuffer + DataView.setFloat32(offset, v, true)填好一批再new Uint8Array(buf)一次推入;不要逐样本 new。 - 整批原子性:坏字节 / NaN / Inf / timed 时间回退会整批拒收(引擎状态一字不改)——收数据时先把无效值过滤掉再入批。
- sampled 模式时间由推流驱动:时间戳按累计样本数推进(
t = 累计数 × 1000 / sampleRate),推流跟不上实时曲线就会滞后于窗口右缘。跟不上时降sampleRate或改timed模式自带时间戳。
option DSL 速览
{
"theme": "dark", // light | dark(缺省 light)
"windowSec": 30, // 必填,滚动窗口秒数 (0, 300]
"series": [ // 必填,1~8 个
{
"name": "CH1", // 必填,角标/十字线标注
"mode": "sampled", // 必填:sampled(定频)| timed(自带时间戳)
"sampleRate": 10000, // sampled 必填(Hz)
"thresholds": { "high": 80.0, "low": -10.0 }, // 可选,任一方向可缺;high 必须 > low
"color": "#5470c6" // 可选,缺省按主题调色板
}
],
"yRange": null, // [min,max] 固定量程;null=自动(窗口 min/max ±5%)
"showStats": true // 统计角标开关(缺省 true)
}
返回 JSON 形状(字段与 原生实现逐字一致,详见 docs/stream-plugin-contract.md):
// getStats —— 窗内无样本时数值字段为 null、count=0
{ "count": 5, "min": 10.0, "max": 90.0, "avg": 55.0, "last": 70.0 }
// drainAlarms / @alarm —— kind 小写;边沿触发(越线瞬间一条,持续越线不重复)
[{ "seriesIndex": 0, "kind": "high", "t": 1234.5, "v": 85.2 }]
// getValuesAt / @crosshair —— 无值可取的系列不出现在数组里
[{ "seriesIndex": 0, "name": "CH1", "t": 2400.0, "v": 29.0 }]
// getMeta / @viewportchange
{ "paused": false, "windowSec": 30.0, "seriesCount": 2, "sampleCounts": [36000, 120],
"renderMs": 1.85, "reviewOffsetMs": 0.0 }
限制与防护(内核硬校验,超限报错不静默截断)
- series ≤ 8 个;窗口
(0, 300]秒;总样本护栏 600 万(环形按系列均分,超配额自动淘汰最旧——sampled 系列配额 =min(窗秒×采样率×1.2, 600万÷系列数)) - 告警队列 1024 条环形覆盖(取走前最多缓存这么多)
- 渲染尺寸:w/h 必须有限正数;物理像素(w×dpr)≤ 4096、dpr ∈ [1,4]
- 推流字节:长度须对齐(sampled 4 字节/样本、timed 8 字节/样本)、NaN/Inf 拒收、timed 时间不可回退——违反整批拒收
- 网络接入 / 数据持久化(快照 PNG 之外)/ FFT 频谱 / y 轴手势缩放:v1 不承诺(数据谁来推是使用方的事——WebSocket/BLE/传感器收到后
pushSamples)
常见问题
- 整张图都是空的 /
getStats恒count=0? 头号陷阱是时间锚没配对。引擎数据时间基是相对毫秒(sampled t 从 0 起步),你若把Date.now()壁钟原值(≈1.78×10¹²)传给nowMs,窗口[epoch−win, epoch]与数据[0, elapsed]永不相交 → 空图。三个具体坑:① 忘记减锚——建引擎时记const t0 = Date.now(),所有nowMs/tMs入参传Date.now() − t0,组件传:clock-epoch-ms="t0";② timed 传了Date.now()原值——timed 的 t 过桥是 f32,epoch 量级 ulp≈131 秒、样本全坍缩,必须传相对锚 ms;③ 组件晚挂载用 auto 锚——组件缺省clockEpochMs=-1时在 attach 时刻自记锚,若引擎已推流一段时间才挂组件、且页面又直调snapshot(nowMs),两个锚会错位,此时显式传:clock-epoch-ms="t0"统一。(详见契约「时钟语义 ⚠️ 时间锚配对」。) - 图中间怎么有一段空白? 这是断流缺口的诚实渲染:
timed系列相邻样本间隔超过窗口中位间隔 4 倍即判断流、留白不连线(数据没了就是没了,不画横线冒充)。恢复推流后缺口保留在历史里,随窗口滚出。 - 曲线越来越落后于右边缘?
sampled模式时间由累计样本数推进而非壁钟——推流慢于sampleRate声明值曲线就滞后。要么保证按率推满,要么改timed模式自带真实时间戳。 - 会不会很耗电? 运行态原生视图 60fps 时钟自驱常驻重绘,确实吃电——后台页/不可见时必须停:
onHide里把running置 false(或pause()),onShow恢复。不满足running && !paused时组件立即停排帧,零开销。v1 无降帧率选项(v1.1 候选)。 - @alarm 没收到 / 少了? ① 告警是边沿触发:越线瞬间报一条,持续越线不重复报(不是每个越线样本一条)。② 队列一次性取空:挂了组件(
<uni-nex-stream-view>/<uni-nex-stream-canvas>)就别再自调drainAlarms()——组件每帧自动 drain 转发,你手动 drain 会把告警从组件嘴里抢走(反之亦然),两边各拿到一部分。裸用 API(不挂组件)时才手动 drain。③ 暂停期间不出帧,告警在队列里攒着(容量 1024),恢复后第一帧一次性转发。 - 暂停了怎么画面还在动 / 手势不对? JS 侧调
pause()/resume()后必须同步翻转组件的pausedprop——组件靠这个 prop 本地判定自驱与手势模式(不逐帧查询引擎),忘翻转就会状态错位。 - JS 直调
setWindow后手势缩放比例不对? 组件缓存窗口值用于手势换算,下次手势按下时才拉取最新真值——第一段拖动可能用旧值(自愈,非持久错误)。建议改窗走双指捏合手势,或改完让用户重新触摸。 - 暂停态点一下怎么不出十字线? v1 已知边界:暂停态视图无法把触点换算成时间(引擎未暴露冻结时刻),单击不发
@crosshair;运行态单击正常。v1.1 候选修复。 - Android 标准基座报"找不到 jna":原生插件须自定义基座(依赖 原生运行依赖,标准基座不含)。制作自定义基座后正常。
- H5 端
getMeta().renderMs恒为 0? H5 端拿不到高精度计时(技术限制),H5 侧用Date.now()前后各测一次即可。 - sampled 和 timed 怎么选? 数据源定频且不带时间戳(ADC/音频/固定频率轮询)→
sampled(省一半带宽,每样本 4 字节);数据源自带时间戳或频率不稳(网络上报/BLE 通知/事件型)→timed(断流缺口如实可见)。
平台说明
| 端 | 实现 | 说明 |
|---|---|---|
| Android | 原生扩展库(arm64-v8a / armeabi-v7a / x86_64)+ 原生绑定层 Kotlin 绑定 + 原生 View 视图 |
需自定义基座(云打包);时钟自驱+手势热环路零桥接开销 |
| iOS | 原生扩展库(device + simulator)+ 原生绑定层 Swift 绑定 + 原生 UIView 视图 |
宿主工程需开启"支持 Swift";绑定层已验,视图运行时适配下版本跟进 |
| H5 | 同一自研原生内核编译 H5 端引擎(base64 内嵌,免静态资源配置)+ <uni-nex-stream-canvas> canvas 帧循环 |
首次调用前 await ensureReady();15 个 API 与 App 端完全一致 |
技术支持 / 问题反馈:插件评论区或仓库 issue。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 1178
赞赏 0
下载 12438998
赞赏 1934
赞赏
京公网安备:11010802035340号