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

Chrome扩展数据位置变更后列表无法正确同步数据求助

数据重排序/位置变化时列表值显示异常问题排查与解决

当数据重新排序或标签页位置改变时,列表中的值无法正确显示。试过用索引作为标识符,后来改用find()以ID匹配,但问题仍未解决。本人是JavaScript对象概念新手,找不到数据一致性相关学习资料,附上popup.js和background.js代码求助。


let data_placeholder = {};
initialize_data();

// Update the storage after the popup is closed
document.addEventListener("visibilitychange", () => {
    chrome.runtime.sendMessage({ message: "set_data", data: data_placeholder });
});

// Update data_placeholder everytime the checkbox is toggled
function update_data(checkbox, type, index, method) {
    if (checkbox.nodeName == "INPUT" && checkbox.type == "checkbox") {
        checkbox.addEventListener("change", () => {
            data_placeholder[type][index][method] = checkbox.checked;
        });
    } else {
        console.warn("This shouldn't be happening: Error(Incompatible type)");
    }
}

// Execute script
document.getElementById("execute-script").addEventListener("click", (e) => {
    chrome.runtime.sendMessage({
        message: "execute_methods",
        data: data_placeholder,
    });
});

// some code

function initialize_data() {
    chrome.runtime.sendMessage({ message: "get_data" }, (data) => {
        // Update data_placeholder with the latest data
        data_placeholder = structuredClone(data);
        console.log("Popup data: ", data_placeholder);
        if (data.extensions.length > 0) {
            let frag_ext = document.createDocumentFragment();
            data.extensions.sort((a, b) => a.ext_name.localeCompare(b.ext_name));
            data.extensions.forEach((extension, i) => {
                // Don't show self
                if (extension.ext_name == "Chroxt") {
                    return;
                }
                // Setting up the elements
                let container = document.createElement("div");
                let name = document.createElement("p");
                let reload = document.createElement("input");
                let uninstall = document.createElement("input");
                let disable = document.createElement("input");
                name.title = `Name: ${extension.ext_name}\nID: ${extension.ext_id}\nVersion: ${extension.version}\nMatches: \n\t${extension.matches.join(",\n\t")}\nPermissions: \n\t${extension.permissions.join(",\n\t")}`;
                name.textContent = extension.ext_name;

                frag_ext.appendChild(batch_append(container, [name, reload, uninstall, disable]));
                batch_set_attr([reload, uninstall, disable], "type", "checkbox");
                container.setAttribute("class", "ext-list-body");

                // Modifying the properties
                reload.checked = extension.reload;
                disable.checked = extension.disable;
                uninstall.checked = extension.uninstall;

                // Adding listeners for checkboxes
                update_data(reload, "extensions", i, "reload");
                update_data(uninstall, "extensions", i, "uninstall");
                update_data(disable, "extensions", i, "disable");

                // Stylization
                if (extension.enabled) {
                    name.style.opacity = "100%";
                } else {
                    name.style.opacity = "30%";
                }
            });
            document.getElementById("list-ext").appendChild(frag_ext);
        } else {
            // Add such empty
        }

        if (data.websites.length > 0) {
            let frag_site = document.createDocumentFragment();
            data.websites.forEach((website, i) => {
                // Setting up the elements
                let container = document.createElement("div");
                let name = document.createElement("p");
                let reload = document.createElement("input");
                name.title = `Title: ${website.site_name}\nID: ${website.site_id}\nURL: ${website.url}`;
                name.textContent = website.site_name;

                frag_site.appendChild(batch_append(container, [name, reload]));
                reload.type = "checkbox";
                container.setAttribute("class", "site-list-body");

                // Modifying the properties
                reload.checked = website.reload;

                // Adding listeners for checkbox
                update_data(reload, "websites", i, "reload");
            });
            document.getElementById("list-site").appendChild(frag_site);
        } else {
            // Add such empty
        }
    });
}

background.js

let storage = chrome.storage.local;

// Purpose: Get new extensions & tabs data, default if new, otherwise copy methods data from original
// Return: Return an object, not merged with old data, but with old data essential properties
// Nutshell: Returns new & updated data
function get_new_data(callback = null) {
    let data_placeholder = {
        extensions: [],
        websites: [],
        enabled: false,
        "show-popup": true,
    };
    return new Promise((resolve, reject) => {
        // Get the current strorage
        storage.get(null, (data) => {
            // Get new extensions and new extension properties
            chrome.management.getAll((extensions) => {
                extensions.sort((a, b) => a.name.localeCompare(b.name));
                extensions.forEach((ext, i) => {
                    // Since PWA is included, only allow extensions
                    if (ext.type == "extension") {
                        // Create an object inside an array of this object for every extension
                        data_placeholder.extensions[i] = {
                            // Brand new data
                            ext_name: ext.name,
                            ext_id: ext.id,
                            version: ext.version,
                            enabled: ext.enabled,
                            matches: ext.hostPermissions,
                            permissions: ext.permissions,
                            icon: ext.icons?.[0].url,

                            // Keep settings the same, if not initialized, set to default
                            reload: data.extensions?.[i]?.reload ?? false,
                            disable: data.extensions?.[i]?.disable ?? false,
                            uninstall: data.extensions?.[i]?.uninstall ?? false,
                        };
                    }
                });
                // After extension operation, get all open tabs
                chrome.tabs.query({}, (tabs) => {
                    tabs.forEach((tab, k) => {
                        // For each tab, add this object
                        data_placeholder.websites[k] = {
                            site_name: tab.title,
                            site_id: tab.id,
                            icon: tab.favIconUrl,
                            url: tab.url,

                            // Keep settings the same, if not initialized, set to default
                            reload: data.websites?.[k]?.reload ?? false,
                        };
                    });
                    // Finally, return the merged data through Promises or callback
                    if (typeof callback == "function") {
                        callback(data_placeholder);
                    } else {
                        resolve(data_placeholder);
                    }
                });
            });
        });
    });
}

chrome.runtime.onMessage.addListener((receive, _, send) => {
    switch (receive.message) {
        case "get_data":
            get_new_data().then((data) => {
                send(data);
                console.log("Background: ", data);
            });
            break;
        case "set_data":
            storage.set(receive.data);
            break;
        case "execute_methods":
            receive.data.extensions.forEach((extension) => {
                if (extension.reload) {
                    chrome.management.setEnabled(extension.ext_id, false);
                    chrome.management.setEnabled(extension.ext_id, true);
                }
                if (extension.disable) {
                    chrome.management.setEnabled(extension.ext_id, !extension.enabled);
                }
                if (extension.uninstall) {
                    chrome.management.uninstall(extension.ext_id);
                }
            });
            receive.data.websites.forEach((website) => {
                if (website.reload) {
                    chrome.tabs.reload(website.site_id);
                }
            });
            break;
        default:
            console.warn("UNHANDLED MESSAGE: ONMESSAGE");
            break;
    }
    return true;
});

问题根源与解决方案

核心问题

  1. get_new_data依赖索引匹配旧数据:排序或元素位置变化后,数组索引对应的元素已不是原目标,导致状态(reload/disable/uninstall)被错误赋值给其他元素。
  2. popup事件监听绑定索引而非唯一ID:update_data通过索引修改data_placeholder,但列表排序后DOM元素索引与数据索引不一致,导致勾选状态错误关联。

修复步骤

1. 修改get_new_data,用唯一ID匹配旧数据

将扩展和标签页的状态通过ext_id/site_id从旧数据中读取,替代不稳定的索引:

function get_new_data(callback = null) {
    let data_placeholder = {
        extensions: [],
        websites: [],
        enabled: false,
        "show-popup": true,
    };
    return new Promise((resolve, reject) => {
        storage.get(null, (storedData) => {
            // 把旧数据转成ID为键的对象,快速查找
            const oldExtensionsMap = storedData.extensions?.reduce((map, ext) => {
                map[ext.ext_id] = ext;
                return map;
            }, {}) || {};
            const oldWebsitesMap = storedData.websites?.reduce((map, site) => {
                map[site.site_id] = site;
                return map;
            }, {}) || {};

            chrome.management.getAll((extensions) => {
                const sortedExts = extensions.filter(ext => ext.type === "extension").sort((a, b) => a.name.localeCompare(b.name));
                sortedExts.forEach((ext) => {
                    const oldExt = oldExtensionsMap[ext.id];
                    data_placeholder.extensions.push({
                        ext_name: ext.name,
                        ext_id: ext.id,
                        version: ext.version,
                        enabled: ext.enabled,
                        matches: ext.hostPermissions,
                        permissions: ext.permissions,
                        icon: ext.icons?.[0].url,
                        reload: oldExt?.reload ?? false,
                        disable: oldExt?.disable ?? false,
                        uninstall: oldExt?.uninstall ?? false,
                    });
                });

                chrome.tabs.query({}, (tabs) => {
                    tabs.forEach((tab) => {
                        const oldSite = oldWebsitesMap[tab.id];
                        data_placeholder.websites.push({
                            site_name: tab.title,
                            site_id: tab.id,
                            icon: tab.favIconUrl,
                            url: tab.url,
                            reload: oldSite?.reload ?? false,
                        });
                    });

                    if (typeof callback === "function") {
                        callback(data_placeholder);
                    } else {
                        resolve(data_placeholder);
                    }
                });
            });
        });
    });
}

2. 修改popup事件监听,绑定唯一ID而非索引

不再依赖索引,通过元素携带的唯一ID找到对应数据对象:

// 重写update_data函数
function update_data(checkbox, type, itemId, method) {
    if (checkbox.nodeName === "INPUT" && checkbox.type === "checkbox") {
        checkbox.addEventListener("change", () => {
            // 通过ID定位目标元素
            const targetItem = data_placeholder[type].find(item => {
                return type === "extensions" ? item.ext_id === itemId : item.site_id === itemId;
            });
            if (targetItem) {
                targetItem[method] = checkbox.checked;
            }
        });
    } else {
        console.warn("This shouldn't be happening: Error(Incompatible type)");
    }
}

// 在initialize_data中调用时传入ID
// 扩展部分
update_data(reload, "extensions", extension.ext_id, "reload");
update_data(uninstall, "extensions", extension.ext_id, "uninstall");
update_data(disable, "extensions", extension.ext_id, "disable");

// 网站部分
update_data(reload, "websites", website.site_id, "reload");

3. 优化popup初始化逻辑,清空旧DOM

每次打开popup时清空旧列表,避免重复渲染:

function initialize_data() {
    chrome.runtime.sendMessage({ message: "get_data" }, (data) => {
        data_placeholder = structuredClone(data);
        console.log("Popup data: ", data_placeholder);
        
        // 清空旧扩展列表
        const extList = document.getElementById("list-ext");
        extList.innerHTML = "";
        if (data.extensions.length > 0) {
            // 原扩展渲染逻辑
        }

        // 清空旧网站列表
        const siteList = document.getElementById("list-site");
        siteList.innerHTML = "";
        if (data.websites.length > 0) {
            // 原网站渲染逻辑
        }
    });
}

数据一致性学习建议

  • 始终用唯一标识符(如ID)而非索引关联数据与DOM,索引在排序/增删元素后会失效。
  • 处理数组数据时,用reduce转成键值对对象,可大幅提升查找匹配效率。
  • Chrome扩展中chrome.storage是异步操作,确保读写完成后再执行后续逻辑,避免状态不一致。

内容的提问来源于stack exchange,提问作者xrypt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 13:37:34