油猴脚本加 IndexedDB 本地备份:掉线也不丢数据

在油猴脚本里做数据备份,通常只往本地服务器 POST 一份。但服务器挂掉或网络问题时,这批数据就丢了。IndexedDB 可以作为本地兜底——不管服务器是否正常,先把数据存本地一份。

IndexedDB 基础封装

const DB_NAME = "localBackupDB";
const STORE_NAME = "backups";
let db;

function initDB() {
    return new Promise((resolve, reject) => {
        const req = indexedDB.open(DB_NAME, 1);

        req.onupgradeneeded = (e) => {
            const d = e.target.result;
            if (!d.objectStoreNames.contains(STORE_NAME)) {
                d.createObjectStore(STORE_NAME, { keyPath: "id" });
            }
        };

        req.onsuccess = (e) => {
            db = e.target.result;
            resolve(db);
        };

        req.onerror = (e) => reject(e);
    });
}

function saveToIndexedDB(fileName, content) {
    if (!db) return;
    const tx = db.transaction(STORE_NAME, "readwrite");
    const store = tx.objectStore(STORE_NAME);
    store.put({ id: fileName, content, timestamp: Date.now() });
}

function getAllBackups() {
    return new Promise((resolve, reject) => {
        if (!db) return resolve([]);
        const tx = db.transaction(STORE_NAME, "readonly");
        const req = tx.objectStore(STORE_NAME).getAll();
        req.onsuccess = () => resolve(req.result);
        req.onerror = (e) => reject(e);
    });
}

集成到备份队列

已有服务端备份逻辑时,在 enqueueSave 里同时存 IndexedDB:

const savedSet = new Set();
const queue = [];
let timer = null;

function enqueueSave(fileName, content) {
    if (savedSet.has(fileName)) return;
    savedSet.add(fileName);

    // 本地 IndexedDB 兜底(不管服务端是否正常)
    saveToIndexedDB(fileName, content);

    queue.push({ fileName, content });
    if (!timer) timer = setTimeout(flushQueue, 1000);
}

function flushQueue() {
    const batch = [...queue];
    queue.length = 0;
    timer = null;
    batch.forEach(({ fileName, content }) => saveToServer(fileName, content));
}

这样即使 saveToServer 失败,IndexedDB 里还有一份。

可视化面板

挂到 window.showPanel,在控制台调用即可弹出历史备份列表:

function showPanel() {
    if (document.getElementById("__backup_panel")) return;

    const panel = document.createElement("div");
    panel.id = "__backup_panel";
    Object.assign(panel.style, {
        position: "fixed", top: "20px", right: "20px",
        width: "320px", maxHeight: "420px", overflowY: "auto",
        background: "#fff", border: "1px solid #ccc",
        boxShadow: "0 0 10px rgba(0,0,0,.3)",
        padding: "12px", zIndex: 99999,
        fontSize: "12px", fontFamily: "monospace"
    });

    panel.innerHTML = `<div style="font-weight:bold;margin-bottom:8px">📦 历史备份</div>
<div id="__backup_list"></div>
<button id="__backup_close" style="margin-top:10px">关闭</button>`;

    document.body.appendChild(panel);
    document.getElementById("__backup_close").onclick = () => panel.remove();

    getAllBackups().then(backups => {
        const list = document.getElementById("__backup_list");
        backups.sort((a, b) => b.timestamp - a.timestamp);

        backups.forEach(item => {
            const row = document.createElement("div");
            row.style.cssText = "display:flex;justify-content:space-between;margin-bottom:4px";

            const name = document.createElement("span");
            name.textContent = item.id;
            name.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";

            const btn = document.createElement("button");
            btn.textContent = "恢复";
            btn.onclick = () => {
                try {
                    console.log("🔄 恢复备份:", item.id);
                    console.log(JSON.parse(item.content));
                } catch {
                    console.log(item.content);
                }
            };

            row.appendChild(name);
            row.appendChild(btn);
            list.appendChild(row);
        });
    });
}

window.showPanel = showPanel;

使用时在浏览器控制台输入 showPanel() 即可查看。

初始化调用

initDB()
    .then(() => console.log("IndexedDB 已就绪"))
    .catch(console.error);

注意事项

  • IndexedDB 存储受同源策略限制,每个域名独立,跨域无法访问
  • 浏览器私有模式下 IndexedDB 数据在关闭标签页后会清除
  • 存储上限一般是磁盘空间的 50% 左右(浏览器决定),备份大量 JSON 不用担心空间
  • store.put()(不是 add()),同 key 会覆盖而不报错
  • 可以在 DevTools → Application → IndexedDB 直接查看和清理数据