在油猴脚本或浏览器控制台中,可以通过覆写 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 级别)。
