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

标签 javascript dom-events vuejs3 web-component

WebComponents 可以很好地处理这个自定义脚本,但我遇到了问题。 我希望能够从外部添加事件监听器以监听从组件内部触发的自定义 Vue 事件(在此示例中单击 WC)。因此我创建了一个事件代理,但由于某种原因从未触发监听器。显然我在某个地方犯了错误。所以我希望有人能指出我正确的解决方案。

我做了一个CodeSandbox供您查看和使用代码。

(由于 CodeSandbox 有一些问题,您需要单击布局右侧的重新加载按钮才能使其正常工作)

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

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

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 我在其中测试 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>

自定义 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));
    }
  }
};

最佳答案

如果我将其添加到您的 render 方法中,一切正常。

因此您的问题与 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
                })
              );

评论后补遗:

  • 您的代码有 bubbles:false - 永远不会工作
  • 这段代码正确地发出了一个事件
  • 您需要研究如何触发您的代理代码
          const createCustomEvent = (name, args = []) => {
            return new CustomEvent(name, {
              bubbles: true,
              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) => {
                  appNode.dispatchEvent(createCustomEvent(name));
                };
              });
              document.addEventListener("foo",evt=>console.warn("foo!",evt));
              appNode.dispatchEvent(createCustomEvent("foo"));
              console.log(
                "@eventProxies",
                eventProxies,
                self,
                "appnode:",
                appNode,
                "rootNode",
                rootNode
              );
            }
            return eventProxies;
          };

关于javascript - Vue3 Web 组件事件不适用于自定义包装器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70580515/

相关文章:

typescript - VUE 3 传递 prop 到 watch composition API

javascript - 在不更改显示属性的情况下隐藏 CSS 元素

javascript - Angularjs Controller 显示错误

javascript - 在 Chrome 中通过 "new Date()"在 javascript 中创建日期对象不正确

javascript - 使用 JavaScript 获取没有 id 的相同 html 元素

javascript - Vue中双向绑定(bind)props的正确方式是什么?

javascript - jQuery 适用于 Chrome、Firefox、IE 但不适用于 iPhone

javascript - 使用jQuery或纯JS获取多选框的值

javascript - 如何捕获 form.submit 的响应

vue.js - 通过安装应用程序的元素上的属性将 Prop 传递给 Vue 根实例