动态渲染CustomElement遇二次渲染props未定义问题求助
动态自定义元素工具的渲染时序问题解决
工具功能说明
- registerComponents:遍历文件路径,搜索
.ce.vue后缀的文件,异步加载并将组件的默认导出存入moduleDefaultsMap。 - pkShowCustomModal:根据传入的标签名创建自定义DOM元素,若元素已存在则删除重建,同时定义对应的CustomElement,用于显示模态框类组件。
原始代码
let moduleDefaults = new Map(); let customElementsMap = new Map(); const registerComponents = () => { // ^ Check for .ce files -> then register components for (const [key, value] of Object.entries(filePaths)) { if (value.name.toLowerCase().includes(".ce.vue")) { let name = value.name.split(/[\\\/]/).pop().toLowerCase(); value().then(module => { moduleDefaults.set(name.substr(0, name.indexOf('.')), module.default); }) } } } registerComponents(); PK_CE.pkShowCustomModal = async (tagName = "", url = "", params = {}) => { let props = {}; try { const res = await axios.get(url, { params: params, headers: { 'Accept': 'application/json', "Content-Type": 'application/json' } }); props = cloneDeep(res.data); } catch (error) { console.log(error); } var existing = document.getElementsByTagName("custom-" + tagName), tag = document.createElement("custom-" + tagName); // ^ IF - If element is not mounted yet -> append element to DOM and set attribute // ^ ELSE - To Mount element after closing delete html tag and create new element with new data if (!existing.length) { document.body.appendChild(tag); existing[0].setAttribute("data", JSON.stringify(props)); } else if (existing && existing[0].hasAttribute("data")) { document.body.removeChild(existing[0]); document.body.appendChild(tag); existing[0].setAttribute("data", JSON.stringify(props)); } // TODO Element shows before custom element is defined after getting re-created!!!! WHY THOO?! if (moduleDefaults.size > 0) { try { if (!customElementsMap.has(tagName)) { customElementsMap.set(tagName, defineCustomElement(moduleDefaults.get(tagName))); } if (!customElements.get("custom-" + tagName)) window.customElements.define("custom-" + tagName, customElementsMap.get(tagName)); } catch (error) { console.log("ERROR:", error); } } return; }
问题描述
首次挂载自定义元素时一切正常,但移除并重新创建元素时,组件会在window.customElements.define执行前尝试渲染,导致props未定义。
原因分析
- 时序倒置:当前逻辑是先把未定义的自定义元素挂载到DOM,之后才执行
customElements.define。浏览器遇到未定义的自定义元素时会先渲染成HTMLUnknownElement,等元素定义后再升级,但这个过程中组件初始化可能已经完成,无法正确获取props。 - 异步加载未等待:
registerComponents是异步加载组件模块,pkShowCustomModal执行时可能对应的组件模块还没存入moduleDefaults,导致元素定义延迟。 - 重复创建元素:每次调用都删除重建元素,不仅浪费性能,还会触发浏览器的即时渲染,加剧时序问题。
解决方案
1. 核心调整思路
- 确保自定义元素先定义完成,再挂载到DOM
- 等待目标组件模块加载完成后再执行后续逻辑
- 复用现有元素更新props,避免重复创建和定义元素
修改后的代码
let moduleDefaults = new Map(); let customElementsMap = new Map(); // 存储组件加载的Promise,用于等待加载完成 const componentLoadPromises = new Map(); const registerComponents = () => { for (const [key, value] of Object.entries(filePaths)) { if (value.name.toLowerCase().includes(".ce.vue")) { let name = value.name.split(/[\\\/]/).pop().toLowerCase(); const tagKey = name.substr(0, name.indexOf('.')); // 存储加载Promise,避免重复加载 if (!componentLoadPromises.has(tagKey)) { const loadPromise = value().then(module => { moduleDefaults.set(tagKey, module.default); }); componentLoadPromises.set(tagKey, loadPromise); } } } } registerComponents(); PK_CE.pkShowCustomModal = async (tagName = "", url = "", params = {}) => { let props = {}; try { const res = await axios.get(url, { params: params, headers: { 'Accept': 'application/json', "Content-Type": 'application/json' } }); props = cloneDeep(res.data); } catch (error) { console.log(error); } const customTagName = `custom-${tagName}`; let existing = document.querySelector(customTagName); // 等待目标组件模块加载完成 if (componentLoadPromises.has(tagName)) { await componentLoadPromises.get(tagName); } // 确保自定义元素已定义 if (!customElements.get(customTagName)) { if (!customElementsMap.has(tagName)) { const component = moduleDefaults.get(tagName); if (!component) { console.error(`组件${tagName}未找到`); return; } customElementsMap.set(tagName, defineCustomElement(component)); } try { window.customElements.define(customTagName, customElementsMap.get(tagName)); } catch (error) { // 忽略重复定义的错误(如果已经定义过) if (error.name !== 'NotSupportedError') { console.log("ERROR:", error); } } } // 复用或创建元素 if (!existing) { existing = document.createElement(customTagName); document.body.appendChild(existing); } // 更新props existing.setAttribute("data", JSON.stringify(props)); return; }
关键修改点
- 新增
componentLoadPromises存储组件加载的Promise,确保pkShowCustomModal等待目标组件加载完成后再继续。 - 调整执行顺序:先完成自定义元素定义,再创建/挂载元素,彻底避免未定义元素提前渲染的问题。
- 复用现有元素,直接更新
data属性,无需删除重建,同时避免重复定义元素的错误。 - 捕获重复定义元素的错误,防止控制台无效报错。
内容的提问来源于stack exchange,提问作者temp
相关产品推荐
相关产品推荐

