iOS设备使用Konva触摸后图层消失的技术求助
问题修复方案:iOS下Konva Canvas触摸后区域消失
核心问题分析
- 重复初始化导致DOM混乱:
useLayoutEffect和监听窗口尺寸的useEffect重复调用initializeCanvas,每次调用都会创建新的Konva Stage,旧实例未销毁,iOS浏览器下触发渲染异常。 - 触摸事件冲突与默认行为干扰:iOS原生触摸手势(如滚动、缩放)干扰Konva事件处理,同时
touchstart与mouseover绑定引发重复触发opacity修改逻辑,导致元素异常消失。 - 资源未清理引发内存泄漏:重新初始化时未销毁旧的Stage和Layer,累积的DOM元素触发浏览器渲染异常。
具体修复步骤
1. 避免重复初始化,清理旧Konva实例
添加stageRef存储当前Stage实例,初始化前先销毁旧实例:
// 组件顶部新增引用 const stageRef = useRef(null); function initializeCanvas() { // 先销毁旧Stage实例 if (stageRef.current) { stageRef.current.destroy(); stageRef.current = null; } // ... 原有初始化逻辑 const stage = new Konva.Stage({ container: props.konvaContainer, width: width, height: height, name: 'stage' }); stageRef.current = stage; // 保存当前实例 // ... }
2. 合并初始化触发条件,添加卸载清理
移除冗余的useLayoutEffect,只保留必要的初始化触发逻辑,并在组件卸载时清理资源:
// 替换原有的useLayoutEffect和useEffect useEffect(() => { initializeCanvas(); // 组件卸载时清理Stage return () => { if (stageRef.current) { stageRef.current.destroy(); } }; }, [width, height, props.inactivePaths, props.selected]); // 添加依赖项避免无效触发
3. 使用Konva统一指针事件,替代mouse/touch分开绑定
Konva提供pointerover/pointerout/pointertap等跨平台统一事件,自动适配鼠标和触摸,避免事件冲突:
// 替换原有的stage.on事件绑定 stage.on('pointerover', function (evt) { if (evt && evt.target) { let shape = evt.target; if (shape && !shape.attrs.editable && shape.attrs.name === 'line') { shape.opacity(1); shapesLayer.draw(); } } }); stage.on('pointerout', function (evt) { if (evt && evt.target) { let shape = evt.target; if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return; if (shape && !shape.attrs.editable && shape.attrs.name === 'line') { shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0); shapesLayer.draw(); tooltip.hide(); tooltipLayer.draw(); } } }); stage.on('pointertap', function(evt) { let shape = evt.target; if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return; if (shape && shape.attrs.target && shape.attrs.name === 'line') props.onClick(shape.attrs.target); });
4. 阻止iOS触摸默认行为,避免手势干扰
在Konva容器上拦截触摸事件,阻止默认行为防止滚动等手势打断渲染:
// 在initializeCanvas中添加 stage.container().addEventListener('touchstart', (e) => { e.preventDefault(); }, { passive: false });
5. 修复尺寸计算逻辑,基于Konva容器初始化
确保Stage尺寸基于实际容器而非placeholder图片计算:
function initializeCanvas() { if (!canvasAreaRef.current || !canvasAreaRef.current.clientWidth || !canvasAreaRef.current.clientHeight) { setTimeout(() => initializeCanvas(), 500); return; } // 用Konva容器的尺寸初始化Stage const stageWidth = canvasAreaRef.current.clientWidth; const stageHeight = canvasAreaRef.current.clientHeight; // ... 后续初始化逻辑 }
核心修改后代码片段
import { useEffect, useState, useRef } from 'react' import Image from 'next/image' import Konva from 'konva'; import i18nService from '../../../helpers/i18nService' import useWindowSize from '../../../hooks/useWindowSize' export default function InteractiveCanvas(props) { const [showCanvas, setShowCanvas] = useState(false); const [placeholderWidth, setPlaceholderWidth] = useState(1); const [placeholderHeight, setPlaceholderHeight] = useState(1); const [width, height] = useWindowSize(); const canvasAreaRef = useRef(null) const canvasPlaceholderRef = useRef() const stageRef = useRef(null); // 新增Stage引用 useEffect(() => { if (showCanvas) { props.loaded() } },[showCanvas]) useEffect(() => { initializeCanvas(); // 组件卸载清理 return () => { if (stageRef.current) { stageRef.current.destroy(); } }; }, [width, height, props.inactivePaths, props.selected]); function initializeCanvas() { // 清理旧实例 if (stageRef.current) { stageRef.current.destroy(); stageRef.current = null; } if (!canvasAreaRef.current || !canvasAreaRef.current.clientWidth || !canvasAreaRef.current.clientHeight) { setTimeout(() => initializeCanvas(), 500); return; } setPlaceholderWidth(canvasAreaRef.current.clientWidth) setPlaceholderHeight(canvasAreaRef.current.clientHeight) const stageWidth = canvasAreaRef.current.clientWidth; const stageHeight = canvasAreaRef.current.clientHeight; const stage = new Konva.Stage({ container: props.konvaContainer, width: stageWidth, height: stageHeight, name: 'stage' }); stageRef.current = stage; // 阻止iOS触摸默认行为 stage.container().addEventListener('touchstart', (e) => { e.preventDefault(); }, { passive: false }); const shapesLayer = new Konva.Layer({ name: 'shapes' }); const tooltipLayer = new Konva.Layer({ name: 'tooltips' }); // 原tooltip创建逻辑不变 let tooltip = new Konva.Label({ opacity: 1, visible: false, listening: false, name: 'label' }); tooltip.add(new Konva.Tag({/* 原有属性不变 */})); tooltip.add(new Konva.Text({/* 原有属性不变 */})); tooltipLayer.add(tooltip); let areas = getData(); // 原shape绘制逻辑不变 for (let key in areas) { let area = areas[key]; let shape = new Konva.Line({/* 原有属性不变 */}); let group = new Konva.Group({name: 'group'}); shapesLayer.add(group); group.add(shape); } stage.add(shapesLayer); stage.add(tooltipLayer); // 统一指针事件绑定 stage.on('pointerover', function (evt) { if (evt && evt.target) { let shape = evt.target; if (shape && !shape.attrs.editable && shape.attrs.name === 'line') { shape.opacity(1); shapesLayer.draw(); } } }); stage.on('pointerout', function (evt) { if (evt && evt.target) { let shape = evt.target; if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return; if (shape && !shape.attrs.editable && shape.attrs.name === 'line') { shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0); shapesLayer.draw(); tooltip.hide(); tooltipLayer.draw(); } } }); stage.on('mousemove', function(evt) { // 原tooltip移动逻辑不变 let shape = evt.target; if (shape) { let mousePos = stage.getPointerPosition(); let x = mousePos.x; let y = mousePos.y - 5; updateTooltip(tooltip, x, y, shape.attrs.key, shape.attrs.subline); tooltipLayer.draw(); } }); stage.on('pointertap', function(evt) { let shape = evt.target; if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return; if (shape && shape.attrs.target && shape.attrs.name === 'line') props.onClick(shape.attrs.target); }) setTimeout(() => { setShowCanvas(true) shapesLayer.draw(); tooltipLayer.draw(); }, 100) } // 原getData和updateTooltip逻辑不变 function getData() { /* ... */ } function updateTooltip(tooltip, x, y, text, subline) { /* ... */ } return ( // 原JSX结构不变 <div className={props.styles.canvasContainer}> <img className={props.styles.canvasImgPlaceholder} ref={canvasPlaceholderRef} alt='Canvas Placeholder' src={props.background} /> <div className={props.styles.canvasPlaceholderBox} data-hide={showCanvas}> <Image src="/images/planpoint_icon.svg" width={200} height={200} alt="" /> </div> <div className={props.styles.canvasKonvaContainer} id={props.konvaContainer} ref={canvasAreaRef} data-hide={!showCanvas} style={{backgroundImage: `url('${props.background}')`}} > </div> </div> ) }
内容的提问来源于stack exchange,提问作者Leousz
相关产品推荐
相关产品推荐

