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"  
  ]  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

其中有些配置和权限是其他功能的,跟本次复活 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});  
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

代码在 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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这段代码的功能是:

  1. 获取所有打开的 Chrome 窗口及其内容。
  2. 遍历每个窗口,检查并处理特定的窗口关闭条件。
  3. 确认每个窗口的第一个标签页是否是预期的,如果不是,则在该窗口中创建一个新的标签页。

通过这些操作,确保所有窗口都包含特定的标签页,并进行必要的检查和处理。

新建窗口

chrome.windows.onCreated.addListener(createNewWindow);

function createNewWindow(window) {  
    if (typeof window !== "undefined" && window.type === "normal" && !checkIfFirstTabIsOurs(window)) {  
        createTabInWindow(window);  
    }  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这段代码的功能是:

  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.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

这段代码的功能是:

  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.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这段代码的功能是:

  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.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

这段代码的功能是:

  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.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这段代码的功能是:

  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);  
            }  
        }  
    }  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.

代码功能这里就不展示细节了。有兴趣的可以后台联系我,来试用一下 复活版 lastTab 拓展。