隐藏扩展名为 chrome 的 DOM 元素而不引起闪烁

Hiding DOM elements with a chrome extension without causing a flicker

前言:

我知道那里有一个 duplicate question。我再次发布它是因为它没有答案(而且是 4 年前的)。

我想要的大致描述:

我希望能够在 DOM 加载到视图之前隐藏 DOM 元素(添加 Element.style.display = "none")。

我试过的:

其他帖子指出在 document 元素上使用 MutationObserver 并运行将其

为了确保我们能够在加载 DOM 之前隐藏元素,我们要 运行 包含 MutationObserver 的脚本作为 content_script"run_at":"document_start".

我做了所有这些,但我仍然看到闪烁(元素在我加载页面时出现,然后很快消失)。

我想做什么:

有一个 ul,其中包含一些 li 以及我将 content_script.js 注入的页面上的一些文本。我用 <text, checkbox> 对填充我的 popup.html。如果选中该复选框,则包含所述文本的 li 可见,否则隐藏。我希望它在刷新之间持续存在,因此使用 storage.

一切正常 - 但每当我刷新页面时都会出现闪烁。元素在那里,然后它们就消失了。我不想让他们首先出现!

我的代码:

当我检测到我可能删除的 DOM 元素已加载时,我会生成一个对象,指示我是否应该隐藏或保持该特定 DOM 元素可见。 然后我相应地将其 Element.style.display 设置为 noneblock

/**manifest.json
...
"content_scripts": [
    {
      "matches": [
        "some_website_url"
      ],
      "js": [
        "content_script.js"
      ],
      "run_at": "document_start"
    }
  ]
...
*/

///content_script.js
const mutationObserver = new MutationObserver((mutations) => {
    for (const { addedNodes } of mutations) {
        for (const node of addedNodes) {
            if (node.tagName) {
                if (node.querySelector(potentially_hidden_element_selector)) {
                    chrome.storage.sync.get("courses", ({ courses }) => {
                        chrome.storage.sync.set({ "courses": generateCourseList(courses) }, () => {
                            const courseElements = Array.from(node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'))
                            courseElements.forEach(courseElement => {
                                const courseName = getCourseName(courseElement)
                                const isVisible = courses[courseName]
                                updateCourseElementInSidebar(courseElement, isVisible)
                            })
                        })
                    })
                    // We found what we were looking for so stop searching
                    mutationObserver.disconnect()
                }
            }
        }
    }
})

mutationObserver.observe(document, { childList: true, subtree: true })

编辑 1:

我的 generateCourseList 方法取决于我可能试图隐藏的 DOM 元素 - 所以我无法在 DOM 加载之前调用 chrome.storage.set 方法想想。

当我刷新页面时,课程列表最终填充 DOM。 然后我根据这些课程元素的 innerText 属性填充存储的 courses 对象。我根据以下两个因素之一将这些元素的可见性设置为 truefalse:如果此课程已在 courses 对象中定义,则保持其可见性状态,否则't,设置为true(默认可见)。

我无法确定某些 DOM 元素 visible/hidden 如果我没有参考它们。因此,如果我尝试在那些特定的 DOM 元素加载之前调用 generateCourseList,我最终会尝试检索所有课程元素 (document.querySelectorAll('a[data-parent-key="mycourses"]')) 并且什么也得不到返回。由于这个 chrome.storage.sync.set({ "courses": generateCourseList(courses) }....

,我最终将 chrome.storage 中的 courses 设置为空

编辑 2:

这是我的全部代码。我会尽快 chrome.storage.sync.get,并且尽量不依赖 chrome.storage.sync.set.

的结果

我尝试尽快删除这些元素,但我很难做到。这是因为我很难知道我想要访问的内容(课程元素)何时已完全加载。以前,我正在检测一个课程元素何时可见,当它可见时,我假设所有元素都是可见的。这是一个错误。我能够在它弹出的那一刻访问一个课程元素,但有时实际上只加载了 6 个课程元素中的 4 个。我不能硬编码这个数字,因为它因人而异。我不能一个一个地解决它们,因为那样我就不知道什么时候断开 MutationObserver 的连接。我使用调试器并试图在所有 6 个课程元素加载后立即找到加载的元素,即 header#page-header.row 元素。我仍然会出现闪烁,虽然没有以前那么明显了。

我可以做些什么来让它不那么引人注目?

function start_mutation_observer() {
    chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
        const observer = new MutationObserver((mutations) => {
            for (const { addedNodes } of mutations) {
                for (const node of addedNodes) {
                    // The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
                    if (document.querySelector('header#page-header.row')) {
                        observer.disconnect()

                        const generatedCourses = generateCourseList(savedCourses)
                        const courseElements = getCourseElements()

                        // Set visibility of course elements 
                        courseElements.forEach(courseElement => {
                            const courseName = getCourseElementTextContent(courseElement);
                            const isShown = generatedCourses[courseName];
                            setCourseElementVisibility(courseElement, isShown);
                        });

                        chrome.storage.sync.set({ 'savedCourses': generatedCourses });
                        return
                    }
                }
            }
        });
        observer.observe(document, { childList: true, subtree: true });

        // In case the content script has been injected when some of the DOM has already loaded
        onMutation([{ addedNodes: [document.documentElement] }]);
    });
}

function getCourseElements() {
    const COURSE_ELEMENT_SELECTOR = 'ul > li > a[data-parent-key="mycourses"]'
    return Array.from(document.querySelectorAll(COURSE_ELEMENT_SELECTOR))
}

function getCourseElementTextContent(courseElement) {
    const COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR = 'a[data-parent-key="mycourses"] > div > div > span.media-body'
    return courseElement.querySelector(COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR).textContent
}

function generateCourseList(savedCourses) {
    // Turns [[a, b], [b,c]] into {a:b, b:c}
    return Object.fromEntries(getCourseElements().map(courseElement => {
        const courseName = getCourseElementTextContent(courseElement)
        const isShown = savedCourses[courseName] ?? true
        return [courseName, isShown]
    }))
}

function setCourseElementVisibility(courseElement, isShown) {
    if (isShown) {
        courseElement.style.display = "block"
    } else {
        courseElement.style.display = "none"
    }
}

start_mutation_observer()

编辑 3:

我认为它现在已经很好了。我只刷新刚刚加载到 DOM 中的课程元素的可见性。现在基本上没有闪烁(有轻微的闪烁,但没有我的扩展,它的闪烁量是一样的)。

这是 MutationObserver 的代码

function start_mutation_observer() {
    let handledCourseElements = new Set()
    chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
        const observer = new MutationObserver((mutations) => {
            for (const { addedNodes } of mutations) {
                for (const node of addedNodes) {
                    const courseElements = getCourseElements()
                    const courseElementsAdded = courseElements.length > handledCourseElements.size
                    // If a courseElement was added, update visibility of those that weren't already processed 
                    if (courseElementsAdded) {
                        const generatedCourses = generateCourseList(savedCourses)
                        courseElements
                            .filter(courseElement => !handledCourseElements.has(courseElement))
                            .forEach(courseElement => {
                                const courseName = getCourseElementTextContent(courseElement)
                                const courseShouldBeVisible = generatedCourses[courseName];
                                setCourseElementVisibility(courseElement, courseShouldBeVisible);
                                handledCourseElements.add(courseElement)
                            })
                    }

                    // The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
                    if (document.querySelector('header#page-header.row')) {
                        observer.disconnect()
                        chrome.storage.sync.set({ 'savedCourses': generateCourseList(savedCourses) });
                        return
                    }
                }
            }
        });
        observer.observe(document, { childList: true, subtree: true });

        // In case the content script has been injected when some of the DOM has already loaded
        onMutation([{ addedNodes: [document.documentElement] }]);
    });
}

读取存储速度慢且不同步,需要一开始就做:

chrome.storage.sync.get('courses', ({ courses }) => {
  chrome.storage.sync.set({ 'courses': generateCourseList(courses) });
  const observer = new MutationObserver(onMutation);
  observer.observe(document, { childList: true, subtree: true });
  onMutation([{addedNodes: [document.documentElement]}]);
  function onMutation(mutations) {
    for (const { addedNodes } of mutations) {
      for (const node of addedNodes) {
        if (node.tagName && node.querySelector(potentially_hidden_element_selector)) {
          observer.disconnect();
          processNode(node, courses);
        }
      }
    }
  }
});

function processNode(node, courses) {
  const courseElements = Array.from(
    node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'));
  courseElements.forEach(courseElement => {
    const courseName = getCourseName(courseElement);
    const isVisible = courses[courseName];
    updateCourseElementInSidebar(courseElement, isVisible);
  });
}