react-dnd 踩坑-原生拖拽事件失效

近期工作中使用到了 react-dnd 开发拖拽功能,开发过程中发现页面中原来使用的一个第三方组件的拖拽功能不能正常工作了。

分析

将组件单独引入页面发现工作正常,排除了第三方组件版本的原因。后面发现是 react-dnd 引入后出现的问题,猜测是 react-dnd 引入影响了原有拖拽。

解决方案

在 github 上查找到相关的 issues,有不少人遇到了同样的问题,最终在 issues/3344 中找到了答案。

具体demo可以查看 codesandbox

解决方案主要是在react-dnd初始化时传入了rootElement

import React, { useState, useCallback, useMemo } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

export const useDndProvider = () => {
  const [dndArea, setDndArea] = useState();
  const handleRef = useCallback((node) => setDndArea(node), []);
  const html5Options = useMemo(
    () => ({ rootElement: dndArea }),
    [dndArea]
  );
  return { dndArea, handleRef, html5Options };
};

const DndProviderWrapper = (props) => {
  const { dndArea, handleRef, html5Options } = useDndProvider();
  return (
    <div ref={handleRef}>
      {dndArea && (
        <DndProvider backend={HTML5Backend} options={html5Options}>
          {props.children}
        </DndProvider>
      )}
    </div>
  );
};

export default DndProviderWrapper;

查看官方文档发现没有关于 rootElement 的描述,看看 react-dnd-html5-backend 源码是否能找出 rootElement 的作用。

import type { BackendFactory, DragDropManager } from 'dnd-core'

import { HTML5BackendImpl } from './HTML5BackendImpl.js'
import type { HTML5BackendContext, HTML5BackendOptions } from './types.js'
export { getEmptyImage } from './getEmptyImage.js'
export * as NativeTypes from './NativeTypes.js'
export type { HTML5BackendContext, HTML5BackendOptions } from './types.js'

export const HTML5Backend: BackendFactory = function createBackend(
    manager: DragDropManager,
    context?: HTML5BackendContext,
    options?: HTML5BackendOptions,
): HTML5BackendImpl {
    return new HTML5BackendImpl(manager, context, options)
}

我们看看 HTML5BackendOptions 的定义

export type HTML5BackendContext = Window | undefined

/**
 * Configuration options for the HTML5Backend
 */
export interface HTML5BackendOptions {
    /**
     * The root DOM node to use for subscribing to events. Default=Window
     */
    rootElement: Node
}

其实这里我们已经可以看到 rootElement 的作用了,是用于订阅事件的根 DOM 节点,默认是 Window 对象。再看看具体是怎么使用的。

export class HTML5BackendImpl implements Backend {
    public constructor(
        manager: DragDropManager,
        globalContext?: HTML5BackendContext,
        options?: HTML5BackendOptions,
    ) {
        this.options = new OptionsReader(globalContext, options)
        this.actions = manager.getActions()
        this.monitor = manager.getMonitor()
        this.registry = manager.getRegistry()
        this.enterLeaveCounter = new EnterLeaveCounter(this.isNodeInDocument)
    }
    //...
    public setup(): void {
        const root = this.rootElement as RootNode | undefined
        if (root === undefined) {
            return
        }

        if (root.__isReactDndBackendSetUp) {
            throw new Error('Cannot have two HTML5 backends at the same time.')
        }
        root.__isReactDndBackendSetUp = true
        this.addEventListeners(root)
    }
        
    private addEventListeners(target: Node) {
        // SSR Fix (https://github.com/react-dnd/react-dnd/pull/813
        if (!target.addEventListener) {
            return
        }
        target.addEventListener(
            'dragstart',
            this.handleTopDragStart as EventListener,
        )
        target.addEventListener('dragstart', this.handleTopDragStartCapture, true)
        target.addEventListener('dragend', this.handleTopDragEndCapture, true)
        target.addEventListener(
            'dragenter',
            this.handleTopDragEnter as EventListener,
        )
        target.addEventListener(
            'dragenter',
            this.handleTopDragEnterCapture as EventListener,
            true,
        )
        target.addEventListener(
            'dragleave',
            this.handleTopDragLeaveCapture as EventListener,
            true,
        )
        target.addEventListener('dragover', this.handleTopDragOver as EventListener)
        target.addEventListener(
            'dragover',
            this.handleTopDragOverCapture as EventListener,
            true,
        )
        target.addEventListener('drop', this.handleTopDrop as EventListener)
        target.addEventListener(
            'drop',
            this.handleTopDropCapture as EventListener,
            true,
        )
    }
}

总结

rootElement作为根节点元素监听事件,没有传值则默认是Window对象。这个属性的说明还是比较重要的,但官方文档没有说明,还是忍不住吐槽一下-。-

posted @ 2022-10-22 22:26  阿林十一  阅读(1390)  评论(0编辑  收藏  举报