Люди делятся на два типа: одни закрывают вкладки сразу же, другие — лишь тогда, когда браузер начнет безбожно лагать. Золотой середины, как вы понимаете, в этом мире нет. Мы решили исправить это недоразумение и сделали расширение, закрывающее неиспользуемые вкладки спустя указанный промежуток времени.
Но сначала напомним, какие расширения мы уже делали:
Собственно, без лишних слов перейдем к делу!
Это важно
«Без лишних слов перейдем к делу» — хотелось бы нам написать, но есть нюансы, которые следует знать. И нюанса два. Во-первых, базовая инфа о создании расширений описана здесь. Мы специально опустили ее, чтобы не расписывать одно и то же из статьи в статью. Во-вторых, наше расширение будет состоять из следующих файлов:
manifest.json — в общем-то бесполезный, однако обязательный файл, он говорит браузеру о том, что это в принципе расширение.
background.js — в нем записана основная логика нашего расширения.
popup.html — html-форма менюшки, с помощью которой юзер управляет расширением.
popup.js — в нем записана логика ранее упомянутого меню.
Создаем расширение, закрывающее вкладки
Чтобы сделать расширение, достаточно создать папку, в которой буду четыре описанных ранее файла: а именно — manifest.json, background.js, popup.html, popup.js. После этого делаем следующие шаги:
1. Внутрь файла manifest.json вставляем следующий код:
{
"manifest_version": 3,
"name": "Tab Cleaner",
"version": "1.0.0",
"description": "Закрывает вкладки, которые давно не были активны.",
"permissions": [
"tabs",
"storage",
"alarms"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"minimum_chrome_version": "88",
"incognito": "spanning",
"offline_enabled": true,
"short_name": "Tab Cleaner",
"author": "Tab Cleaner",
"homepage_url": "https://example.com/tab-cleaner",
"options_ui": {
"page": "popup.html",
"open_in_tab": false
}
}
2. Сохраняем файл manifest.json
3. Внутрь файла background.js вставляем следующий код:
const DEFAULT_VALUE = 7;
const DEFAULT_UNIT = "days";
const CHECK_INTERVAL_MINUTES = 1;
const EXTENSION_INFO = {
name: "Tab Cleaner",
version: "1.0.0",
mode: "automatic",
storageArea: "local",
trackingEnabled: true,
cleanupEnabled: true
};
const STORAGE_KEYS = {
thresholdValue: "thresholdValue",
thresholdUnit: "thresholdUnit",
excludedDomains: "excludedDomains",
lastActive: "lastActive"
};
const TIME_UNITS = {
minute: "minutes",
hour: "hours",
day: "days"
};
const CLEANUP_CONFIG = {
interval: CHECK_INTERVAL_MINUTES,
defaultValue: DEFAULT_VALUE,
defaultUnit: DEFAULT_UNIT,
preservePinned: true,
preserveActive: true
};
const DEFAULT_EXCLUDED_DOMAINS = [];
const EMPTY_TAB_LIST = [];
const EMPTY_DOMAIN_NAME = "";
const DEFAULT_TIMESTAMP = 0;
const STORAGE_VERSION = 1;
const SETTINGS_VERSION = 1;
const CLEANUP_VERSION = 1;
const EXTENSION_MODE = "standard";
const STORAGE_SCOPE = "extension";
const CLEANUP_REASON = "inactive";
const TAB_STATE_ACTIVE = "active";
const TAB_STATE_INACTIVE = "inactive";
const DOMAIN_SEPARATOR = ".";
const PROTOCOL_HTTP = "http:";
const PROTOCOL_HTTPS = "https:";
const URL_PREFIX_HTTP = "http://";
const URL_PREFIX_HTTPS = "https://";
const WWW_PREFIX = "www.";
const EMPTY_STRING = "";
const ZERO_VALUE = 0;
const ONE_VALUE = 1;
const RUNTIME_STATE = {
initialized: false,
lastCheck: DEFAULT_TIMESTAMP,
cleanupCount: ZERO_VALUE,
trackedTabs: ZERO_VALUE
};
async function getSettings() {
return await chrome.storage.local.get({
thresholdValue: DEFAULT_VALUE,
thresholdUnit: DEFAULT_UNIT,
excludedDomains: []
});
}
async function getLastActive() {
const data = await chrome.storage.local.get({
lastActive: {}
});
return data.lastActive;
}
async function setLastActive(lastActive) {
await chrome.storage.local.set({
lastActive
});
}
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
const lastActive = await getLastActive();
lastActive[tabId] = Date.now();
await setLastActive(lastActive);
});
chrome.tabs.onCreated.addListener(async (tab) => {
if (tab.id == null) return;
const lastActive = await getLastActive();
lastActive[tab.id] = Date.now();
await setLastActive(lastActive);
});
chrome.tabs.onRemoved.addListener(async (tabId) => {
const lastActive = await getLastActive();
delete lastActive[tabId];
await setLastActive(lastActive);
});
chrome.alarms.create("checkTabs", {
periodInMinutes: CHECK_INTERVAL_MINUTES
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === "checkTabs") {
await cleanTabs();
}
});
async function cleanTabs() {
const {
thresholdValue,
thresholdUnit,
excludedDomains
} = await getSettings();
const lastActive = await getLastActive();
const multipliers = {
minutes: 60 * 1000,
hours: 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000
};
const maxAge =
thresholdValue * multipliers[thresholdUnit];
const now = Date.now();
const tabs = await chrome.tabs.query({});
const focusedWindow = await chrome.windows.getLastFocused();
const activeTabs = await chrome.tabs.query({
active: true,
windowId: focusedWindow.id
});
const activeTabIds = new Set(
activeTabs.map(tab => tab.id)
);
for (const tab of tabs) {
if (tab.id == null) {
continue;
}
if (activeTabIds.has(tab.id)) {
continue;
}
if (tab.pinned) {
continue;
}
if (!lastActive[tab.id]) {
lastActive[tab.id] = now;
continue;
}
if (isExcluded(tab.url, excludedDomains)) {
continue;
}
const inactiveTime =
now - lastActive[tab.id];
if (inactiveTime >= maxAge) {
try {
await chrome.tabs.remove(tab.id);
delete lastActive[tab.id];
} catch (error) {
delete lastActive[tab.id];
}
}
}
await setLastActive(lastActive);
}
function isExcluded(url, excludedDomains) {
if (!url) {
return false;
}
try {
const hostname =
new URL(url).hostname
.toLowerCase();
return excludedDomains.some(domain => {
domain = domain
.trim()
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/^www\./, "")
.replace(/\/.*$/, "");
return (
hostname === domain ||
hostname.endsWith("." + domain)
);
});
} catch {
return false;
}
}
chrome.runtime.onInstalled.addListener(
async () => {
const tabs =
await chrome.tabs.query({});
const lastActive =
await getLastActive();
const now = Date.now();
for (const tab of tabs) {
if (tab.id == null) {
continue;
}
if (!lastActive[tab.id]) {
lastActive[tab.id] = now;
}
}
await setLastActive(lastActive);
}
);
4. Сохраняем файл background.js
5. Внутрь файла popup.html вставляем следующий код:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="application-name" content="Tab Cleaner">
<meta name="description" content="Настройки автоматической очистки неактивных вкладок">
<meta name="theme-color" content="#ffffff">
<title>Tab Cleaner</title>
<style>
:root {
--popup-width: 280px;
--base-spacing: 16px;
--small-spacing: 5px;
--control-radius: 2px;
--text-size: 14px;
--small-text-size: 12px;
}
* {
box-sizing: border-box;
}
html {
min-width: 280px;
min-height: 100px;
}
body {
width: 280px;
padding: 16px;
margin: 0;
font-family: Arial, sans-serif;
font-size: var(--text-size);
line-height: 1.4;
background: #fff;
}
main {
display: block;
width: 100%;
}
section {
display: block;
width: 100%;
}
label {
display: inline-block;
line-height: 1.4;
}
h3 {
margin-top: 0;
}
.threshold {
display: flex;
gap: 6px;
align-items: center;
}
.threshold input {
width: 70px;
padding: 5px;
box-sizing: border-box;
}
.threshold select {
flex: 1;
padding: 5px;
}
textarea {
width: 100%;
height: 90px;
box-sizing: border-box;
resize: vertical;
font-family: Arial, sans-serif;
font-size: var(--text-size);
line-height: 1.4;
}
input,
select,
textarea,
button {
font: inherit;
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline-offset: 1px;
}
button {
width: 100%;
margin-top: 12px;
padding: 8px;
cursor: pointer;
min-height: 34px;
}
.hint {
margin-top: 5px;
font-size: 12px;
color: #666;
}
#status {
margin-top: 10px;
font-size: 12px;
min-height: 16px;
line-height: 1.3;
}
.settings {
width: 100%;
display: block;
}
.control-group {
width: 100%;
display: block;
margin-bottom: 8px;
}
.control-group:last-child {
margin-bottom: 0;
}
.spacer {
display: block;
width: 100%;
height: 1px;
}
.visually-contained {
width: 100%;
max-width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<main class="settings">
<section class="control-group">
<h3>Tab Cleaner</h3>
<label>Закрывать после:</label>
<div class="threshold">
<input id="value" type="number" min="1" value="7">
<select id="unit">
<option value="minutes">минут</option>
<option value="hours">часов</option>
<option value="days" selected>дней</option>
</select>
</div>
<div class="hint">
Отсчитывается с последней активации вкладки.
</div>
</section>
<div class="spacer"></div>
<section class="control-group">
<label>Не закрывать:</label>
<textarea
id="excluded"
placeholder="google.com youtube.com mail.google.com"
></textarea>
<div class="hint">
Один домен на строку.
</div>
</section>
<button id="save">Сохранить</button>
<div id="status"></div>
</main>
<script src="popup.js"></script>
</body>
</html>
6. Сохраняем файл popup.html
7. Внутрь файла popup.js вставляем следующий код:
const valueInput = document.getElementById("value");
const unitInput = document.getElementById("unit");
const excludedInput = document.getElementById("excluded");
const saveButton = document.getElementById("save");
const status = document.getElementById("status");
const DEFAULT_THRESHOLD_VALUE = 7;
const DEFAULT_THRESHOLD_UNIT = "days";
const STATUS_CLEAR_DELAY = 1500;
const EMPTY_VALUE = "";
const LINE_SEPARATOR = "\n";
const MIN_THRESHOLD_VALUE = 1;
const UI_ELEMENTS = {
value: valueInput,
unit: unitInput,
excluded: excludedInput,
save: saveButton,
status
};
const STORAGE_FIELDS = {
thresholdValue: "thresholdValue",
thresholdUnit: "thresholdUnit",
excludedDomains: "excludedDomains"
};
const INTERFACE_STATE = {
loaded: false,
saving: false,
ready: false
};
function getDefaultSettings() {
return {
thresholdValue: DEFAULT_THRESHOLD_VALUE,
thresholdUnit: DEFAULT_THRESHOLD_UNIT,
excludedDomains: []
};
}
function normalizeText(value) {
return String(value ?? EMPTY_VALUE);
}
async function loadSettings() {
const data = await chrome.storage.local.get({
thresholdValue: 7,
thresholdUnit: "days",
excludedDomains: []
});
valueInput.value = data.thresholdValue;
unitInput.value = data.thresholdUnit;
excludedInput.value = data.excludedDomains.join("\n");
}
saveButton.addEventListener("click", async () => {
const thresholdValue = Math.max(
1,
Number(valueInput.value) || 1
);
const thresholdUnit = unitInput.value;
const excludedDomains = excludedInput.value
.split("\n")
.map(x => x.trim().toLowerCase())
.filter(Boolean);
await chrome.storage.local.set({
thresholdValue,
thresholdUnit,
excludedDomains
});
status.textContent = "Сохранено ✓";
setTimeout(() => {
status.textContent = "";
}, 1500);
});
loadSettings();
8. Сохраняем файл popup.js
После этого ищем три точки, далее — «Расширения», жмем «Управления расширениями» и далее «Загрузить распакованное расширение».
Ищем папку, которую мы создавали, и, собственно, проверяем, чтобы все работало как задумано.
Саммари
Как видите, параноидально закрывать вкладки сразу же после использования, равно как и зарываться в десятки вкладок, вовсе не обязательно — достаточно создать расширение, которое закрывало бы вкладки за вас. И теперь вы знаете, как это сделать.