浏览器 XHR Hook + IndexedDB:油猴脚本实现网络请求本地备份

在油猴脚本或浏览器控制台中,可以通过覆写 XMLHttpRequest.prototype 方法拦截页面发出的所有 XHR 请求,配合本地服务实现数据自动备份。

XHR Hook 核心实现

const originOpen = XMLHttpRequest.prototype.open;
const originSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this._method = method;
    this._url = url;
    return originOpen.apply(this, [method, url, ...rest]);
};

XMLHttpRequest.prototype.send = function (body) {
    this.addEventListener('readystatechange', function () {
        if (this.readyState === 4) {
            // 匹配目标接口
            if (/\/api\/data/.test(this._url)) {
                enqueueSave(`data_${Date.now()}.json`, this.responseText);
            }
        }
    });
    return originSend.apply(this, arguments);
};

readyState === 4 表示请求完成,this.responseText 是响应体。

Set 去重 + 队列节流

高频请求下需要防止重复保存和并发过多:

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

function enqueueSave(fileName, content) {
    if (savedSet.has(fileName)) return;  // 去重

    savedSet.add(fileName);
    queue.push({ fileName, content });

    // 1秒后批量发送
    if (!timer) {
        timer = setTimeout(flushQueue, 1000);
    }
}

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

保存到本地 HTTP 服务(带超时)

function saveToServer(fileName, content) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), 5000);

    return fetch('http://127.0.0.1:8099/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ fileName, content }),
        signal: controller.signal
    })
        .then(res => { clearTimeout(id); return res; })
        .catch(err => console.error('保存失败:', fileName, err));
}

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');
    tx.objectStore(STORE_NAME).put({
        id: fileName,
        content,
        timestamp: Date.now()
    });
}

enqueueSave 中同时写入:

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

    saveToIndexedDB(fileName, content);  // 本地先存一份

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

读取 IndexedDB 备份

function getAllBackups() {
    return new Promise((resolve) => {
        const tx = db.transaction(STORE_NAME, 'readonly');
        const req = tx.objectStore(STORE_NAME).getAll();
        req.onsuccess = () => resolve(req.result);
    });
}

getAllBackups().then(records => {
    records.forEach(r => console.log(r.id, r.timestamp));
});

IndexedDB 在同源页面刷新后仍然保留数据,是浏览器端容量最大的持久化存储方案(通常限额数百 MB 到 GB 级别)。