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

Chrome扩展(Manifest V3)多标签下按钮点击事件绑定失效求助

问题

开发了一个Manifest V3的Chrome扩展,仅使用Content Script,功能是在多个ServiceTitan网页上添加自定义按钮并绑定点击事件。单标签页下功能正常,但多标签页时,部分标签页的按钮能显示,但偶尔出现点击事件未绑定的情况,该问题偶发可复现。

绑定点击事件的代码:

$("#btnEZsaltGo").on("click", function(){ezsaltButtonClicked()});

尝试检测事件绑定的函数(始终返回null,即使事件生效):

function isClickHandlerAttachedToButton(){
    // check whether click event handler has been attached
    var clickHandlers = $("#btnEZsaltGo").data('events')?.click;
    var clickHandlerIsAttached = (clickHandlers && clickHandlers.length > 0);
    return clickHandlerIsAttached;
}

完整源码:

var interval = 0;
var rootEZsaltURL;

console.log("EZsaltGo extension loaded...");

if(location.href.indexOf("next.servicetitan.com") > -1 || location.href.indexOf("integration.servicetitan.com") > -1){ // Sandbox ServiceTitan
    rootEZsaltURL = "https://test.ezsaltgo.com/ServiceTitanLanding.php?CustomerID="; // TEST EzsaltGo
}else if(location.href.indexOf("go.servicetitan.com") > -1){ // LIVE ServiceTitan
    rootEZsaltURL = "https://ezsaltgo.com/ServiceTitanLanding.php?CustomerID="; // LIVE EZsaltGo
}

(function() { // initial page load (static page content)
    interval = setInterval(function(){ checkForEZsaltButton(); }, 1000);
})();

function checkForEZsaltButton() {
    if($("#btnEZsaltGo").length == 0){ // only create the ezsalt button if it's not already there
        var insertBeforeAnchorElement; // insert the EZsaltGo button BEFORE this element
        var insertAfterAnchorElement; // insert the EZsaltGo button AFTER this element
        var pageType;
        var buttonMarginLeft = 0;
        var buttonMarginRight = 0;
        var hasBillingAddress = ($("span:contains('Billing Address')").length > 0);
        var hasServiceAddress = ($("span:contains('Service Address')").length > 0);
        var hasMoreActionsMenu = ($('div[data-testid="more-actions-menu"]').length > 0);
        var hasCustomerIDSpan = ($("span:contains('Customer ID')").length > 0);
        var hasCustomerIDLink = ($('a[role="link"][href*="/Customer/"]').length > 0);
        var isModal = ($("h2.Modal__header-title").length > 0);
        
        if((hasBillingAddress || hasServiceAddress) && hasMoreActionsMenu && (hasCustomerIDSpan || (!hasCustomerIDSpan && hasCustomerIDLink))){
            // clearInterval(interval);
            // console.log("Customer DIV loaded!! Interval stopped.");
            
            if(isModal){
                // we're inside the modal customer window
                pageType = "modal";
                insertAfterAnchorElement = $("h2.Modal__header-title"); 
            }else{
                // we're inside a normal ST web page - let's figure out which page exactly

                if(location.href.indexOf("/customer/") > 0) pageType = "customer";
                if(location.href.indexOf("/location/") > 0) pageType = "location";

                if(pageType == "customer"){ 
                    insertBeforeAnchorElement = $("div.ButtonGroup").parent();
                    // no margin padding needed
                }
                
                if(pageType == "location"){ 
                    insertBeforeAnchorElement = $("div.ButtonGroup").parent().parent();
                    buttonMarginRight = 19;
                }
            }
            
            var ezsaltButton = $('<button id="btnEZsaltGo" class="Button Button--blue Button--solid Button--focus-visible" style="' + (buttonMarginRight > 0 ? "margin-right: " + buttonMarginRight + "px" : "") + (buttonMarginLeft > 0 ? "margin-left: " + buttonMarginLeft + "px" : "") + '>EZsaltGo</button>');
            
            if(insertBeforeAnchorElement)
                ezsaltButton.insertBefore(insertBeforeAnchorElement);
            else if(insertAfterAnchorElement)
                ezsaltButton.insertAfter(insertAfterAnchorElement);
            else
                return; // we don't have anything to anchor against, so just return
        }
        
        $("#btnEZsaltGo").on("click", function(){ezsaltButtonClicked()});
        
        // attach click event to the little X at the top right of the modal - if that is clicked, the modal will close, and we need to resume checking
        if($(".Modal__header-close").length > 0)
            $(".Modal__header-close").on("click", function(){
                //interval = setInterval(function(){ checkForEZsaltButton(); }, 1000);
                console.log("Modal closed. Interval check resuming...");
            });
    }else{
        // check whether click event handler has been attached - doesn't work

        // var clickHandlers = $("#btnEZsaltGo").data('events')?.click; // clickHandlers always null even when button is there and click handler is attached
        // if(clickHandlers && clickHandlers.length > 0) doSomething(); //  clickHandlers always null even when button is there and click handler is attached
    }
}

function ezsaltButtonClicked(){
    console.log("EZsalt button clicked!");
    
    if(rootEZsaltURL && rootEZsaltURL != ""){
        var customerID = getCustomerIDForEZsalt();
        
        if(customerID && customerID != ""){
            window.open(rootEZsaltURL + customerID);    
        }else{
            alert("Unable to locate Customer ID. Contact EZsalt."); 
        }
    }else{
        alert("EZsalt URL could not be obtained. Contact EZsalt."); 
    }
}

function getCustomerIDForEZsalt(){
    var hasCustomerIDSpan = ($("span:contains('Customer ID')").length > 0);
    
    if(hasCustomerIDSpan){
        var customerIDSpan = $("span:contains('Customer ID')");
        var customerIDChunk = customerIDSpan.next('p');
        return $.trim(customerIDChunk.html());
    }else{
        var hasCustomerIDLink = ($('a[role="link"][href*="/Customer/"]').length > 0);
        
        if(hasCustomerIDLink){
            var hrefChunk = $('a[role="link"][href*="/Customer/"]').attr('href');
            var customerID = $.trim(hrefChunk.match(/\d+/)[0]);
            return customerID;
        }
    }
    
    return ""; // something went wrong - should never get this far
}

解决方案

  • 改用事件委托绑定点击事件:直接绑定动态元素容易因DOM重新渲染导致事件丢失,委托给稳定的父元素(比如document)能确保事件始终生效。修改绑定代码为:

    $(document).on("click", "#btnEZsaltGo", ezsaltButtonClicked);
    

    移除原来直接绑定到按钮的代码,这样不管按钮何时被创建或替换,点击事件都会被捕获。

  • 修正事件检测逻辑:jQuery 1.8及以上版本不再将事件存储在data('events')中,所以原检测方法无效。如果需要验证事件状态,可以直接触发测试点击,或者确认委托事件已在页面加载时初始化(无需每次定时器执行都重复绑定)。

  • 替换定时器为MutationObserver:定时器每秒轮询可能导致竞态条件(比如按钮未插入DOM就绑定事件),用MutationObserver监听目标区域的DOM变化,能精准捕捉按钮需要插入的时机,减少不必要的执行,提升稳定性。示例代码:

    // 替换原定时器初始化代码
    (function() {
      const observer = new MutationObserver(() => {
        checkForEZsaltButton();
      });
      observer.observe(document.body, { childList: true, subtree: true });
    })();
    

    同时在按钮创建成功后,可以根据需要停止观察特定区域,避免重复执行。

  • 确保脚本隔离性:虽然Manifest V3默认隔离每个标签页的Content Script,但将代码完全包裹在IIFE中,避免全局变量潜在的冲突问题,确保每个标签页的状态独立:

    (function() {
      // 所有代码放在这里
      var interval = 0;
      var rootEZsaltURL;
      // ... 剩余代码
    })();
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 03:50:12