稀土掘金技术社区

女朋友炸了:刚打开的网页怎么又没了?我反手甩出一键恢复按钮!

❝

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

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

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

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

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

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

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

❞

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

Image

功能亮点

  • 自动列出最近关闭的标签页(最多 25 个)
  • 支持单个恢复、全部恢复
  • 显示网站图标、标题、URL、关闭时间
  • 平滑动效与简洁 UI
  • 零后台进程,权限最小化(仅用 sessions、tabs、favicon)

核心技术实现

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


// 获取最近关闭的标签页列表chrome.sessions.getRecentlyClosed({ maxResults: 25 }, (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>    <metacharset="utf-8" />    <linkrel="stylesheet"href="styles.css" />  </head>  <body>    <divclass="container">      <divclass="header">        <divclass="header-content">          <div>            <h1>最近关闭</h1>            <divclass="tab-count"id="tabCount">0 个标签页</div>          </div>          <buttonclass="restore-all"id="restoreAllBtn">全部恢复</button>        </div>      </div>      <divclass="tab-list"id="tabList">        <divclass="loading"></div>      </div>    </div>    <scriptsrc="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文件」  (弹窗界面样式)

      * {        margin: 0;        padding: 0;        box-sizing: border-box;      }
      @keyframes fadeIn {        from {          opacity: 0;          transform: translateY(10px);        }
        to {          opacity: 1;          transform: translateY(0);        }      }
      @keyframes float {
        0%,        100% {          transform: translateY(0);        }
        50% {          transform: translateY(-10px);        }      }
      html,      body {        width: 440px;        height: 600px;        overflow: hidden;        font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display',          'Segoe UI', Roboto, sans-serif;        background: #ffffff;        color: #000000;      }
      .container {        width: 100%;        height: 100%;        display: flex;        flex-direction: column;        overflow: hidden;      }
      .header {        flex-shrink: 0;        background: rgba(255, 255, 255, 0.8);        backdrop-filter: saturate(180%) blur(20px);        -webkit-backdrop-filter: saturate(180%) blur(20px);        padding: 20px 20px 16px;        border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);      }
      .header-content {        display: flex;        justify-content: space-between;        align-items: center;      }
      h1 {        font-size: 26px;        font-weight: 600;        letter-spacing: -0.5px;        color: #000000;      }
      .tab-count {        font-size: 13px;        color: #8e8e93;        font-weight: 400;        margin-top: 2px;      }
      .restore-all {        background: #007aff;        color: white;        border: none;        padding: 9px 18px;        border-radius: 18px;        cursor: pointer;        font-size: 14px;        font-weight: 500;        transition: all 0.2s ease;        box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);      }
      .restore-all:hover {        background: #0051d5;        transform: scale(1.02);        box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);      }
      .restore-all:active {        transform: scale(0.98);      }
      .tab-list {        flex: 1;        overflow-y: auto;        overflow-x: hidden;        background: #ffffff;        padding: 12px 16px;      }
      .tab-list::-webkit-scrollbar {        width: 6px;      }
      .tab-list::-webkit-scrollbar-track {        background: transparent;      }
      .tab-list::-webkit-scrollbar-thumb {        background: rgba(0, 0, 0, 0.15);        border-radius: 3px;      }
      .tab-list::-webkit-scrollbar-thumb:hover {        background: rgba(0, 0, 0, 0.25);      }
      .tab-item {        display: flex;        align-items: center;        padding: 12px 14px;        margin-bottom: 6px;        background: #f9f9f9;        border-radius: 10px;        cursor: pointer;        transition: all 0.2s ease;        animation: fadeIn 0.3s ease-out backwards;      }
      .tab-item:nth-child(1) {        animation-delay: 0.05s;      }
      .tab-item:nth-child(2) {        animation-delay: 0.1s;      }
      .tab-item:nth-child(3) {        animation-delay: 0.15s;      }
      .tab-item:nth-child(4) {        animation-delay: 0.2s;      }
      .tab-item:nth-child(5) {        animation-delay: 0.25s;      }
      .tab-item:hover {        background: #f0f0f0;        transform: scale(1.005);      }
      .tab-item:active {        transform: scale(0.995);        background: #e8e8e8;      }
      .favicon {        width: 24px;        height: 24px;        margin-right: 12px;        border-radius: 6px;        flex-shrink: 0;        background: white;        padding: 2px;      }
      .tab-info {        flex: 1;        overflow: hidden;        min-width: 0;      }
      .tab-title {        font-size: 14px;        font-weight: 500;        margin: 0 0 3px 0;        white-space: nowrap;        overflow: hidden;        text-overflow: ellipsis;        color: #000000;        letter-spacing: -0.2px;        line-height: 1.3;      }
      .tab-url {        font-size: 12px;        color: #8e8e93;        margin: 0;        white-space: nowrap;        overflow: hidden;        text-overflow: ellipsis;        font-weight: 400;      }
      .closed-time {        font-size: 12px;        color: #007aff;        margin-left: 12px;        padding: 5px 10px;        background: rgba(0, 122, 255, 0.1);        border-radius: 8px;        white-space: nowrap;        font-weight: 500;      }
      .empty-message {        text-align: center;        color: #8e8e93;        padding: 80px 20px;        font-size: 15px;        font-weight: 400;        animation: fadeIn 0.5s ease-out;      }
      .empty-message::before {        content: '📭';        display: block;        font-size: 64px;        margin-bottom: 16px;        animation: float 3s ease-in-out infinite;      }
      .empty-message::after {        content: '没有最近关闭的标签页';        display: block;        margin-top: 8px;        font-size: 14px;        color: #c7c7cc;      }
      .loading {        text-align: center;        padding: 60px 20px;      }
      .loading::before {        content: '';        display: inline-block;        width: 40px;        height: 40px;        border: 3px solid rgba(0, 122, 255, 0.2);        border-top-color: #007aff;        border-radius: 50%;        animation: spin 0.8s linear infinite;      }
      @keyframes spin {        to {          transform: rotate(360deg);        }      }

安装方式(开发者模式)

Image
  1. 打开 Chrome,访问 chrome://extensions/

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

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

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

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

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

使用说明

  • 打开扩展弹窗,即可看到最近关闭的标签页列表
  • 点击某一项恢复该标签页
  • 点击右上角“全部恢复”按钮,一次性恢复所有列表内的标签页
  • 若列表为空,会显示“没有最近关闭的标签页”

常见问题

  • 看不到任何记录?

    • 需要近期确实关闭过标签页;浏览器重启后记录可能被系统回收
  • 想调整条目显示数量?

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

隐私与权限声明

  • 本扩展不采集任何用户数据
  • 数据来自浏览器内置 chrome.sessions API,仅在本地运行
  • 权限精简:sessions、tabs、favicon
    图片
    点击"阅读原文"了解详情~