当 Shadow DOM 中的 React 组件时,单击事件不会触发

IT技术 javascript reactjs web-component shadow-dom custom-element
2021-04-01 18:10:50

我有一个特殊情况,我需要用一个 Web 组件封装一个 React 组件。设置似乎非常简单。这是react代码:

// React Component
class Box extends React.Component {
  handleClick() {
    alert("Click Works");
  }
  render() {
    return (
      <div 
        style={{background:'red', margin: 10, width: 200, cursor: 'pointer'}} 
        onClick={e => this.handleClick(e)}>

        {this.props.label} <br /> CLICK ME

      </div>
    );
  }
};

// Render React directly
ReactDOM.render(
  <Box label="React Direct" />,
  document.getElementById('mountReact')
);

HTML:

<div id="mountReact"></div>

这很好地安装并且点击事件有效。现在,当我围绕 React 组件创建 Web 组件包装器时,它可以正确呈现,但单击事件不起作用。这是我的 Web 组件包装器:

// Web Component Wrapper
class BoxWebComponentWrapper extends HTMLElement {
  createdCallback() {
    this.el      = this.createShadowRoot();
    this.mountEl = document.createElement('div');
    this.el.appendChild(this.mountEl);

    document.onreadystatechange = () => {
      if (document.readyState === "complete") {
        ReactDOM.render(
          <Box label="Web Comp" />,
          this.mountEl
        );
      }
    };
  }
}

// Register Web Component
document.registerElement('box-webcomp', {
  prototype: BoxWebComponentWrapper.prototype
});

这是 HTML:

<box-webcomp></box-webcomp>

有什么我想念的吗?还是 React 拒绝在 Web 组件中工作?我见过像 Maple.JS 这样的库可以做这种事情,但他们的库有效。我觉得我错过了一件小事。

这是 CodePen,因此您可以看到问题:

http://codepen.io/homeslicesolutions/pen/jrrpLP

5个回答

事实证明,Shadow DOM 重新定位了点击事件并将这些事件封装在阴影中。React 不喜欢这样,因为它们本身不支持 Shadow DOM,所以事件委托是关闭的,并且不会触发事件。

我决定做的是将事件重新绑定到技术上“在光照中”的实际阴影容器。我使用event.path上下文中的所有 React 事件处理程序跟踪事件的冒泡,并将其触发到影子容器。

我添加了一个“retargetEvents”方法,它将所有可能的事件类型绑定到容器。然后它将通过查找“__reactInternalInstances”并在事件范围/路径中寻找相应的事件处理程序来调度正确的 React 事件。

retargetEvents() {
    let events = ["onClick", "onContextMenu", "onDoubleClick", "onDrag", "onDragEnd", 
      "onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop", 
      "onMouseDown", "onMouseEnter", "onMouseLeave","onMouseMove", "onMouseOut", 
      "onMouseOver", "onMouseUp"];

    function dispatchEvent(event, eventType, itemProps) {
      if (itemProps[eventType]) {
        itemProps[eventType](event);
      } else if (itemProps.children && itemProps.children.forEach) {
        itemProps.children.forEach(child => {
          child.props && dispatchEvent(event, eventType, child.props);
        })
      }
    }

    // Compatible with v0.14 & 15
    function findReactInternal(item) {
      let instance;
      for (let key in item) {
        if (item.hasOwnProperty(key) && ~key.indexOf('_reactInternal')) {
          instance = item[key];
          break;
        } 
      }
      return instance;
    }

    events.forEach(eventType => {
      let transformedEventType = eventType.replace(/^on/, '').toLowerCase();

      this.el.addEventListener(transformedEventType, event => {
        for (let i in event.path) {
          let item = event.path[i];

          let internalComponent = findReactInternal(item);
          if (internalComponent
              && internalComponent._currentElement 
              && internalComponent._currentElement.props
          ) {
            dispatchEvent(event, eventType, internalComponent._currentElement.props);
          }

          if (item == this.el) break;
        }

      });
    });
  }

当我将 React 组件渲染到 shadow DOM 中时,我会执行“retargetEvents”

createdCallback() {
    this.el      = this.createShadowRoot();
    this.mountEl = document.createElement('div');
    this.el.appendChild(this.mountEl);

    document.onreadystatechange = () => {
      if (document.readyState === "complete") {

        ReactDOM.render(
          <Box label="Web Comp" />,
          this.mountEl
        );

        this.retargetEvents();
      }
    };
  }

我希望这适用于 React 的未来版本。这是它工作的codePen:

http://codepen.io/homeslicesolutions/pen/ZOpbWb

感谢@mrlew 的链接,它给了我解决这个问题的线索,也感谢@Wildhoney 与我思考相同的波长=)。

哦,伙计,想给你 10.000 个赞。你救了我的命。
2021-05-23 18:10:50
@Lukas 很久以前了,我不记得了。(这是我使用的脚本gist.github.com/DimitryDushkin/c091d5a6c33e10641eef0828261d5398但似乎有 awwester 所说的错误。
2021-05-28 18:10:50
@Dimitry 和 josephnvu 我修复了一个错误,清理了代码并在npmjs.com/package/react-shadow-dom-retarget-events 上发布了这个解决方法
2021-06-05 18:10:50
很好的解决方案。想知道为什么 React 团队不想整合它。我看到了两个关于这个的公关。(
2021-06-09 18:10:50
你会“开源”吗?我发现了一些错误(大量的 dispatchEvent 调用),修复它并分享会很好。
2021-06-16 18:10:50

我修复了一个错误,清理了@josephvnu 接受的答案的代码我在此处将其作为 npm 包发布:https ://www.npmjs.com/package/react-shadow-dom-retarget-events

用法如下

安装

yarn add react-shadow-dom-retarget-events 或者

npm install react-shadow-dom-retarget-events --save

利用

导入retargetEvents并调用它shadowDom

import retargetEvents from 'react-shadow-dom-retarget-events';

class App extends React.Component {
  render() {
    return (
        <div onClick={() => alert('I have been clicked')}>Click me</div>
    );
  }
}

const proto = Object.create(HTMLElement.prototype, {
  attachedCallback: {
    value: function() {
      const mountPoint = document.createElement('span');
      const shadowRoot = this.createShadowRoot();
      shadowRoot.appendChild(mountPoint);
      ReactDOM.render(<App/>, mountPoint);
      retargetEvents(shadowRoot);
    }
  }
});
document.registerElement('my-custom-element', {prototype: proto});

作为参考,这是修复的完整源代码https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js

这个答案是五年后的更新。

坏消息:@josephnvu 的回答(在撰写本文时已接受)并且该react-shadow-dom-retarget-events包不再正常工作,至少在 React 16.13.1 中 - 尚未使用早期版本进行测试。看起来 React 内部发生了一些变化,导致代码调用错误的侦听器回调。

好消息:

  • 在 React 16.13.1 中(同样,没有用早期的 16.x 测试),可以直接渲染到阴影根中,没有中间块。在这种情况下,侦听器将附加到影子根而不是文档,因此 React 能够正确捕获和分派所有事件。显而易见的权衡是你不能向同一个影子根添加任何其他东西,因为 React 会用渲染的 JSX 覆盖你的元素。
  • 在 React 17 中,React 将其侦听器附加到渲染根,而不是文档或阴影根,因此无论我们渲染到哪里,一切都是开箱即用的。

替换this.el = this.createShadowRoot();this.el = document.getElementById("mountReact");刚刚工作。也许是因为 react 有一个全局事件处理程序,而 shadow dom 意味着事件重定向。

谢谢@mrlew。这部分有帮助:“由于 Shadow DOM 具有用于封装目的的事件重定向的概念,因此事件委托将无法正常运行,因为所有事件似乎都来自 Shadow DOM——因此 ReactShadow 使用每个元素的 React ID 来调度事件来自原始元素,因此维护 React 的事件委托实现。”
2021-05-22 18:10:50
但我想使用 shadow dom。否则它不会被封装。这里的挑战是将 React 组件包装在自定义元素中。
2021-05-25 18:10:50
想了个办法。往上看。
2021-06-17 18:10:50
我找到了这个也许值得一试。
2021-06-19 18:10:50

我偶然发现了另一个解决方案。使用preact-compat代替react在 ShadowDOM 中似乎工作正常;Preact 必须以不同的方式绑定到事件?