You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS设备使用Konva触摸后图层消失的技术求助

问题修复方案:iOS下Konva Canvas触摸后区域消失

核心问题分析

  1. 重复初始化导致DOM混乱:useLayoutEffect和监听窗口尺寸的useEffect重复调用initializeCanvas,每次调用都会创建新的Konva Stage,旧实例未销毁,iOS浏览器下触发渲染异常。
  2. 触摸事件冲突与默认行为干扰:iOS原生触摸手势(如滚动、缩放)干扰Konva事件处理,同时touchstart与mouseover绑定引发重复触发opacity修改逻辑,导致元素异常消失。
  3. 资源未清理引发内存泄漏:重新初始化时未销毁旧的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 10:26:04