lastTab—Chrome 擴充開發實踐

FunTester發表於2024-08-06

Chrome 作為桌面瀏覽器扛把子,其豐富的擴充是吸引眾多使用者的重要原因。當時在使用當中,當關閉了一個視窗的最後一個 Tab 的時候,整個視窗也會被關閉。這一點讓我非常頭疼,在早些年的時候,我接觸到了一個 lastTab 的擴充,非常完美的解決了我的問題。

但是好景不長,這個外掛下線了,猜測可能是因為 Chrome 升級了版本(2->3),外掛沒有及時更新導致的。後來我就從一些神奇的網站上找到歷史版本,使用離線安裝的方式繼續使用,及手續香。

在最近學習了 Chrome 擴充開發的基礎知識以後,突然香著手復活這個神器。努力了一段時間,算是有些成效,寫篇文章記錄一下。在 Chrome 商店裡面同樣的功能的擴充還有在更新,有個同款名字的擴充,目測是本子開發,功能不一樣,大家請注意甄別。

manifest 配置

經過前一篇文章的介紹,這裡就不多說了,先發一下 manifest 配置資訊。

{  
  "manifest_version": 3,  
  "version": "1.0",  
  "action": {  
    "default_icon": "funtester.png",  
    "default_popup": "popup.html"  // 設定預設的 Popup 頁面  
  },  
  "background": {  
    "service_worker": "background.js"  
  },  
  "description": "hello,FunTester !!!",  
  "icons": {  
    "48": "funtester.png",  
    "128": "funtester128.png"  
  },  
  "name": "FunTester Tab",  
  "offline_enabled": true,  
  "content_scripts": [  
    {  
      "matches": ["<all_urls>"],  
      "js": ["content.js"],  
      "run_at": "document_end",  
      "all_frames": true  
    }  
  ],  
  "host_permissions": ["<all_urls>"],  
  "permissions": [  
    "tabs",  
    "storage",  
    "contextMenus",  
    "scripting",  
    "activeTab"  
  ]  
}

其中有些配置和許可權是其他功能的,跟本次復活 lastTab 無關,由於專案開發的其他功能,不太好恢復,懶得改了。

background

下面就是 lastTab 的核心功能區了。首先說一下簡單的原理,Chrome 擴充提供了一些瀏覽器事件的監聽,然後做出相應的處理。而 lastTab 擴充的核心就是保障一個視窗至少有兩個 Tab ,其中第一個(index=0)屬於擴充自定義,第二個如果是使用者頁面則不會改動。當使用者關閉掉倒數第二個頁面的時候,建立一個新的頁面,預設使用的是瀏覽器的 newTab 頁面。下面分享一下我對於這些邏輯的實現。

安裝

安裝並不是 lastTab 的功能,這裡我新增了一些徽章和展示了一個頁面,主要是 FunTester 的原創文章列表。

chrome.runtime.onInstalled.addListener(function () {  
    chrome.action.setBadgeText({text: "Fun"});  
    chrome.action.setBadgeBackgroundColor({color: [255, 0, 0, 255]});  
    chrome.tabs.create({url: "caption.html", active: true});  
});

程式碼在 Chrome 擴充套件程式安裝時執行以下操作:

  1. 設定擴充套件圖示上的徽章文字:在擴充套件圖示上顯示 "Fun" 字樣的徽章。
  2. 設定徽章的背景顏色:將徽章的背景顏色設定為紅色。
  3. 建立一個新的標籤頁並開啟指定的頁面:在瀏覽器中建立一個新的標籤頁,並開啟擴充套件程式目錄下的 "caption.html" 檔案。

這些操作透過監聽擴充套件安裝事件,實現初始化邏輯和使用者介面的設定。

初始化

這裡在外掛安裝之後,初始化資源,主要建立第一個 Tab 並且固定。

chrome.windows.getAll({populate: true}, initialCheck)

function initialCheck(windows) {  
    for (let index = 0; index < windows.length; ++index) {  
        let window = windows[index];  
        checkWinowClose(window)  
        if (!checkIfFirstTabIsOurs(window)) createTabInWindow(windows[index]);  
    }  
}

這段程式碼的功能是:

  1. 獲取所有開啟的 Chrome 視窗及其內容。
  2. 遍歷每個視窗,檢查並處理特定的視窗關閉條件。
  3. 確認每個視窗的第一個標籤頁是否是預期的,如果不是,則在該視窗中建立一個新的標籤頁。

透過這些操作,確保所有視窗都包含特定的標籤頁,並進行必要的檢查和處理。

新建視窗

chrome.windows.onCreated.addListener(createNewWindow);

function createNewWindow(window) {  
    if (typeof window !== "undefined" && window.type === "normal" && !checkIfFirstTabIsOurs(window)) {  
        createTabInWindow(window);  
    }  
}

這段程式碼的功能是:

  1. 監聽新的 Chrome 視窗建立事件。
  2. 當新視窗建立時,呼叫 createNewWindow 函式。
  3. createNewWindow 函式中,檢查新建立的視窗是否為正常型別視窗,並且第一個標籤頁是否為預期的標籤頁。
  4. 如果第一個標籤頁不是預期的,則在該視窗中建立一個新的標籤頁。

透過這些操作,確保在每次建立新視窗時,都包含特定的標籤頁。

Tab 被關閉

這裡相容的地方有點多,有時候當使用者操作時間過長可能會失敗,所以加上了 400 ms 的延遲。

chrome.tabs.onRemoved.addListener(tabRemoved);

function tabRemoved(tabId, removeInfo) {  
    console.info("Tab removed", tabId, removeInfo)  
    if (typeof removeInfo.windowId != 'undefined') {  
        chrome.windows.get(removeInfo.windowId, {"populate": true}, function (window) {  
            if (typeof window !== 'undefined' && window.type === "normal") {  
                setTimeout(function () {  
                    if (!checkTabIsOurs(window.tabs[0])) {  
                        createTabInWindow(window);  
                    } else if (window.tabs.length === 1) {  
                        createSecondTabInWindow(window)  
                    }  
                }, 400);  
            }  
        });  
    }  
}

這段程式碼的功能是:

  1. 監聽標籤頁被移除的事件。
  2. 當一個標籤頁被移除時,呼叫 tabRemoved 函式,並傳遞標籤頁的 ID 和移除資訊。
  3. tabRemoved 函式中,檢查被移除標籤頁所在的視窗 ID。
  4. 獲取該視窗的詳細資訊,並檢查視窗是否為正常型別。
  5. 延遲 400 毫秒後:
    • 檢查視窗的第一個標籤頁是否為預期的標籤頁,如果不是,則在視窗中建立一個新的標籤頁。
    • 如果視窗中只剩一個標籤頁,則在視窗中建立第二個標籤頁。

透過這些操作,確保在移除標籤頁後,視窗仍然包含預期的標籤頁或必要的數量的標籤頁。

Tab 分離

這裡跟上面有同樣的問題,分離的操作通常比較耗時,我加了 1000 ms 的延遲,但也不能保障每次都成功。

chrome.tabs.onDetached.addListener(tabDetached);

function tabDetached(tabId, detachInfo) {  
    console.info("Tab detached", tabId, detachInfo);  
    setTimeout(function () {  
        chrome.windows.getAll({populate: true}, initialCheck)  
    }, 1000);  
    console.info("Checking windows...")  
}

這段程式碼的功能是:

  1. 監聽標籤頁被從視窗中分離的事件。
  2. 當一個標籤頁被分離時,呼叫 tabDetached 函式,並傳遞標籤頁的 ID 和分離資訊。
  3. tabDetached 函式中,記錄標籤頁分離的日誌資訊。
  4. 延遲 1000 毫秒後,獲取所有開啟的 Chrome 視窗及其內容,並呼叫 initialCheck 函式進行處理。
  5. 記錄檢查視窗的日誌資訊。

透過這些操作,確保在標籤頁分離後,對所有視窗進行檢查和必要的處理。

Tab 啟用

chrome.tabs.onActivated.addListener(tabActivated);

function tabActivated(activeInfo) {  
    console.info("Tab activated", activeInfo);  
    if (typeof activeInfo.windowId !== 'undefined') {  
        chrome.windows.get(activeInfo.windowId, {"populate": true}, function (window) {  
            if (window.tabs[0].active === true && window.tabs.length > 1) {  
                setTimeout(function () {  
                    chrome.tabs.update(window.tabs[1].id, {active: true});  
                }, 200);  
            }  
        });  
    }  
}

這段程式碼的功能是:

  1. 監聽標籤頁被啟用的事件。
  2. 當一個標籤頁被啟用時,呼叫 tabActivated 函式,並傳遞啟用資訊。
  3. tabActivated 函式中,記錄啟用事件的日誌資訊。
  4. 檢查啟用資訊中是否包含視窗 ID。
  5. 獲取該視窗的詳細資訊並檢查視窗中的標籤頁:
    • 如果視窗的第一個標籤頁處於啟用狀態,並且視窗中有多個標籤頁,則延遲 200 毫秒後啟用視窗中的第二個標籤頁。

透過這些操作,確保在某些情況下,自動啟用視窗中的第二個標籤頁,而不是預設的第一個標籤頁。

Tab 建立

chrome.tabs.onCreated.addListener(checkTab);

function checkTab(tab) {
    console.info("Tab created", tab)
    setTimeout(function () {
        chrome.windows.get(tab.windowId, {"populate": true}, function (window) {
            checkWinowClose(window)
        });
    }, 300);
}

這段程式碼的功能是:

  1. 監聽新標籤頁建立的事件。
  2. 當一個新標籤頁被建立時,呼叫 checkTab 函式,並傳遞新建立的標籤頁資訊。
  3. checkTab 函式中,記錄標籤頁建立的日誌資訊。
  4. 延遲 300 毫秒後,獲取新標籤頁所在視窗的詳細資訊。
  5. 呼叫 checkWindowClose 函式對該視窗進行檢查。

透過這些操作,確保在新標籤頁建立後,對其所在的視窗進行特定的檢查和處理。

其他

很多功能的設計都可能會遭遇超時的問題,一般來講可以透過不斷地重試解決,但是這樣會讓功能變得非常複雜,為了相容極少部分場景,增加專案的複雜度,有點違背初衷了。

這裡有幾個方法並沒有在上面列出來,這裡補充一下:

function createTabInWindow(window) {  
    console.info("Creating tab in window", window.id, window.type);  
    if (window.type === "normal") {  
        chrome.tabs.create({  
            windowId: window.id,  
            index: 0,  
            url: "blank.html",  
            active: false,  
            pinned: true  
        }, (tab) => {  
            if (chrome.runtime.lastError) {  
                console.error(`Error creating tab: ${chrome.runtime.lastError.message}`);  
            }  
        });  
    }  
}  

function createNewWindow(window) {  
    if (typeof window !== "undefined" && window.type === "normal" && !checkIfFirstTabIsOurs(window)) {  
        createTabInWindow(window);  
    }  
}  

function createSecondTabInWindow(window) {  
    console.info("Creating second tab in window", window.id, window.type)  
    if (window.type === "normal") {  
        chrome.tabs.create({  
            "windowId": window.id,  
            "index": 1,  
            "url": "chrome://newtab",  
            "active": true,  
        })  
    }  
}  

function checkIfFirstTabIsOurs(window) {  
    try {  
        return typeof window.tabs !== 'undefined' && window.tabs[0].url !== "undefined" && window.tabs[0].url.search(regExpr) !== -1;  
    } catch (error) {  
        console.error("Error in checkIfFirstTabIsOurs:", error);  
        return false;  
    }  
}  

let regex = "^chrome-extension\:\/\/.+blank\.html$";  
let regExpr = new RegExp(regex);  

function checkTabIsOurs(tab) {  
    try {  
        return regExpr.test(tab.url);  
    } catch (error) {  
        console.error("Error in checkTabIsOurs:", error);  
        return false;  
    }  
}  

function checkWinowClose(window) {  
    if (typeof window.tabs !== 'undefined' && window.type === "normal" && window.tabs.length > 1) {  
        let tabs = window.tabs;  
        for (let i = 1; i < tabs.length; ++i) {  
            if (checkTabIsOurs(tabs[i])) {  
                chrome.tabs.remove(tabs[i].id);  
            }  
        }  
    }  
}

程式碼功能這裡就不展示細節了。有興趣的可以後臺聯系我,來試用一下 復活版 lastTab 擴充。

FunTester 原創精華
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章