女朋友炸了:刚打开的网页怎么又没了?我反手甩出一键恢复按钮!
❝女朋友经常手滑关掉标签页这事儿头大了?
跟女朋友说用
Ctrl/Cmd+Shift+T,她皱眉:“键盘上哪有这个键!!!”让她翻历史记录,她摇头:“根本找不到,全是我今天打开的!”
最后指向左上角的“最近关闭”,她叹气:“才8个,根本不够用
行,那就不讲道理,直接解决问题。
于是我写了这个 Chrome 扩展—— 「“后悔药”」
点一下就能看到“最近关闭的标签页”,想恢复单个点一下,想全恢复一键搞定。还有网站图标、关闭时间、顺滑的小动画,装上就能用。代码已经开源,想改界面随便改。「5分钟搞定安装」:复制代码 → 创建文件 → 加载扩展 → 开始使用!
❞
先看效果: 打开弹窗 → 点击恢复单个 → 全部恢复
功能亮点
自动列出最近关闭的标签页(最多 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 | |
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);}}
安装方式(开发者模式)
打开 Chrome,访问 chrome://extensions/
右上角开启“开发者模式”
点击“加载已解压的扩展程序”
选择刚刚创建的recently-closed-tabs文件夹进行加载
在工具栏固定扩展图标,点击即可使用 如下图:
「至此,我们的一键恢复插件就搞完了」
使用说明
打开扩展弹窗,即可看到最近关闭的标签页列表 点击某一项恢复该标签页 点击右上角“全部恢复”按钮,一次性恢复所有列表内的标签页 若列表为空,会显示“没有最近关闭的标签页”
常见问题
看不到任何记录?
需要近期确实关闭过标签页;浏览器重启后记录可能被系统回收 想调整条目显示数量?
修改 popup.js中CONFIG.MAX_TABS,最大25
隐私与权限声明
本扩展不采集任何用户数据 数据来自浏览器内置 chrome.sessionsAPI,仅在本地运行权限精简: sessions、tabs、favicon点击"阅读原文"了解详情~