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
擴充套件程式安裝時執行以下操作:
- 設定擴充套件圖示上的徽章文字:在擴充套件圖示上顯示 "Fun" 字樣的徽章。
- 設定徽章的背景顏色:將徽章的背景顏色設定為紅色。
- 建立一個新的標籤頁並開啟指定的頁面:在瀏覽器中建立一個新的標籤頁,並開啟擴充套件程式目錄下的 "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]);
}
}
這段程式碼的功能是:
- 獲取所有開啟的 Chrome 視窗及其內容。
- 遍歷每個視窗,檢查並處理特定的視窗關閉條件。
- 確認每個視窗的第一個標籤頁是否是預期的,如果不是,則在該視窗中建立一個新的標籤頁。
透過這些操作,確保所有視窗都包含特定的標籤頁,並進行必要的檢查和處理。
新建視窗
chrome.windows.onCreated.addListener(createNewWindow);
function createNewWindow(window) {
if (typeof window !== "undefined" && window.type === "normal" && !checkIfFirstTabIsOurs(window)) {
createTabInWindow(window);
}
}
這段程式碼的功能是:
- 監聽新的 Chrome 視窗建立事件。
- 當新視窗建立時,呼叫
createNewWindow
函式。 - 在
createNewWindow
函式中,檢查新建立的視窗是否為正常型別視窗,並且第一個標籤頁是否為預期的標籤頁。 - 如果第一個標籤頁不是預期的,則在該視窗中建立一個新的標籤頁。
透過這些操作,確保在每次建立新視窗時,都包含特定的標籤頁。
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);
}
});
}
}
這段程式碼的功能是:
- 監聽標籤頁被移除的事件。
- 當一個標籤頁被移除時,呼叫
tabRemoved
函式,並傳遞標籤頁的 ID 和移除資訊。 - 在
tabRemoved
函式中,檢查被移除標籤頁所在的視窗 ID。 - 獲取該視窗的詳細資訊,並檢查視窗是否為正常型別。
- 延遲 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...")
}
這段程式碼的功能是:
- 監聽標籤頁被從視窗中分離的事件。
- 當一個標籤頁被分離時,呼叫
tabDetached
函式,並傳遞標籤頁的 ID 和分離資訊。 - 在
tabDetached
函式中,記錄標籤頁分離的日誌資訊。 - 延遲 1000 毫秒後,獲取所有開啟的 Chrome 視窗及其內容,並呼叫
initialCheck
函式進行處理。 - 記錄檢查視窗的日誌資訊。
透過這些操作,確保在標籤頁分離後,對所有視窗進行檢查和必要的處理。
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);
}
});
}
}
這段程式碼的功能是:
- 監聽標籤頁被啟用的事件。
- 當一個標籤頁被啟用時,呼叫
tabActivated
函式,並傳遞啟用資訊。 - 在
tabActivated
函式中,記錄啟用事件的日誌資訊。 - 檢查啟用資訊中是否包含視窗 ID。
- 獲取該視窗的詳細資訊並檢查視窗中的標籤頁:
- 如果視窗的第一個標籤頁處於啟用狀態,並且視窗中有多個標籤頁,則延遲 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);
}
這段程式碼的功能是:
- 監聽新標籤頁建立的事件。
- 當一個新標籤頁被建立時,呼叫
checkTab
函式,並傳遞新建立的標籤頁資訊。 - 在
checkTab
函式中,記錄標籤頁建立的日誌資訊。 - 延遲 300 毫秒後,獲取新標籤頁所在視窗的詳細資訊。
- 呼叫
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
- 測試理論雞湯
- 社群風采&影片合集