稀土掘金技术社区 10月30日 09:51
Chrome“后悔药”扩展:一键恢复误关标签页
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

当女朋友不小心关闭标签页时,别再纠结Ctrl+Shift+T的键盘位置了!这款名为“后悔药”的Chrome扩展,能够直观展示最近关闭的标签页,支持单个或一键全部恢复。它还提供网站图标、关闭时间等详细信息,界面简洁流畅,且代码开源。安装仅需5分钟,通过复制代码、创建文件、加载扩展即可轻松搞定,让标签页恢复变得简单高效。

💡 **便捷恢复误关标签页:** 该Chrome扩展“后悔药”专为解决用户误关标签页的痛点而设计,提供了一个直观的界面来查看和恢复最近关闭的标签页,无需记忆复杂的快捷键。用户可以直接在扩展弹窗中进行操作,大大提升了效率。

🚀 **多种恢复模式与丰富信息:** 支持单个标签页恢复和一键全部恢复两种模式,满足不同场景的需求。同时,扩展还会显示每个关闭标签页的网站图标、标题、URL以及关闭时间,帮助用户快速识别并找回所需页面。

⚙️ **简洁高效的技术实现:** 该扩展基于Chrome的`sessions`、`tabs`和`favicon` API实现,代码简洁,权限最小化,仅需少量后台进程。其安装过程也极为简便,仅需5分钟即可完成,包括复制代码、创建文件和加载扩展,让技术门槛降到最低。

🌟 **开源与用户友好:** 代码已在GitHub上开源,用户可以自由修改界面和功能。扩展的设计注重用户体验,拥有顺滑的小动画和简洁的UI,力求在提供强大功能的同时,保持操作的便捷性。

原创 不一样的少年_ 2025-10-30 08:30 重庆

点击关注公众号,技术干货及时达。

女朋友经常手滑关掉标签页这事儿头大了?

跟女朋友说用 Ctrl/Cmd+Shift+T,她皱眉:“键盘上哪有这个键!!!”

让她翻历史记录,她摇头:“根本找不到,全是我今天打开的!”

最后指向左上角的“最近关闭”,她叹气:“才8个,根本不够用

行,那就不讲道理,直接解决问题。

于是我写了这个 Chrome 扩展—— 「“后悔药”」

点一下就能看到“最近关闭的标签页”,想恢复单个点一下,想全恢复一键搞定。还有网站图标、关闭时间、顺滑的小动画,装上就能用。代码已经开源,想改界面随便改。「5分钟搞定安装」:复制代码 → 创建文件 → 加载扩展 → 开始使用!

先看效果:  打开弹窗 → 点击恢复单个 → 全部恢复

功能亮点自动列出最近关闭的标签页(最多 25 个)

支持单个恢复、全部恢复

显示网站图标、标题、URL、关闭时间

平滑动效与简洁 UI

零后台进程,权限最小化(仅用 sessions、tabs、favicon)

核心技术实现本扩展主要依赖以下 3 个 Chrome API 实现核心功能:


    // 获取最近关闭的标签页列表
    chrome.sessions.getRecentlyClosed({ maxResults25 }, (sessions) => {
      // sessions 包含关闭的标签页、窗口数据
    });

    // 恢复特定标签页
    chrome.sessions.restore(sessionId, (restoredSession) => {
      // 恢复成功后的回调
    });

    // Chrome 内部获取网站图标的专用方式
    function getFaviconURL(url) {
      const faviconUrl = new URL(chrome.runtime.getURL('/_favicon/'));
      faviconUrl.searchParams.set('pageUrl', url);
      faviconUrl.searchParams.set('size''16');
      return faviconUrl.toString();
    }

    「API 文档速查」

    https://developer.chrome.google.cn/docs/extensions/reference/api/sessions?hl=zh-cn#method-getRecentlyClosed

    https://developer.chrome.google.cn/docs/extensions/reference/api/sessions?hl=zh-cn#method-restore

    https://developer.chrome.google.cn/docs/extensions/reference/api/runtime?hl=zh-cn#method-getURL

    数据恢复流程

    用户点击恢复按钮
             ↓
    chrome.sessions.restore(sessionId)
             ↓
    Chrome 内部查找会话记录
             ↓
    创建新标签页并加载原URL
             ↓
    回调函数执行成功/失败处理
             ↓
    更新界面状态(移除已恢复项)

    立即尝试「5分钟搞定安装」:复制代码 → 创建文件 → 加载扩展 → 开始使用!

    🚀 「浏览项目的完整代码可以点击这里 https://github.com/Teernage/recently-closed-tabs,如果对你有帮助欢迎Star。」

    目录结构

    recently-closed-tabs
    ├─ manifest.json
    ├─ popup.html
    ├─ styles.css
    └─ popup.js

    完整代码「创建文件夹」recently-closed-tabs「创建manifest.json文件」这是扩展的「配置文件」,定义了扩展的基本信息、权限要求和行为规范。

    {
      "manifest_version": 3,
      "name": "最近关闭标签页管理器",
      "version": "1.0",
      "description": "查看和恢复最近关闭的标签页",
      "permissions": ["sessions", "tabs", "favicon"],
      "action": {
        "default_popup": "popup.html",
        "default_title": "最近关闭的标签页"
      }
    }

    关键点解读:

    字段

    说明

    manifest_version: 3

    使用最新的 Manifest V3 扩展规范,更安全、性能更好

    name

    插件在应用商店和工具栏中显示的名称

    version

    插件版本号,遵循语义化版本规范

    description

    插件的功能描述,在管理页面中显示

    permissions「申请的API权限」:

     • sessions - 访问浏览器会话数据,获取关闭的标签页 

    • tabs - 管理标签页,用于恢复关闭的页面 

    • favicon - 获取网站图标显示

    action

    工具栏图标的行为配置: • default_popup - 点击图标弹出的页面 

    • default_title - 鼠标悬停时显示的提示文字

    「创建popup.html文件」  (弹窗界面UI)

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="styles.css" />
      </head>
      <body>
        <div class="container">
          <div class="header">
            <div class="header-content">
              <div>
                <h1>最近关闭</h1>
                <div class="tab-count" id="tabCount">0 个标签页</div>
              </div>
              <button class="restore-all" id="restoreAllBtn">全部恢复</button>
            </div>
          </div>
          <div class="tab-list" id="tabList">
            <div class="loading"></div>
          </div>
        </div>
        <script src="popup.js"></script>
      </body>
    </html>

    「创建popup.js文件」  (弹窗界面交互)

    // DOM元素
    const elements = {
      tabList: document.getElementById('tabList'),
      restoreAllBtn: document.getElementById('restoreAllBtn'),
      tabCount: document.getElementById('tabCount'),
    };

    // 常量
    const CONFIG = {
      MAX_TABS: 25,
      MESSAGES: {
        EMPTY: '<div class="empty-message">没有找到最近关闭的标签页</div>',
        RESTORED: '<div class="empty-message">所有标签页已恢复</div>',
      },
    };

    /**
     * 初始化应用
     */
    function init() {
      loadRecentlyClosedTabs();
      elements.restoreAllBtn.addEventListener('click', restoreAllTabs);
    }

    /**
     * 加载并渲染最近关闭的标签页
     */
    function loadRecentlyClosedTabs() {
      chrome.sessions.getRecentlyClosed(
        { maxResults: CONFIG.MAX_TABS },
        (sessions) => {
          const tabs = sessions
            .filter((s) => s.tab)
            .map((s) => ({
              id: s.tab.sessionId,
              title: s.tab.title || '无标题',
              url: s.tab.url,
              closedTime: s.lastModified * 1000,
            }));

          updateTabCount(tabs.length);
          renderTabList(tabs);
        }
      );
    }

    /**
     * 更新标签计数
     */
    function updateTabCount(count) {
      if (elements.tabCount) {
        elements.tabCount.textContent = `${count} 个标签页`;
      }
    }

    /**
     * 渲染标签页列表
     */
    function renderTabList(tabs) {
      if (tabs.length === 0) {
        elements.tabList.innerHTML = CONFIG.MESSAGES.EMPTY;
        return;
      }

      const fragment = document.createDocumentFragment();
      tabs.forEach((tab) => fragment.appendChild(createTabElement(tab)));
      elements.tabList.innerHTML = '';
      elements.tabList.appendChild(fragment);
    }

    /**
     * 创建标签页元素
     */
    function createTabElement(tab) {
      const div = document.createElement('div');
      div.className = 'tab-item';
      div.dataset.sessionId = tab.id;
      div.innerHTML = `
        <img class="favicon" src="${getFaviconURL(tab.url)}" alt="">
        <div class="tab-info">
          <h3 class="tab-title">${escapeHTML(tab.title)}</h3>
          <p class="tab-url">${escapeHTML(tab.url)}</p>
        </div>
        <div class="closed-time">${formatTime(tab.closedTime)}</div>
      `;

      div.addEventListener('click', () => restoreTab(tab.id, div));
      return div;
    }

    /**
     * 恢复单个标签页
     */
    function restoreTab(sessionId, element) {
      // 添加加载状态
      element.style.opacity = '0.5';
      element.style.pointerEvents = 'none';

      chrome.sessions.restore(sessionId, (restored) => {
        if (chrome.runtime.lastError) {
          console.error('恢复失败:', chrome.runtime.lastError);
          // 恢复状态
          element.style.opacity = '1';
          element.style.pointerEvents = 'auto';
          return;
        }

        loadRecentlyClosedTabs();
      });
    }

    /**
     * 恢复所有标签页
     */
    function restoreAllTabs() {
      chrome.sessions.getRecentlyClosed(
        { maxResults: CONFIG.MAX_TABS },
        (sessions) => {
          const tabs = sessions.filter((s) => s.tab);
          if (tabs.length === 0) return;

          elements.restoreAllBtn.disabled = true;

          Promise.all(
            tabs.map(
              (s) =>
                new Promise((resolve) =>
                  chrome.sessions.restore(s.tab.sessionId, resolve)
                )
            )
          ).then(() => {
            elements.tabList.innerHTML = CONFIG.MESSAGES.RESTORED;
            elements.restoreAllBtn.disabled = false;
          });
        }
      );
    }

    /**
     * 转义HTML
     */
    function escapeHTML(str) {
      const map = {
        '&': '&',
        '<': '<',
        '>': '>',
        '"': '"',
        "'": ''',
      };
      return str.replace(/[&<>"']/g, (m) => map[m]);
    }

    /**
     * 获取网站图标URL
     */
    function getFaviconURL(url) {
      const faviconUrl = new URL(chrome.runtime.getURL('/_favicon/'));
      faviconUrl.searchParams.set('pageUrl', url);
      faviconUrl.searchParams.set('size''16');
      return faviconUrl.toString();
    }

    /**
     * 格式化时间
     */
    function formatTime(timestamp) {
      const diff = Date.now() - timestamp;
      const units = [
        [86400000, (d) => `${d}天前`],
        [3600000, (h) => `${h}小时前`],
        [60000, (m) => `${m}分钟前`],
      ];

      for (const [unit, format] of units) {
        const value = Math.floor(diff / unit);
        if (value > 0) return format(value);
      }

      return '刚刚';
    }

    // 启动应用
    document.addEventListener('DOMContentLoaded', init);

    「创建styles.css文件」  (弹窗界面样式)

          * {
            margin0;
            padding0;
            box-sizing: border-box;
          }

          @keyframes fadeIn {
            from {
              opacity0;
              transformtranslateY(10px);
            }

            to {
              opacity1;
              transformtranslateY(0);
            }
          }

          @keyframes float {

            0%,
            100% {
              transformtranslateY(0);
            }

            50% {
              transformtranslateY(-10px);
            }
          }

          html,
          body {
            width440px;
            height600px;
            overflow: hidden;
            font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display',
              'Segoe UI', Roboto, sans-serif;
            background#ffffff;
            color#000000;
          }

          .container {
            width100%;
            height100%;
            display: flex;
            flex-direction: column;
            overflow: hidden;
          }

          .header {
            flex-shrink0;
            backgroundrgba(2552552550.8);
            backdrop-filtersaturate(180%blur(20px);
            -webkit-backdrop-filtersaturate(180%blur(20px);
            padding20px 20px 16px;
            border-bottom0.5px solid rgba(0000.06);
          }

          .header-content {
            display: flex;
            justify-content: space-between;
            align-items: center;
          }

          h1 {
            font-size26px;
            font-weight600;
            letter-spacing: -0.5px;
            color#000000;
          }

          .tab-count {
            font-size13px;
            color#8e8e93;
            font-weight400;
            margin-top2px;
          }

          .restore-all {
            background#007aff;
            color: white;
            border: none;
            padding9px 18px;
            border-radius18px;
            cursor: pointer;
            font-size14px;
            font-weight500;
            transition: all 0.2s ease;
            box-shadow0 2px 8px rgba(01222550.3);
          }

          .restore-all:hover {
            background#0051d5;
            transformscale(1.02);
            box-shadow0 4px 12px rgba(01222550.4);
          }

          .restore-all:active {
            transformscale(0.98);
          }

          .tab-list {
            flex1;
            overflow-y: auto;
            overflow-x: hidden;
            background#ffffff;
            padding12px 16px;
          }

          .tab-list::-webkit-scrollbar {
            width6px;
          }

          .tab-list::-webkit-scrollbar-track {
            background: transparent;
          }

          .tab-list::-webkit-scrollbar-thumb {
            backgroundrgba(0000.15);
            border-radius3px;
          }

          .tab-list::-webkit-scrollbar-thumb:hover {
            backgroundrgba(0000.25);
          }

          .tab-item {
            display: flex;
            align-items: center;
            padding12px 14px;
            margin-bottom6px;
            background#f9f9f9;
            border-radius10px;
            cursor: pointer;
            transition: all 0.2s ease;
            animation: fadeIn 0.3s ease-out backwards;
          }

          .tab-item:nth-child(1) {
            animation-delay0.05s;
          }

          .tab-item:nth-child(2) {
            animation-delay0.1s;
          }

          .tab-item:nth-child(3) {
            animation-delay0.15s;
          }

          .tab-item:nth-child(4) {
            animation-delay0.2s;
          }

          .tab-item:nth-child(5) {
            animation-delay0.25s;
          }

          .tab-item:hover {
            background#f0f0f0;
            transformscale(1.005);
          }

          .tab-item:active {
            transformscale(0.995);
            background#e8e8e8;
          }

          .favicon {
            width24px;
            height24px;
            margin-right12px;
            border-radius6px;
            flex-shrink0;
            background: white;
            padding2px;
          }

          .tab-info {
            flex1;
            overflow: hidden;
            min-width0;
          }

          .tab-title {
            font-size14px;
            font-weight500;
            margin0 0 3px 0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            color#000000;
            letter-spacing: -0.2px;
            line-height1.3;
          }

          .tab-url {
            font-size12px;
            color#8e8e93;
            margin0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            font-weight400;
          }

          .closed-time {
            font-size12px;
            color#007aff;
            margin-left12px;
            padding5px 10px;
            backgroundrgba(01222550.1);
            border-radius8px;
            white-space: nowrap;
            font-weight500;
          }

          .empty-message {
            text-align: center;
            color#8e8e93;
            padding80px 20px;
            font-size15px;
            font-weight400;
            animation: fadeIn 0.5s ease-out;
          }

          .empty-message::before {
            content'📭';
            display: block;
            font-size64px;
            margin-bottom16px;
            animation: float 3s ease-in-out infinite;
          }

          .empty-message::after {
            content'没有最近关闭的标签页';
            display: block;
            margin-top8px;
            font-size14px;
            color#c7c7cc;
          }

          .loading {
            text-align: center;
            padding60px 20px;
          }

          .loading::before {
            content'';
            display: inline-block;
            width40px;
            height40px;
            border3px solid rgba(01222550.2);
            border-top-color#007aff;
            border-radius50%;
            animation: spin 0.8s linear infinite;
          }

          @keyframes spin {
            to {
              transformrotate(360deg);
            }
          }

    安装方式(开发者模式)

    打开 Chrome,访问 chrome://extensions/

    右上角开启“开发者模式”

    点击“加载已解压的扩展程序”

    选择刚刚创建的recently-closed-tabs文件夹进行加载

    在工具栏固定扩展图标,点击即可使用 如下图:

    「至此,我们的一键恢复插件就搞完了」

    使用说明打开扩展弹窗,即可看到最近关闭的标签页列表

    点击某一项恢复该标签页

    点击右上角“全部恢复”按钮,一次性恢复所有列表内的标签页

    若列表为空,会显示“没有最近关闭的标签页”

    常见问题看不到任何记录?

    需要近期确实关闭过标签页;浏览器重启后记录可能被系统回收

    想调整条目显示数量?

    修改 popup.js 中 CONFIG.MAX_TABS,最大25

    隐私与权限声明本扩展不采集任何用户数据

    数据来自浏览器内置 chrome.sessions API,仅在本地运行

    权限精简:sessionstabsfavicon

    ""~

    阅读原文

    跳转微信打开

    Fish AI Reader

    Fish AI Reader

    AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

    FishAI

    FishAI

    鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

    联系邮箱 441953276@qq.com

    相关标签

    Chrome扩展 标签页恢复 后悔药 效率工具 Chrome Extension Tab Recovery Regret Pill Productivity Tool 开源
    相关文章