🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

女朋友炸了:剛打開的網頁怎麼又沒了?我反手甩出一鍵恢復按鈕!

女朋友經常手滑關掉標籤頁這事兒頭大了?
跟女朋友說用 Ctrl/Cmd+Shift+T,她皺眉:“鍵盤上哪有這個鍵!!!”
讓她翻歷史紀錄,她搖頭:“根本找不到,全是我今天打開的!”
最後指向左上角的“最近關閉”,她嘆氣:“才8個,根本不夠用
行,那就不講道理,直接解決問題。
於是我寫了這個 Chrome 擴展—— “悔藥”
點一下就能看到“最近關閉的標籤頁”,想恢復單個點一下,想全恢復一鍵搞定。還有網站圖標、關閉時間、順滑的小動畫,裝上就能用。程式碼已經開源,想改介面隨便改。5分鐘搞定安裝:複製程式碼 → 創建文件 → 載入擴展 → 開始使用!

先看效果: 打開彈窗 → 點擊恢復單個 → 全部恢復

演示.gif

功能亮點

  • 自動列出最近關閉的標籤頁(最多 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 文檔速查

數據恢復流程

用戶點擊恢復按鈕
         ↓
chrome.sessions.restore(sessionId)
         ↓
Chrome 內部查找會話記錄
         ↓
創建新標籤頁並載入原URL
         ↓
回調函數執行成功/失敗處理
         ↓
更新介面狀態(移除已恢復項)

立即嘗試

5分鐘搞定安裝:複製程式碼 → 創建文件 → 載入擴展 → 開始使用!
🚀 瀏覽專案的完整程式碼可以點擊這裡 github.com/Teernage/re…,如果對你有幫助歡迎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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  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);
  }
}

安裝方式(開發者模式)

加載一鍵恢復插件.gif

  1. 打開 Chrome,訪問 chrome://extensions/
  2. 右上角開啟“開發者模式”
  3. 點擊“加載已解壓的擴展程序”

選擇剛創建的recently-closed-tabs文件夾進行加載

  1. 在工具欄固定擴展圖標,點擊即可使用 如下圖:

固定擴展並打開.gif

至此,我們的一鍵恢復插件就搞完了

使用說明

  • 打開擴展彈窗,即可看到最近關閉的標籤頁列表
  • 點擊某一項恢復該標籤頁
  • 點擊右上角“全部恢復”按鈕,一次性恢復所有列表內的標籤頁
  • 若列表為空,會顯示“沒有最近關閉的標籤頁”

常見問題

  • 看不到任何紀錄?
    • 需要近期確實關閉過標籤頁;瀏覽器重啟後紀錄可能被系統回收
  • 想調整條目顯示數量?
    • 修改 popup.jsCONFIG.MAX_TABS,最大25

隱私與權限聲明

  • 本擴展不採集任何用戶數據
  • 數據來自瀏覽器內置 chrome.sessions API,僅在本地運行
  • 權限精簡:sessionstabsfavicon

如果覺得對您有幫助,歡迎點贊 👍 收藏 ⭐ 关注 🔔 支持一下!
往期實戰推薦:


原文出處:https://juejin.cn/post/7561612065707065390


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝26   💬9   ❤️7
656
🥈
我愛JS
📝4   💬13   ❤️7
284
🥉
御魂
💬1  
4
#4
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付