1. 问题背景与现象描述
最近在uni-app中开发一个直尺测量工具时,遇到了两个非常典型的问题:canvas绘制的刻度不显示,以及测量结果返回NaN。这两个问题看似简单,但排查起来却相当棘手,尤其是当它们同时出现时,很容易让人摸不着头脑。
在实际项目中,我们使用canvas绘制了一个带刻度的直尺,用户可以通过触摸屏幕两点来测量距离。理论上,canvas应该清晰显示毫米刻度,测量结果也应该是一个准确的数字。但现实情况是:刻度线完全看不到,控制台不断输出"Measurement result: NaN"的错误提示。
提示:NaN(Not a Number)是JavaScript中表示"非数字"的特殊值,通常出现在数学运算失败时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心问题诊断与排查思路
2.1 刻度不显示的常见原因
经过反复测试和排查,发现刻度不显示通常由以下几个原因导致:
- Canvas上下文获取失败:没有正确获取2D渲染上下文
- 绘制代码执行时机不当:在canvas未准备好时就执行了绘制
- 样式问题:canvas元素尺寸与样式尺寸不匹配导致的缩放
- 坐标计算错误:刻度线绘制在可视区域之外
- 颜色设置问题:使用了与背景相同的颜色
2.2 测量结果NaN的常见原因
测量结果返回NaN则通常源于:
- 未初始化的变量:参与计算的变量未被正确赋值
- 非数字类型参与计算:如字符串与数字混合运算
- 数学运算错误:如除以零、对负数开平方等
- 事件对象处理不当:触摸坐标提取错误
- 异步数据未就绪:在数据可用前就进行了计算
3. 完整解决方案与代码实现
3.1 基础环境准备
首先确保uni-app项目结构正确,pages.json中配置了需要使用的页面:
json复制{
"pages": [
{
"path": "pages/ruler/index",
"style": {
"navigationBarTitleText": "直尺测量工具"
}
}
]
}
3.2 Canvas刻度绘制解决方案
3.2.1 正确的canvas初始化方式
html复制<template>
<view class="container">
<canvas
canvas-id="rulerCanvas"
id="rulerCanvas"
class="ruler-canvas"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</view>
</template>
关键CSS设置:
css复制.ruler-canvas {
width: 100%;
height: 300px;
background-color: #f5f5f5;
}
3.2.2 可靠的绘制函数实现
javascript复制export default {
data() {
return {
ctx: null,
canvasWidth: 0,
canvasHeight: 0,
dpr: 1
}
},
onReady() {
this.initCanvas()
},
methods: {
async initCanvas() {
// 获取设备像素比
this.dpr = uni.getSystemInfoSync().pixelRatio
// 必须使用nextTick确保DOM就绪
await this.$nextTick()
// 获取canvas节点
const query = uni.createSelectorQuery().in(this)
query.select('#rulerCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) {
console.error('Canvas元素未找到')
return
}
const canvas = res[0].node
this.canvasWidth = res[0].width
this.canvasHeight = res[0].height
// 设置canvas实际绘制尺寸
canvas.width = this.canvasWidth * this.dpr
canvas.height = this.canvasHeight * this.dpr
// 获取2D上下文
this.ctx = canvas.getContext('2d')
this.ctx.scale(this.dpr, this.dpr)
// 绘制刻度
this.drawRuler()
})
},
drawRuler() {
if (!this.ctx) return
// 清除画布
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 设置绘制样式
this.ctx.strokeStyle = '#333'
this.ctx.lineWidth = 1
this.ctx.font = '10px Arial'
this.ctx.fillStyle = '#333'
// 绘制厘米刻度
for (let i = 0; i < this.canvasWidth; i += 10) {
const isCm = i % 100 === 0
const height = isCm ? 20 : 10
this.ctx.beginPath()
this.ctx.moveTo(i, 0)
this.ctx.lineTo(i, height)
this.ctx.stroke()
if (isCm) {
this.ctx.fillText((i / 100).toFixed(0) + 'cm', i + 5, 30)
}
}
}
}
}
3.3 测量功能实现与NaN问题解决
3.3.1 正确的触摸事件处理
javascript复制data() {
return {
startPoint: null,
endPoint: null,
isMeasuring: false
}
},
methods: {
handleTouchStart(e) {
const touch = e.touches[0]
this.startPoint = {
x: touch.x,
y: touch.y
}
this.isMeasuring = true
},
handleTouchMove(e) {
if (!this.isMeasuring) return
const touch = e.touches[0]
this.endPoint = {
x: touch.x,
y: touch.y
}
this.drawMeasurement()
},
handleTouchEnd() {
this.isMeasuring = false
// 可以在这里保存测量结果
},
drawMeasurement() {
if (!this.startPoint || !this.endPoint) return
// 重新绘制刻度(清除之前的测量线)
this.drawRuler()
// 绘制测量线
this.ctx.strokeStyle = '#f00'
this.ctx.lineWidth = 2
this.ctx.beginPath()
this.ctx.moveTo(this.startPoint.x, this.startPoint.y)
this.ctx.lineTo(this.endPoint.x, this.endPoint.y)
this.ctx.stroke()
// 计算并显示距离
const distance = this.calculateDistance(
this.startPoint.x,
this.startPoint.y,
this.endPoint.x,
this.endPoint.y
)
// 显示测量结果
this.ctx.fillStyle = '#f00'
this.ctx.font = '14px Arial'
this.ctx.fillText(
`距离: ${distance.toFixed(1)}mm`,
(this.startPoint.x + this.endPoint.x) / 2,
(this.startPoint.y + this.endPoint.y) / 2 - 10
)
},
calculateDistance(x1, y1, x2, y2) {
// 确保所有参数都是数字
x1 = Number(x1)
y1 = Number(y1)
x2 = Number(x2)
y2 = Number(y2)
// 验证参数有效性
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) {
console.error('无效的坐标值:', {x1, y1, x2, y2})
return 0
}
// 计算两点间距离(勾股定理)
const dx = x2 - x1
const dy = y2 - y1
return Math.sqrt(dx * dx + dy * dy)
}
}
4. 关键问题分析与解决方案
4.1 为什么刻度不显示?
通过排查发现,主要原因有两个:
-
Canvas尺寸问题:在uni-app中,canvas有显示尺寸和实际绘制尺寸两个概念。如果只设置了CSS样式尺寸而没有设置canvas的width/height属性,会导致内容被缩放。
-
绘制时机问题:在组件的mounted生命周期中直接获取canvas上下文并绘制,此时canvas可能还未完全初始化。正确的做法是在onReady生命周期或使用$nextTick确保DOM就绪。
解决方案:
- 显式设置canvas的width/height属性
- 在onReady或使用$nextTick确保绘制时机正确
- 考虑设备像素比(dpr)进行适当缩放
4.2 为什么测量结果是NaN?
NaN问题的根源在于:
-
触摸事件坐标提取不当:直接从event对象获取的可能是字符串类型,参与数学运算前需要转换为数字。
-
未初始化的变量:在endPoint未设置时就尝试计算距离。
-
坐标值为undefined:触摸事件对象结构可能在不同平台有差异。
解决方案:
- 确保所有参与计算的变量都已初始化
- 显式将坐标值转换为Number类型
- 添加参数有效性验证
- 使用默认值处理可能的undefined情况
5. 性能优化与进阶技巧
5.1 减少不必要的重绘
javascript复制// 在data中添加
data() {
return {
lastFrameTime: 0,
frameInterval: 1000 / 30 // 目标30fps
}
}
// 修改handleTouchMove
handleTouchMove(e) {
if (!this.isMeasuring) return
const now = Date.now()
if (now - this.lastFrameTime < this.frameInterval) {
return // 跳过帧以降低重绘频率
}
this.lastFrameTime = now
// 其余逻辑不变
}
5.2 支持横竖屏切换
javascript复制onShow() {
// 监听屏幕旋转
uni.onWindowResize(() => {
this.initCanvas()
})
},
onHide() {
// 移除监听
uni.offWindowResize()
}
5.3 添加测量历史记录功能
javascript复制data() {
return {
measurements: []
}
},
methods: {
handleTouchEnd() {
if (this.startPoint && this.endPoint) {
const distance = this.calculateDistance(
this.startPoint.x,
this.startPoint.y,
this.endPoint.x,
this.endPoint.y
)
this.measurements.unshift({
distance,
time: new Date().toLocaleTimeString(),
points: {
start: {...this.startPoint},
end: {...this.endPoint}
}
})
// 限制历史记录数量
if (this.measurements.length > 10) {
this.measurements.pop()
}
}
this.isMeasuring = false
}
}
6. 跨平台兼容性处理
6.1 处理平台差异
不同平台下触摸事件对象结构可能不同:
javascript复制handleTouchStart(e) {
// 兼容各平台触摸事件差异
const touch = e.touches[0] || e.changedTouches[0]
if (!touch) return
this.startPoint = {
x: Number(touch.x || touch.clientX),
y: Number(touch.y || touch.clientY)
}
this.isMeasuring = true
}
6.2 解决iOS上的显示问题
iOS上可能需要特殊处理:
javascript复制initCanvas() {
// ...其他代码
// iOS特殊处理
if (uni.getSystemInfoSync().platform === 'ios') {
canvas.style.width = `${this.canvasWidth}px`
canvas.style.height = `${this.canvasHeight}px`
}
}
7. 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 刻度完全不显示 | 1. Canvas未正确初始化 2. 绘制时机过早 3. 尺寸设置不当 |
1. 检查canvas-id和获取上下文代码 2. 在onReady或nextTick中绘制 3. 显式设置canvas的width/height属性 |
| 刻度显示模糊 | 未考虑设备像素比(dpr) | 根据dpr缩放canvas实际绘制尺寸 |
| 测量结果NaN | 1. 坐标值未初始化 2. 非数字类型参与计算 3. 触摸事件处理不当 |
1. 初始化所有变量 2. 显式转换为Number 3. 验证事件对象结构 |
| 触摸不灵敏 | 事件监听设置不当 | 确保正确绑定touchstart/touchmove事件 |
| 性能卡顿 | 重绘频率过高 | 限制重绘频率,使用requestAnimationFrame |
| iOS显示异常 | 平台特定样式问题 | 添加iOS特定样式处理 |
8. 实际开发中的经验总结
-
设备像素比是关键:在高DPI设备上,如果不考虑dpr,canvas内容会变得模糊。正确的做法是根据dpr缩放canvas的实际绘制尺寸,同时保持显示尺寸不变。
-
生命周期很重要:uni-app中canvas的初始化时机非常关键。onLoad时组件可能还未挂载,onReady才是更安全的选择。
-
类型检查不能少:JavaScript的弱类型特性容易导致NaN问题,所有从事件对象中获取的值都应该进行类型转换和验证。
-
平台差异要测试:特别是在处理触摸事件时,不同平台的事件对象结构可能有细微差别,务必在真机上进行全面测试。
-
性能优化要趁早:即使是简单的测量工具,不加限制的重绘也可能导致性能问题。合理使用节流和脏检查可以显著提升体验。
这个直尺测量工具的开发过程让我深刻体会到,即使是看似简单的功能,也可能隐藏着许多陷阱。特别是在跨平台开发中,考虑不同设备的特性和平台的差异至关重要。希望这篇避坑指南能帮助你在uni-app的canvas开发中少走弯路。
