一文详解为何在Vue3中uni.createCanvasContext画不出canvas

 更新时间:2026年01月20日 11:39:28   作者:超级无敌大蟑王  
这篇文章主要介绍了为何在Vue3中uni.createCanvasContext画不出canvas的相关资料,包括Canvas画布空白、组件作用域不同导致的渲染失败以及如何实现连线效果,文中通过代码介绍的非常详细,需要的朋友可以参考下

1. 现象描述

当我们封装一个通用的手势组件< GestureCheckIn/> 时,最直观的操作就是在父组件中直接引入。

代码看起来非常完美:

<GestureCheckIn @confirm="handleGestureSubmit" />

然而,当我们在 UniApp (Vue 3) 环境下运行这段代码时,经常遇到一种“代码没报错,但画面一片白”的Bug:

  •  Canvas 画布全是空白,无论怎么在这个区域滑动手势,都没有线条出现。

  •  控制台没有报错,  draw() 方法似乎执行了,  console.log 也能打印出坐标点。

而当我们逐步检查时,初始化 Canvas 的代码长这样:

ctx = uni.createCanvasContext('gestureCanvas')

如果我们把这段代码从组件里搬出来,直接写在根页面里,它又是正常的。这背后的根本原因,在于 组件作用域 的不同。

2. 小程序端的陷阱

当你直接调用 时,uni.createCanvasContext('id') UniApp 默认是在 当前页面 的范围内查找这个canvas-id。

但是,现在我们将 Canvas 封装在了一个 自定义组件 内部。为了实现组件化隔离,Vue 3 对组件内部的 DOM 节点进行了封装。

限制一:为何渲染失败?

这是新手在 Vue 3 + UniApp 最容易踩的坑。

如果不传入第二个参数,UniApp 会去页面根节点找 gestureCanvas 。但因为你的 Canvas 藏在 < GestureCheckIn/> 组件的 Shadow DOM 或者组件作用域里,它根本找不到

错误的代码:

// 在 Vue 3 组件中,这样写会导致找不到 Canvas 上下文
ctx = uni.createCanvasContext('gestureCanvas') 

解决方案:显式传入 instance

在 Vue 3 的 setup语法糖中,我们没有 this 。我们需要手动获取当前组件的实例,并把它作为第二个参数传给创建函数,告诉 UniApp:“请在这个组件实例的范围内找 Canvas”

正确的代码(Vue 3 正解):

import { getCurrentInstance, onMounted } from 'vue'

// 获取当前组件实例
const instance = getCurrentInstance()
onMounted(() => {
  // 必须将 instance 传进去!
  ctx = uni.createCanvasContext('gestureCanvas', instance)
  if (ctx) {
     draw() 
  }
})

3. 如何实现连线效果?

解决了画不出来的问题,下一个挑战是:如何让线条既连接已选中的点,又能实时跟随手指移动?

很多新手实现的连线效果往往是“断裂”的,或者只能连接点与点,没有那条“正在寻找下一个点”的动态线。

核心逻辑拆解

要实现完美的连线,我们需要在 draw() 函数中分两步走:

  1. 连接“历史”:画出已经确定的点之间的线段。

  2. 连接“当下”:画出最后一个点到手指当前位置的线段。

关键代码解析

// 绘制连线逻辑
if (selectedIndices.length > 0) {
  ctx.beginPath() // 必须开启新路径,否则会和圆点的绘制混在一起
  
  // 1. 移动画笔到第一个选中的点
  const startPoint = points[selectedIndices[0]]
  ctx.moveTo(startPoint.x, startPoint.y)
  
  // 2. 遍历后续所有已选中的点,将它们连起来
  for (let i = 1; i < selectedIndices.length; i++) {
    const p = points[selectedIndices[i]]
    ctx.lineTo(p.x, p.y)
  }
  
  // 3. 【关键】如果是正在触摸状态,画一条线到当前手指的位置
  // 这就是为什么手势看起来像在“拉橡皮筋”
  if (isDrawing) {
    ctx.lineTo(currentPos.x, currentPos.y)
  }
  
  // 4. 样式设置(改为蓝色主题 #007AFF)
  ctx.setStrokeStyle('#007AFF') 
  ctx.setLineWidth(6)
  
  // 5. 【细节】让线条拐角和端点变得圆润,防止出现锯齿或尖角
  ctx.setLineCap('round') 
  ctx.setLineJoin('round')
  
  ctx.stroke()
}

视觉优化Tips:

  • ctx.beginPath() 的重要性:在 Canvas 中,如果你不重新 beginPath,当你调用 stroke 时,它会把之前画过的所有圆圈再描一遍边,导致性能下降且样式混乱。

  • 动态跟随: 当下的点是在 touchmove 事件中实时更新的。只有将它加入到 lineTo 序列的最后,用户才会感觉线条是“长”在手上的。

  • 圆角处理:默认的线条连接处是尖的,在手势转折时非常难看。设置为 round 可以让折线连接处变得平滑圆润,提升质感。

4. 组件最终实现方案:

<template>
  <view class="gesture-container">
    <canvas canvas-id="gestureCanvas" id="gestureCanvas" class="gesture-canvas" @touchstart="start" @touchmove="move"
      @touchend="end"></canvas>
    <view class="action">
      <button @click="reset">重设手势</button>
      <text class="debug-info">{{ debugInfo }}</text>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted, defineEmits } from 'vue'
import { getCurrentInstance } from 'vue'

// 定义颜色常量
const themeColor = '#007AFF'
const themeColorDark = '#005BBB' 

const instance = getCurrentInstance()
const emit = defineEmits(['confirm'])

let ctx = null
const debugInfo = ref('等待初始化...')

// 基础样式配置
const canvasWidth = 300
const canvasHeight = 300
const r = 25

// 状态
let isDrawing = false
let points = []
let selectedIndices = []
let currentPos = { x: 0, y: 0 }

onMounted(() => {
  setTimeout(() => {
    ctx = uni.createCanvasContext('gestureCanvas', instance)

    if (ctx) {
      initPoints()
      draw()
    }
  }, 200)
})

const initPoints = () => {
  points = []
  const padding = 50
  const stepX = (canvasWidth - 2 * padding) / 2
  const stepY = (canvasHeight - 2 * padding) / 2

  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      points.push({
        x: padding + j * stepX,
        y: padding + i * stepY,
        index: i * 3 + j
      })
    }
  }
  debugInfo.value = '请绘制手势密码(至少连接4个点)'
}

const draw = () => {
  if (!ctx) {
    return
  }

  ctx.clearRect(0, 0, canvasWidth, canvasHeight)

  points.forEach((p, index) => {
    // 绘制外圆路径
    ctx.beginPath()
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2)
    ctx.setLineWidth(3)
    ctx.setStrokeStyle(themeColor)

    if (selectedIndices.includes(index)) {
      // 选中状态:绘制外圈边框 + 内部实心圆点
      ctx.stroke()

      ctx.beginPath()
      ctx.arc(p.x, p.y, r / 3.5, 0, Math.PI * 2)
      // 选中时的内部实心圆点
      ctx.setFillStyle(themeColor)
      ctx.fill()
    } else {
      // 未选中状态:白色填充 + 边框
      ctx.setFillStyle('#ffffff')
      ctx.fill()
      ctx.stroke()
    }
  })

  // 绘制连线
  if (selectedIndices.length > 0) {
    ctx.beginPath()
    const startPoint = points[selectedIndices[0]]
    ctx.moveTo(startPoint.x, startPoint.y)
    for (let i = 1; i < selectedIndices.length; i++) {
      const p = points[selectedIndices[i]]
      ctx.lineTo(p.x, p.y)
    }
    if (isDrawing) {
      ctx.lineTo(currentPos.x, currentPos.y)
    }
    // 连线颜色改为蓝色
    ctx.setStrokeStyle(themeColor)
    ctx.setLineWidth(6)
    ctx.setLineCap('round')
    ctx.setLineJoin('round')
    ctx.stroke()
  }

  ctx.draw()

  if (selectedIndices.length > 0) {
    debugInfo.value = `已连接 ${selectedIndices.length} 个点`
  }
}

const getPosition = (e) => {
  if (!e.touches || !e.touches[0]) {
    return { x: 0, y: 0 }
  }
  // 增加兼容性处理,防止部分环境 e.touches[0] 不包含 x, y
  const touch = e.touches[0]
  return {
    x: touch.x || touch.clientX,
    y: touch.y || touch.clientY
  }
}

const start = (e) => {
  isDrawing = true
  selectedIndices = []
  const pos = getPosition(e)
  currentPos = pos
  checkCollision(pos)
  draw()
}

const move = (e) => {
  if (!isDrawing) return
  const pos = getPosition(e)
  currentPos = pos
  checkCollision(pos)
  draw()
}

const end = (e) => {
  isDrawing = false
  draw()

  if (selectedIndices.length >= 4) {
    const pattern = selectedIndices.join('')
    emit('confirm', pattern)
    debugInfo.value = `手势已确认`
  } else if (selectedIndices.length > 0) {
    debugInfo.value = `至少需要连接 4 个点(已连接 ${selectedIndices.length} 个)`
    setTimeout(() => {
      reset()
    }, 1500)
  }
}

const checkCollision = (pos) => {
  points.forEach((p, i) => {
    const dx = pos.x - p.x
    const dy = pos.y - p.y
    const distance = Math.sqrt(dx * dx + dy * dy)
    if (distance < r && !selectedIndices.includes(i)) {
      selectedIndices.push(i)
    }
  })
}

const reset = () => {
  selectedIndices = []
  isDrawing = false
  currentPos = { x: 0, y: 0 }
  draw()
  debugInfo.value = '请绘制手势密码(至少连接4个点)'
}
</script>

<style lang="scss" scoped>
// 定义 CSS 变量以便样式中使用
$theme-color: #007AFF;
$theme-color-dark: #005BBB;
$theme-shadow-light: rgba(0, 122, 255, 0.1);
$theme-shadow-medium: rgba(0, 122, 255, 0.2);

.gesture-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background: #ffffff;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
  border-radius: 24rpx;
}

.gesture-canvas {
  width: 300px;
  height: 300px;
  background: #ffffff;
  border: 2px solid $theme-color;
  border-radius: 12px;
  box-shadow: 0 2px 8px $theme-shadow-light;
}

.action {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;

  button {
    padding: 12px 30px;
    background: $theme-color;
    color: #ffffff;
    border: none;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 500;
    box-shadow: 0 2px 6px $theme-shadow-medium;

    &:active {
      background: $theme-color-dark;
    }
  }

  .debug-info {
    font-size: 13px;
    color: $theme-color;
    text-align: center;
  }
}
</style>

总结

到此这篇关于为何在Vue3中uni.createCanvasContext画不出canvas的文章就介绍到这了,更多相关Vue3 uni.createCanvasContext画不出canvas内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JS去掉字符串前后空格或去掉所有空格的用法

    JS去掉字符串前后空格或去掉所有空格的用法

    这篇文章主要介绍了JS去掉字符串前后空格或去掉所有空格的用法,需要的朋友可以参考下
    2017-03-03
  • 使用mini-define实现前端代码的模块化管理

    使用mini-define实现前端代码的模块化管理

    这篇文章主要介绍了使用mini-define实现前端代码的模块化管理,十分不错的一篇文章,这里推荐给有需要的小伙伴。
    2014-12-12
  • 一文详解JavaScript中的事件循环(event loop)机制

    一文详解JavaScript中的事件循环(event loop)机制

    JavaScript中的事件循环(Event Loop)是一种重要的机制,用于管理异步代码的执行,它确保 JavaScript 单线程环境中的任务按照正确的顺序执行,同时允许异步操作如定时器、网络请求和事件处理,本将给大家详细的介绍一下JavaScript事件循环机制,感兴趣的朋友可以参考下
    2023-12-12
  • Webpack实现多页面打包的方法步骤

    Webpack实现多页面打包的方法步骤

    本文主要介绍了Webpack实现多页面打包的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • PJBlog插件 防刷新的在线播放器

    PJBlog插件 防刷新的在线播放器

    该播放器类似框架式的~设置在页面底部 即使查看网页的另一个页面,歌曲也不会因为刷新而停止并重新播放
    2006-10-10
  • JS动态遍历json中所有键值对的方法(不知道属性名的情况)

    JS动态遍历json中所有键值对的方法(不知道属性名的情况)

    这篇文章主要介绍了JS动态遍历json中所有键值对的方法,实例分析了针对不知道属性名的情况简单遍历json键值对的操作技巧,需要的朋友可以参考下
    2016-12-12
  • 详解ES6通过WeakMap解决内存泄漏问题

    详解ES6通过WeakMap解决内存泄漏问题

    本篇文章主要介绍了详解ES6通过WeakMap解决内存泄漏问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • 原生js实现轮播图特效

    原生js实现轮播图特效

    这篇文章主要为大家详细介绍了原生js实现轮播图特效,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • TypeScript中正则表达式的用法及实际应用

    TypeScript中正则表达式的用法及实际应用

    正则表达式是处理字符串查找、匹配、替换的非常有效的工具,这篇文章主要介绍了TypeScript中正则表达式的用法及实际应用,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-07-07
  • js实现简单模态框实例

    js实现简单模态框实例

    这篇文章主要为大家详细介绍了js实现简单模态框实例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11

最新评论