一文詳解為何在Vue3中uni.createCanvasContext畫不出canvas
1. 現(xiàn)象描述
當(dāng)我們封裝一個通用的手勢組件< GestureCheckIn/> 時,最直觀的操作就是在父組件中直接引入。
代碼看起來非常完美:
<GestureCheckIn @confirm="handleGestureSubmit" />
然而,當(dāng)我們在 UniApp (Vue 3) 環(huán)境下運行這段代碼時,經(jīng)常遇到一種“代碼沒報錯,但畫面一片白”的Bug:
Canvas 畫布全是空白,無論怎么在這個區(qū)域滑動手勢,都沒有線條出現(xiàn)。
控制臺沒有報錯, draw() 方法似乎執(zhí)行了, console.log 也能打印出坐標點。
而當(dāng)我們逐步檢查時,初始化 Canvas 的代碼長這樣:
ctx = uni.createCanvasContext('gestureCanvas')如果我們把這段代碼從組件里搬出來,直接寫在根頁面里,它又是正常的。這背后的根本原因,在于 組件作用域 的不同。
2. 小程序端的陷阱
當(dāng)你直接調(diào)用 時,uni.createCanvasContext('id') UniApp 默認是在 當(dāng)前頁面 的范圍內(nèi)查找這個canvas-id。
但是,現(xiàn)在我們將 Canvas 封裝在了一個 自定義組件 內(nèi)部。為了實現(xiàn)組件化隔離,Vue 3 對組件內(nèi)部的 DOM 節(jié)點進行了封裝。
限制一:為何渲染失???
這是新手在 Vue 3 + UniApp 最容易踩的坑。
如果不傳入第二個參數(shù),UniApp 會去頁面根節(jié)點找 gestureCanvas 。但因為你的 Canvas 藏在 < GestureCheckIn/> 組件的 Shadow DOM 或者組件作用域里,它根本找不到
錯誤的代碼:
// 在 Vue 3 組件中,這樣寫會導(dǎo)致找不到 Canvas 上下文
ctx = uni.createCanvasContext('gestureCanvas')
解決方案:顯式傳入 instance
在 Vue 3 的 setup語法糖中,我們沒有 this 。我們需要手動獲取當(dāng)前組件的實例,并把它作為第二個參數(shù)傳給創(chuàng)建函數(shù),告訴 UniApp:“請在這個組件實例的范圍內(nèi)找 Canvas”。
正確的代碼(Vue 3 正解):
import { getCurrentInstance, onMounted } from 'vue'
// 獲取當(dāng)前組件實例
const instance = getCurrentInstance()
onMounted(() => {
// 必須將 instance 傳進去!
ctx = uni.createCanvasContext('gestureCanvas', instance)
if (ctx) {
draw()
}
})
3. 如何實現(xiàn)連線效果?
解決了畫不出來的問題,下一個挑戰(zhàn)是:如何讓線條既連接已選中的點,又能實時跟隨手指移動?
很多新手實現(xiàn)的連線效果往往是“斷裂”的,或者只能連接點與點,沒有那條“正在尋找下一個點”的動態(tài)線。
核心邏輯拆解
要實現(xiàn)完美的連線,我們需要在 draw() 函數(shù)中分兩步走:
連接“歷史”:畫出已經(jīng)確定的點之間的線段。
連接“當(dāng)下”:畫出最后一個點到手指當(dāng)前位置的線段。
關(guān)鍵代碼解析
// 繪制連線邏輯
if (selectedIndices.length > 0) {
ctx.beginPath() // 必須開啟新路徑,否則會和圓點的繪制混在一起
// 1. 移動畫筆到第一個選中的點
const startPoint = points[selectedIndices[0]]
ctx.moveTo(startPoint.x, startPoint.y)
// 2. 遍歷后續(xù)所有已選中的點,將它們連起來
for (let i = 1; i < selectedIndices.length; i++) {
const p = points[selectedIndices[i]]
ctx.lineTo(p.x, p.y)
}
// 3. 【關(guān)鍵】如果是正在觸摸狀態(tài),畫一條線到當(dāng)前手指的位置
// 這就是為什么手勢看起來像在“拉橡皮筋”
if (isDrawing) {
ctx.lineTo(currentPos.x, currentPos.y)
}
// 4. 樣式設(shè)置(改為藍色主題 #007AFF)
ctx.setStrokeStyle('#007AFF')
ctx.setLineWidth(6)
// 5. 【細節(jié)】讓線條拐角和端點變得圓潤,防止出現(xiàn)鋸齒或尖角
ctx.setLineCap('round')
ctx.setLineJoin('round')
ctx.stroke()
}
視覺優(yōu)化Tips:
ctx.beginPath() 的重要性:在 Canvas 中,如果你不重新 beginPath,當(dāng)你調(diào)用 stroke 時,它會把之前畫過的所有圓圈再描一遍邊,導(dǎo)致性能下降且樣式混亂。
動態(tài)跟隨: 當(dāng)下的點是在 touchmove 事件中實時更新的。只有將它加入到 lineTo 序列的最后,用戶才會感覺線條是“長”在手上的。
圓角處理:默認的線條連接處是尖的,在手勢轉(zhuǎn)折時非常難看。設(shè)置為 round 可以讓折線連接處變得平滑圓潤,提升質(zhì)感。
4. 組件最終實現(xiàn)方案:
<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">重設(shè)手勢</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('等待初始化...')
// 基礎(chǔ)樣式配置
const canvasWidth = 300
const canvasHeight = 300
const r = 25
// 狀態(tài)
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)) {
// 選中狀態(tài):繪制外圈邊框 + 內(nèi)部實心圓點
ctx.stroke()
ctx.beginPath()
ctx.arc(p.x, p.y, r / 3.5, 0, Math.PI * 2)
// 選中時的內(nèi)部實心圓點
ctx.setFillStyle(themeColor)
ctx.fill()
} else {
// 未選中狀態(tài):白色填充 + 邊框
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 }
}
// 增加兼容性處理,防止部分環(huán)境 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>總結(jié)
到此這篇關(guān)于為何在Vue3中uni.createCanvasContext畫不出canvas的文章就介紹到這了,更多相關(guān)Vue3 uni.createCanvasContext畫不出canvas內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript關(guān)于某元素點擊事件的監(jiān)聽和觸發(fā)
javascript中加var和不加var的區(qū)別 你真的懂嗎

