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);
关键修改点
- 调整查询范围:
getAllEditButtonContainers改为接收一个元素参数,在该元素内部查询目标容器,而非全局查询,避免遗漏新增节点内的目标元素 - 遍历逻辑优化:遍历
addedNodes时,无需判断是否为聊天面板节点,直接在每个新增节点内查询目标容器,覆盖所有可能包含目标元素的场景 - 扩大观察范围:将观察目标从
document.body改为document,确保能捕获到所有DOM层级的变更,包括缓存页面渲染时的body替换操作
内容的提问来源于stack exchange,提问作者gradient_dissent
相关产品推荐
相关产品推荐

