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/