女朋友經常手滑關掉標籤頁這事兒頭大了?
跟女朋友說用 Ctrl/Cmd+Shift+T
,她皺眉:“鍵盤上哪有這個鍵!!!”
讓她翻歷史紀錄,她搖頭:“根本找不到,全是我今天打開的!”
最後指向左上角的“最近關閉”,她嘆氣:“才8個,根本不夠用
行,那就不講道理,直接解決問題。
於是我寫了這個 Chrome 擴展—— “悔藥”
點一下就能看到“最近關閉的標籤頁”,想恢復單個點一下,想全恢復一鍵搞定。還有網站圖標、關閉時間、順滑的小動畫,裝上就能用。程式碼已經開源,想改介面隨便改。5分鐘搞定安裝:複製程式碼 → 創建文件 → 載入擴展 → 開始使用!
本擴展主要依賴以下 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
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);
}
}
選擇剛創建的recently-closed-tabs文件夾進行加載
至此,我們的一鍵恢復插件就搞完了
popup.js
中 CONFIG.MAX_TABS
,最大25chrome.sessions
API,僅在本地運行sessions
、tabs
、favicon
如果覺得對您有幫助,歡迎點贊 👍 收藏 ⭐ 关注 🔔 支持一下!
往期實戰推薦: