简体   繁体   English

Vue3 Web 组件事件不适用于自定义包装器

[英]Vue3 Web Components events not working with custom wrapper

The WebComponents do work fine with this custom script but I have problems to. WebComponents 可以很好地使用这个自定义脚本,但我有问题。 I want to be able to add event listener from outside to listen to the custom Vue events fired from within the component (in this example on click on the WC).我希望能够从外部添加事件侦听器以侦听从组件内部触发的自定义 Vue 事件(在此示例中单击 WC)。 Thus I created an event proxy but for some reason the listener is never triggered.因此,我创建了一个事件代理,但由于某种原因,监听器从未被触发。 Obviously I made a mistake somewhere.显然我在某个地方犯了错误。 So I hope that someone can point me to the right solution.所以我希望有人能指出我正确的解决方案。

I made a CodeSandbox for you to take a look and play around with the code.我制作了一个CodeSandbox供您查看并使用代码。

(as CodeSandbox has some issues you'll need to click on the reload button on the right side of the layout in order to make it work) (因为 CodeSandbox 有一些问题,您需要单击布局右侧的重新加载按钮才能使其工作)

//main.js
import { createApp } from "vue";
import App from "./App.vue";
import WebComponentService from "./services/wc";
WebComponentService.init();

createApp(App).mount("#app");

Vue Component used as source file for Web Component Vue 组件用作 Web 组件的源文件

//src/wcs/MyComp.vue
<template>
  <div class="m" @click="click">
    Hello My Comp {{ name }}
    <div v-for="(i, index) in values" :key="index">ID: {{ i.title }}</div>
  </div>
</template>

<script>
export default {
  __useShadowDom: false,
  emits: ["my-click"],
  // emits: ["my-click", "myclick"],
  props: {
    name: {
      default: "Test",
    },
    values: {
      type: Object,
      default: () => {
        return [{ title: "A" }, { title: "B" }];
      },
    },
  },
  methods: {
    click() {
      // emit the event
      this.$emit("my-click");
      console.log("EMIT");
    },
  },
};
</script>

<style lang="less" scoped>
.m {
  border: 5px solid red;
  margin: 10px;
  padding: 10px;
  border-radius: 5px;
}
</style>

Index.html where I test the web component and added an event listener Index.html 我在其中测试 web 组件并添加了一个事件侦听器

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <h3>WC Test here</h3>

    <div class="component-canvas">
      <!-- web component testing -->
      <xvue-my-comp
        id="vtest"
        name="MyComp HTML Rendering"
        :values='[{"title": "Element 1"}, {"title": "Element"}]'
        >Inner HTML Caption</xvue-my-comp
      >
    </div>

    <script>
      const mySelector = document.querySelector("xvue-my-comp");
      //listen to the event
      mySelector.addEventListener("my-click", function (ev) {
        console.log("@LISTEN");
        alert(ev);
      });
    </script>
  </body>
</html>

Custom WebComponents wrapper自定义 WebComponents 包装器

import HTMLParsedElement from "html-parsed-element";
import { createApp, h, toHandlerKey } from "vue";
import { snakeCase, camelCase } from "lodash";

let registeredComponents = {};

const tagPrefix = "xvue-"; // Prefix for Custom Elements (HTML5)
const vueTagPrefix = "xvue-init-"; // Prefix for VUE Components - Must not be tagPrefix to avoid loops

// We use only one file as web component for simplicity
// and because codesandbox doesen't work with dynamic requires
const fileName = "MyComp.vue";
const webComponent = require(`../wcs/MyComp.vue`);

const componentName = snakeCase(
  camelCase(
    // Gets the file name regardless of folder depth
    fileName
      .split("/")
      .pop()
      .replace(/.ce/, "")
      .replace(/\.\w+$/, "")
  )
).replace(/_/, "-");
// store our component
registeredComponents[componentName] = {
  component: webComponent.default
};

export default {
  init() {
    // HTMLParsedElement is a Polyfil Library (NPM Package) to give a clean parsedCallback as the browser
    // default implementation has no defined callback when all props and innerHTML is available
    class VueCustomElement extends HTMLParsedElement {
      // eslint-disable-next-line
      constructor() {
        super();
      }

      parsedCallback() {
        console.log("Legacy Component Init", this);

        if (!this.getAttribute("_innerHTML")) {
          this.setAttribute("data-initialized", this.innerHTML);
        }

        let rootNode = this;
        let vueTagName = this.tagName
          .toLowerCase()
          .replace(tagPrefix, vueTagPrefix);
        let compBaseName = this.tagName.toLowerCase().replace(tagPrefix, "");
        let compConfig = registeredComponents[compBaseName];

        // Optional: Shadow DOM Mode. Can be used by settings __useShadowDom in component vue file
        if (compConfig.component.__useShadowDom) {
          rootNode = this.attachShadow({ mode: "open" });
          document
            .querySelectorAll('head link[rel=stylesheet][href*="core."]')
            .forEach((el) => {
              rootNode.appendChild(el.cloneNode(true));
            });
        }

        if (vueTagName) {
          // If we have no type we do nothing
          let appNode = document.createElement("div");
          // let appNode = rootNode;
          appNode.innerHTML +=
            "<" +
            vueTagName +
            ">" +
            this.getAttribute("data-initialized") +
            "</" +
            vueTagName +
            ">"; // @TODO: Some issues with multiple objects being created via innerHTML and slots
          rootNode.appendChild(appNode);

          // eslint-disable-next-line @typescript-eslint/no-this-alias
          const self = this;

          function createCustomEvent(name, args) {
            return new CustomEvent(name, {
              bubbles: false,
              cancelable: false,
              detail: args.length === 1 ? args[0] : args
            });
          }

          const createEventProxies = (eventNames) => {
            const eventProxies = {};
            if (eventNames) {
              console.log("eventNames", eventNames);
              // const handlerName = toHandlerKey(camelCase(name));
              eventNames.forEach((name) => {
                const handlerName = name;
                eventProxies[handlerName] = (...args) => {
                  this.dispatchEvent(createCustomEvent(name, args));
                };
              });
            }
            return eventProxies;
          };
          const eventProxies = createEventProxies(compConfig.component.emits);
          this._props = {};

          const app = createApp({
            render() {
              let props = Object.assign({}, self._props, eventProxies);
              // save our attributes as props
              [...self.attributes].forEach((attr) => {
                let newAttr = {};
                newAttr[attr.nodeName] = attr.nodeValue;
                props = Object.assign({}, props, newAttr);
              });
              console.log("props", props);
              delete props.dataVApp;
              return h(compConfig.component, props);
            },

            beforeCreate: function () {}
          });

          // Append only relevant VUE components for this tag
          app.component(vueTagPrefix + compBaseName, compConfig.component);

          this.vueObject = app.mount(appNode);
          console.log("appNode", app.config);
        }
      }

      disconnectedCallback() {
        if (this.vueObject) {
          this.vueObject.$destroy(); // Remove VUE Object
        }
      }

      adoptedCallback() {
        //console.log('Custom square element moved to new page.');
      }
    }

    // Register for all available component tags ---------------------------

    // Helper to Copy Classes as customElement.define requires separate Constructors
    function cloneClass(parent) {
      return class extends parent {};
    }

    for (const [name, component] of Object.entries(registeredComponents)) {
      customElements.define(tagPrefix + name, cloneClass(VueCustomElement));
    }
  }
};

I guess you could try to use Vue3's (since 3.2) defineCustomElement我想你可以尝试使用 Vue3(自 3.2 起) defineCustomElement

import CustomElement from './some/path/CustomElement.ce.vue'

const CustomElementCE = defineCustomElement(CustomElement)
customElements.define('custom-element', CustomElementCE);

More on that here: Vue web components更多信息: Vue web 组件

If I whack this into your render method, it all works fine.如果我将它添加到您的render方法中,一切正常。

Thus your problem is not Web Components or Events related因此,您的问题与 Web 组件或事件无关

              document.addEventListener("foo", (evt) => {
                console.log("FOOd", evt.detail, evt.composedPath());
              });
              self.dispatchEvent(
                new CustomEvent("foo", {
                  bubbles: true,
                  composed: true,
                  cancelable: false,
                  detail: self
                })
              );

I found one possible solution.我找到了一种可能的解决方案。 As stated in the vue docs we can use static event listeners that are passed as props.如 vue 文档中所述,我们可以使用作为道具传递的 static 事件侦听器。 They have to be prefixed with "on" iE onMyevent for event name my-event .对于事件名称my-event ,它们必须以“on” iE onMyevent为前缀。 Though it works fine - the downside is that I can't pass any arguments as I don't see where to get them from.虽然它工作正常 - 缺点是我无法通过任何 arguments 因为我不知道从哪里得到它们。

One strange thing is that I cant add this event to the instance.一件奇怪的事情是我无法将此事件添加到实例中。 Something like self.dispatchEvent() doesn't work when placed inside createEventProxies method.self.dispatchEvent()这样的东西放在createEventProxies方法中时不起作用。 So I have to stick to document.dispatchEvent()所以我必须坚持document.dispatchEvent()

Working CodeSandbox .工作代码沙盒

  const createCustomEvent = (name, args = []) => {
    return new CustomEvent(name, {
      bubbles: false,
      composed: true,
      cancelable: false,
      detail: !args.length ? self : args.length === 1 ? args[0] : args
    });
  };

  const createEventProxies = (eventNames) => {
    const eventProxies = {};
    if (eventNames) {
      eventNames.forEach((name) => {
        const handlerName =
          "on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
        eventProxies[handlerName] = (...args) => {
          document.dispatchEvent(createCustomEvent(name));
        };
      });
      console.log("@eventProxies", eventProxies);
    }
    return eventProxies;
  };

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM