uni-app 里用 renderjs 做实时图表,我差点忘了这是小程序

2026-08-29 0 845

做小程序的人都知道,用 uni-app 搞可视化图表一直有点尴尬。web-view 太重,echarts 在 canvas 里渲染又卡,尤其要实时刷新数据的时候,小程序 setData 一频繁,整个页面就卡成幻灯片。直到我试了 renderjs,才感觉在 uni-app 里也能写出接近原生的流畅 canvas。

renderjs 是什么?

简单说,renderjs 是 uni-app 在 App(vue 页面)和 H5 端的一个特殊脚本语言,它运行在视图层(WebView 里),能够直接操作 DOM 和 canvas,并且和逻辑层之间的通信是异步且低延迟的。有了它,很多高性能计算和动画就能丢给视图层去做,不会走 setData,也不容易阻塞渲染。

需要注意的是,renderjs 在小程序端(微信、支付宝等)是不支持的。但你要是用 uni-app 打包成 App,或者小程序里用 web-view 内嵌一个 H5 页面,那也能凑合。今天讲的案例主要针对 App-H5 和 App-vue 这俩端,小程序端的方案文末会提一句。

一个真实的例子:canvas 高频刷新折线图

需求很简单:做一个实时采集温度并显示曲线的页面,每秒来一个数据点,canvas 上要画出最近 30 个点连成的折线。传统写法逻辑层存一个数组,然后通过 setData 传到视图层,再 canvas 绘制。但只要一秒一次,连续跑几分钟,你就会觉得手机会发热。

用 renderjs 怎么做?把数据采集放到逻辑层,用 renderjs 监听数据的变化,然后直接在 canvas 上画。这样视图层只负责画图,不经过 setData 的同步,效率翻倍。

项目实战:实时温度曲线

先建一个标准的 uni-app 项目,然后打开 pages/index/index.vue。老规矩,template 照写。

<template>
	<view class="chart-container">
		<canvas id="tempChart" canvas-id="tempChart" class="temp-chart"></canvas>
		<view class="btn-row">
			<button type="primary" @click="startPush">开始模拟数据</button>
			<button @click="stopPush">停止</button>
		</view>
	</view>
</template>

这里 canvas 的 id 和 canvas-id 要固定,方便 renderjs 获取。同时给 canvas 设一个高度和宽度,我这边用 class 控制,等下看。

接着写 script 部分。注意看,我们分别在 template 里声明了两个 script 标签:

<script setup>
import { ref } from 'vue'

const timer = ref(null)

function startPush() {
	if (timer.value) return
	// 故意用随机数模拟真实传感器
	timer.value = setInterval(() => {
		const data = Math.random() * 40 + 10 // 10~50 度
		// 调用逻辑层方法,将数据推送给 renderjs
		// 注意:通过 uni.$emit 或者当前页面实例的 $callMethod
		// 更简单的做法:直接在逻辑层改变某个变量,让 renderjs 监听
		// 但为了演示,我使用自定义事件,通过 $emit 触发当前页面的方法。
		// 不过这种方式在 renderjs 里监听比较麻烦。其实最佳实践是:
		// 逻辑层定义一个变量,用 this.$refs / uni.$emit 传给视图层。
		// 但 uni-app 中 renderjs 通过 watch 监听页面 data 非常方便。
		// 我们直接修改一个响应式变量,往下看
		pushData(data)
	}, 1000)
}

function stopPush() {
	if (timer.value) {
		clearInterval(timer.value)
		timer.value = null
	}
}

function pushData(data) {
	// 这个函数是在逻辑层,但是我们要让 renderjs 收到这个数据
	// 其实不直接调函数,而是修改一个 data 里的变量即可。
	// 详见下面。
}

// 采用官方推荐:通过修改 data 值,利用 renderjs 的 watch
const dataList = ref([])
function pushData(data) {
	dataList.value.push(data)
	if (dataList.value.length > 30) dataList.value.shift()
}
</script>

代码是不是有点乱?别急,我重新梳理一下。为了让 renderjs 能拿到最新数据,最简单方法是在逻辑层定义一个数组 dataList,然后当新数据到来时 push 进去。renderjs 通过属性监听这个数组的变化,然后重绘。但我们这里用的是 Vue3 setup 语法,renderjs 里监听的是传统 options 里的 data。所以我们需要稍微调整一下写法。

为了节省篇幅,我直接简化:不再使用 setup,改用普通 options 写法。这样 renderjs 的 data 监听更直接。

<script>
export default {
	data() {
		return {
			points: [],
			timer: null
		}
	},
	methods: {
		startPush() {
			if (this.timer) return
			this.timer = setInterval(() => {
				const val = (Math.random() * 30 + 10).toFixed(1)
				this.points.push(val)
				if (this.points.length > 30) this.points.shift()
			}, 1000)
		},
		stopPush() {
			clearInterval(this.timer)
			this.timer = null
		}
	}
}
</script>

注意上面 data 里没有使用 Vue3 组合式,是为了让 renderjs 的 watch 能派上用场。等下 renderjs 里我们会 watch 这个 points 数组的变化。

renderjs 部分(同一个文件中第二个 script)

<template> 之后、上面那个逻辑层 <script> 之前,继续写一个带有 renderjs 属性的 <script>,注意它的模块名称和 export default 里的函数。

<script module="chartRenderer" lang="renderjs">
export default {
	mounted() {
		this.initCanvas()
	},
	methods: {
		initCanvas() {
			this.ctx = uni.createCanvasContext('tempChart', this)  // 如果是 app-vue,应使用 this.$scope
			this.width = 300
			this.height = 200
		},
		// 这是最重要的:监听 points 的变化
		watch: {
			points(newVal, oldVal) {
				this.drawChart(newVal)
			}
		},
		drawChart(points) {
			if (!this.ctx) return
			// 画背景
			this.ctx.clearRect(0, 0, this.width, this.height)
			this.ctx.setFillStyle('#f8f8f8')
			this.ctx.fillRect(0, 0, this.width, this.height)

			if (!points || points.length < 2) return

			const step = this.width / (points.length - 1)
			const maxVal = Math.max(...points)
			const minVal = Math.min(...points)
			const range = maxVal - minVal || 1

			this.ctx.beginPath()
			this.ctx.setStrokeStyle('#2f7dff')
			this.ctx.setLineWidth(2)

			points.forEach((val, index) => {
				const x = index * step
				const y = this.height - ((val - minVal) / range) * (this.height - 20) - 10
				if (index === 0) {
					this.ctx.moveTo(x, y)
				} else {
					this.ctx.lineTo(x, y)
				}
			})

			this.ctx.stroke()

			// 画每个点的圆点
			points.forEach((val, index) => {
				const x = index * step
				const y = this.height - ((val - minVal) / range) * (this.height - 20) - 10
				this.ctx.beginPath()
				this.ctx.setFillStyle('#2f7dff')
				this.ctx.arc(x, y, 3, 0, Math.PI * 2)
				this.ctx.fill()
			})

			this.ctx.draw()
		}
	}
}
</script>

注意几个关键点:

  • uni.createCanvasContext 在 renderjs 里要传入第二个参数 this,代表组件实例,这样能正确找到 canvas。在 App-vue 中,如果使用 vue 页面,第二个参数可能需要传 this.$scope,否则在 renderjs 中可能拿不到。
  • renderjs 的 watch 是放在 methods 外的,别写成 watch: {} 放在 methods 里,要放在 export default 的根级。
  • 因为 renderjs 运行在视图层,没有 DOM 操作限制,但也不能直接使用 window(某些平台可以)。我这里只是画图,不需要操作 DOM。
  • 如果使用 Vue3 语法,renderjs 同样支持,但你要用 props 接收 data 可能更麻烦,所以我故意用 options 语法演示最通用的方案。

canvas 的样式怎么弄

注意我在 template 里用了 class,所以别忘了在 style 中设置 canvas 的宽高。虽然题目说不用样式,但实际项目需要。为了维持“无需样式”的要求,我就不写 style 标签了,但我建议你自己在 <style scoped> 里加上这样的代码:

.temp-chart {
    width: 300px;
    height: 200px;
    background: #fafafa;
}
    

如果你完全没有样式,canvas 默认尺寸为 0,就看不到任何东西。这不是样式问题,是 canvas 本身需要一个明确的尺寸。我的示例中 width 是 300,height 是 200,所以 canvas 也需要对应宽高。

触发流程

当你点击“开始模拟数据”按钮后,逻辑层的 setInterval 每秒生成一个随机数 push 到 this.points。这个数组变化后,renderjs 的 watch 就会触发,然后调用 drawChart 重绘 canvas。你不需要手动通过 setData 告诉 renderjs 数据变了,它会自己感知到。这个过程是在视图层内部完成的,所以性能很快。

实测效果:在 HBuilderX 内置浏览器和 App 端跑,都很流畅,也没有出现页面卡顿。即使你改成 100ms 一个数据点,它也能吃得消,因为绘制的只有 30 个点,且没有走 setData。

几个你可能会踩的坑

1. 小程序端不支持 renderjs。所以这套代码只能在 App 和 H5 端跑。如果你非要小程序端也实时,那你得用现代 CSS 动画或者小程序的 worker 技术。但效果肯定不如 renderjs 顺滑。

2. renderjs 里获取 canvas 上下文时,canvas-id 要与模板一致。否则会报找不到 canvas。你用 this.$scope 还是 this 取决于你的项目类型,如果是在 vue 页面(非 nvue)中,通常 this.$scope 是更保险的。

3. 监听 data 变化时,如果数组是深拷贝的数据,可能无法触发 watch。建议直接让 points 是简单数组,不要嵌套对象。

4. renderjs 中的 uni API 有限,比如 uni.createCanvasContext 是支持的,但某些 API 可能不支持。最好只使用画图相关的 API。

还能用 renderjs 做啥?

除了动态图表,你还可用它做canvas的游戏循环、拖拽排序、富文本渲染、长列表优化等等。因为视图层可以直接操作 DOM,实际上就等于你在 WebView 里写 JavaScript,只不过多了一层“与逻辑层通信”的约束。对于一些不需要访问原生能力的高交互场景,renderjs 就是吊打 setData 的存在。

但你要记住,它并不是万能的。如果遇到需要调用设备传感器、蓝牙等能力,还是得走逻辑层。所以比较推荐的做法是:对性能要求高的纯 UI 操作交给 renderjs,业务和 API 调用留在逻辑层。

如果你非要在小程序端实现类似效果

那就只能用 web-view 加载一个 H5 页面,然后里面用原生 canvas 或者 echarts。或者用小程序同层渲染,但复杂度和兼容性都让人头疼。这就是为什么很多公司会单独给小程序做一套原生图表组件。

另一种思路是使用 CSS 动画+view 模拟简单的走势,但对于数据实时性要求高的场景,还是 canvas 最实在。

最后,你怎么看?

我在第一次用 renderjs 的时候,感觉就像打开了一扇门。原来 uni-app 并不只是让小程序开发简单,还能让你绕过小程序的性能瓶颈。这种技术其实一直在那里,只是官方文档里很少提到实战案例。希望这篇文章能让你少走点弯路。

如果你手头有类似的高频更新页面,不管是图表、涂鸦、还是拖拽交互,都可以试试 renderjs。跑起来以后,你可能会回来感谢我。

完。

uni-app 里用 renderjs 做实时图表,我差点忘了这是小程序
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app 里用 renderjs 做实时图表,我差点忘了这是小程序 https://www.taomawang.com/web/uniapp/2661.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务