You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

动态渲染CustomElement遇二次渲染props未定义问题求助

动态自定义元素工具的渲染时序问题解决

工具功能说明

  • registerComponents:遍历文件路径,搜索.ce.vue后缀的文件,异步加载并将组件的默认导出存入moduleDefaults Map。
  • 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未定义。

原因分析

  1. 时序倒置:当前逻辑是先把未定义的自定义元素挂载到DOM,之后才执行customElements.define。浏览器遇到未定义的自定义元素时会先渲染成HTMLUnknownElement,等元素定义后再升级,但这个过程中组件初始化可能已经完成,无法正确获取props。
  2. 异步加载未等待:registerComponents是异步加载组件模块,pkShowCustomModal执行时可能对应的组件模块还没存入moduleDefaults,导致元素定义延迟。
  3. 重复创建元素:每次调用都删除重建元素,不仅浪费性能,还会触发浏览器的即时渲染,加剧时序问题。

解决方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 15:32:24