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

Chrome扩展Content Script代码失效:YouTube存视频至播放列表异常

问题分析与解决方案

你的代码在Chrome开发者工具控制台能正常运行,但复制到扩展Content Script中就失效,核心原因是Content Script与页面脚本上下文隔离,再加上YouTube使用Polymer/React这类框架,直接操作DOM属性或调用click()无法触发框架内部的状态更新和事件绑定。以下是针对性修复方案:

核心问题拆解

  • 输入框值修改无效:YouTube的输入框是受控组件,直接改value属性不会同步到框架内部状态,必须触发框架能识别的事件链。
  • 点击事件不生效:页面元素的点击事件绑定在页面上下文脚本中,Content Script里的原生click()无法触发这些绑定。
  • 元素选择器不准确:#endpoint这类ID在页面中可能存在多个实例,会导致选中错误元素。

修复后的代码

function waitForElm(selector, checkVisibility = false) {
    return new Promise(resolve => {
        const checkElement = () => {
            const elm = document.querySelector(selector);
            if (elm && (!checkVisibility || elm.offsetParent !== null)) {
                return elm;
            }
            return null;
        };

        const existingElm = checkElement();
        if (existingElm) {
            return resolve(existingElm);
        }

        const observer = new MutationObserver(mutations => {
            const elm = checkElement();
            if (elm) {
                observer.disconnect();
                resolve(elm);
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

// 模拟真实点击,适配页面上下文的事件绑定
function simulateClick(element) {
    const event = new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window,
        composed: true // 关键:允许事件穿透Shadow DOM
    });
    element.dispatchEvent(event);
}

// 模拟真实输入,触发框架状态更新
function setInputValue(element, value) {
    // 先清空原有值并触发事件
    element.value = '';
    element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
    
    // 设置新值并触发完整事件链
    element.value = value;
    element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
    element.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
    
    // 适配Polymer组件的专属事件
    if (element.dispatchEvent) {
        element.dispatchEvent(new CustomEvent('iron-input-change', { bubbles: true, composed: true }));
    }
}

let name = "Playlist" + parseInt(Math.random()*100);

async function savePlaylist(listname) {
    // 等待可见的保存按钮并点击
    let saveBtn = await waitForElm("[aria-label='Save to playlist']", true);
    simulateClick(saveBtn);

    // 等待"创建播放列表"按钮加载
    let createPlaylistBtn = await waitForElm('ytd-playlist-add-to-option-renderer #endpoint', true);
    // 检查目标播放列表是否存在
    let checkboxElm = await waitForElm(`yt-formatted-string[title='${listname}']`, false);
    
    if (checkboxElm) {
        let checkboxContainer = checkboxElm.closest('ytd-playlist-add-to-option-renderer');
        let isChecked = checkboxContainer.querySelector('input').checked;
        console.log('播放列表已存在,选中状态:', isChecked);
        if (!isChecked) {
            simulateClick(checkboxElm);
        } else {
            console.log('视频已在播放列表中');
        }
    } else {
        console.log('创建新播放列表:', listname);
        // 点击创建按钮
        simulateClick(createPlaylistBtn);
        
        // 等待可见的输入框
        let listInput = await waitForElm('#name-input', true);
        // 设置输入框值
        setInputValue(listInput, listname);
        console.log('输入框值已设置为:', listInput.value);
        
        // 等待"创建"确认按钮并点击
        let confirmCreateBtn = await waitForElm('ytd-button-renderer#actions button[aria-label="Create"]', true);
        simulateClick(confirmCreateBtn);
    }

    // 等待保存成功通知
    const waitNotif = new MutationObserver(mutations => {
        let notifs = document.querySelectorAll("tp-yt-paper-toast");
        for (let notif of notifs) {
            const textElm = notif.querySelector('.toast-message');
            if (textElm && textElm.textContent.includes("Saved to ")) {
                console.log('视频已成功保存到播放列表');
                waitNotif.disconnect();
                return;
            }
        }
    });

    const popupContainer = await waitForElm("ytd-popup-container");
    waitNotif.observe(popupContainer, {
        childList: true,
        subtree: true
    });
}

savePlaylist(name);

关键改动说明

  1. 增强元素等待逻辑:新增checkVisibility参数,确保等待的元素是可见的,避免选中隐藏的DOM节点。
  2. 模拟真实点击:使用带composed: true的MouseEvent,让事件能穿透Shadow DOM并触发页面上下文的事件绑定。
  3. 受控输入框处理:通过触发input、change甚至Polymer专属的iron-input-change事件,确保框架能感知到值的变化。
  4. 精准元素选择:修改选择器为更具体的路径,避免选中错误元素。
  5. 简化通知检测:直接选择.toast-message元素,更稳定地判断保存成功状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 01:35:56