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

Next.js缓存页面中MutationObserver失效的原因与解决方法

问题描述

开发一款Chrome扩展,用于在Next.js构建的ChatGPT网站中,给动态生成的重复容器添加按钮。需求是页面初始加载和客户端路由切换时都能插入按钮。

目前能检测到初始加载和路由切换并触发函数,但用querySelector找不到目标元素——因为组件不是立即渲染的。用MutationObserver时,首次访问页面有效,但加载缓存页面(可在Chrome开发者工具>Application>XHR标签查看)时,哪怕body被清空重渲染,MutationObserver也检测不到变化。需要解释原因并给出修复方案。


现有配置与代码

manifest.json

{
    "manifest_version": 3,
    "name": "chatGPT UI",
    "description": "Extension to experiment with and add useful user interface elements to OpenAI's chatGPT",
    "version": "1.0",
    "icons": {
        "16": "images/icon-16.png",
        "32": "images/icon-32.png",
        "64": "images/icon-64.png",
        "128": "images/icon-128.png"
    },
    "permissions": [
        "tabs",
        "webNavigation"
    ],
    "content_scripts": [
        {
            "js": ["scripts/content.js"],
            "matches": [
                "https://chat.openai.com/*"
            ]
        }
    ]
}

原content.js代码

// Content script is executed on initial page loads

// MutationObserver is executed for newly added elements for both the initial
// page loads and client-side routing as long as the pages have not been cached

const isChatPanelNode = (node) => {
    return node.matches('#__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1 > div > main > div.flex-1.overflow-hidden > div');
};

const isEditButtonContainerNode = () => {
    return node.matches('div.text-gray-400.flex.self-end.lg\:self-center.justify-center.mt-2.gap-2.md\:gap-3.lg\:gap-1.lg\:absolute.lg\:top-0.lg\:translate-x-full.lg\:right-0.lg\:mt-0.lg\:pl-2');
};

const getAllEditButtonContainers = () => {
    return [...document.querySelectorAll('div.text-gray-400.flex.self-end.lg\:self-center.justify-center.mt-2.gap-2.md\:gap-3.lg\:gap-1.lg\:absolute.lg\:top-0.lg\:translate-x-full.lg\:right-0.lg\:mt-0.lg\:pl-2')];
}

const setBackgroundColor = ( element, color ) => {
    element.style.backgroundColor = color;
}

const observer = new MutationObserver((mutations) => {
    console.log(`New mutations observed`);
    mutations.forEach((record) => {
        if (record.type === 'attributes') {}
        // mutation to the tree of nodes
        if (record.type === 'childList') {
            let editButtonContainers = null;
            record.addedNodes.forEach((node) => {
                // ignore text nodes
                if (node.nodeType === 3) return;
                
                if (isChatPanelNode(node)) {
                    editButtonContainers = getAllEditButtonContainers();
                    editButtonContainers.forEach(( container ) => setBackgroundColor( container, 'yellow' ));
                }
            });
        }
    });
});

const config = {
    childList: true, // changes in the direct children of node
    subtree: true, // changes in all descendants of node
    attributes: true, // changes attributes of node
    // attributeFilter // an array of attribute names, to observe only selected ones
    characterData: false // whether to observe node.data (text content)
};

const node = document.body;
observer.observe(node, config);

问题原因分析

原代码的核心错误是对MutationObserver的addedNodes机制理解偏差:

  • MutationObserver的addedNodes仅包含直接被添加到DOM的节点,也就是变更操作中最上层的父节点,不会直接包含你要找的目标子元素
  • 原代码只在addedNodes匹配到聊天面板节点时才全局查询目标容器,缓存页面渲染时,聊天面板可能不是直接被添加的节点,或者全局查询时机不对,导致无法捕获到目标元素

修复方案

修改后的content.js代码

// Content script is executed on initial page loads

// MutationObserver is executed for newly added elements for both the initial
// page loads and client-side routing as long as the pages have not been cached

const isChatPanelNode = (node) => {
    return node.matches('#__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1 > div > main > div.flex-1.overflow-hidden > div');
};

const isEditButtonContainerNode = () => {
    return node.matches('div.text-gray-400.flex.self-end.lg\:self-center.justify-center.mt-2.gap-2.md\:gap-3.lg\:gap-1.lg\:absolute.lg\:top-0.lg\:translate-x-full.lg\:right-0.lg\:mt-0.lg\:pl-2');
};

const getAllEditButtonContainers = (element) => {
    return [...element.querySelectorAll('div.group.w-full.text-gray-800.dark\:text-gray-100.border-b.border-black\/10.dark\:border-gray-900\/50.dark\:bg-gray-800 > div div > div.text-gray-400.flex.self-end.lg\:self-center.justify-center.mt-2.gap-2.md\:gap-3.lg\:gap-1.lg\:absolute.lg\:top-0.lg\:translate-x-full.lg\:right-0.lg\:mt-0.lg\:pl-2')];
}

const setBackgroundColor = ( element, color ) => {
    element.style.backgroundColor = color;
}

const observer = new MutationObserver((mutations) => {
    console.log(`New mutations observed`);
    mutations.forEach((record) => {
        let editButtonContainers = null;
        if (record.type === 'attributes') {
        }
        // mutation to the tree of nodes
        if (record.type === 'childList') {
            record.addedNodes.forEach((node) => {
                if (node.nodeType === 3) return; // ignore text nodes

                editButtonContainers = getAllEditButtonContainers(node);
                editButtonContainers.forEach(editButtonContainer => setBackgroundColor(editButtonContainer, 'yellow'));
            });
        }
    });
});

const config = {
    childList: true, // changes in the direct children of node
    subtree: true, // changes in all descendants of node
    attributes: true, // changes attributes of node
    // attributeFilter // an array of attribute names, to observe only selected ones
    characterData: false // whether to observe node.data (text content)
};

const node = document;
observer.observe(node, config);

关键修改点

  1. 调整查询范围:getAllEditButtonContainers改为接收一个元素参数,在该元素内部查询目标容器,而非全局查询,避免遗漏新增节点内的目标元素
  2. 遍历逻辑优化:遍历addedNodes时,无需判断是否为聊天面板节点,直接在每个新增节点内查询目标容器,覆盖所有可能包含目标元素的场景
  3. 扩大观察范围:将观察目标从document.body改为document,确保能捕获到所有DOM层级的变更,包括缓存页面渲染时的body替换操作

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 07:32:16