跳到主要内容前端日志本地持久化方案 | 极客日志JavaScript大前端
前端日志本地持久化方案
前端日志在浏览器或 WebView 内的本地持久化方案。主要包含四种方式:推荐使用 IndexedDB 存储大容量结构化日志;localStorage 适用于轻量级场景;Web Worker 结合 IndexedDB 可避免高频日志阻塞主线程;UI 面板便于实时查看与导出。文章详细对比了各方案的容量、复杂度及适用场景,提供了完整的代码实现示例,包括 LocalLogger、SimpleLocalLogger、Worker 模式及跨平台降级方案。同时涵盖了兼容性检查、配额检测及最佳实践建议,确保在不上传后端的情况下实现日志留存与导出。
追风少年14K 浏览 前端日志本地持久化方案
本文针对「不传后端、仅在浏览器或 WebView 内保存并支持导出」的前端/SDK 日志需求,整理多种本地持久化方案(IndexedDB、localStorage、Web Worker、UI 面板)、兼容性、容量与平台限制,以及业界常见做法与最佳实践。适合 Web SDK、WebView 内嵌页、前端监控等场景的开发者。
一、需求与场景
典型需求:
- 不把日志传到后端,只在浏览器或 WebView 内保存。
- 支持导出(如点击按钮下载为 JSON/TXT)或通过浏览器/WebView 提供的入口查看、保存。
当前仅用 console 输出时,浏览器保存 console 日志不便,希望落到本地存储并可持续累积、可导出。适用场景:Web SDK 在浏览器或移动端 WebView 中运行,需要本地留痕、问题复现与排查。
二、方案一:IndexedDB(推荐)
IndexedDB 为浏览器内置的 NoSQL 数据库,容量大(通常 50MB+)、异步、支持结构化数据,适合作为日志主存储。
2.1 特点
| 特点 | 说明 |
|---|
| 容量 | 通常 50MB+,部分浏览器可申请更多 |
| 异步 | 不阻塞主线程 |
| 结构化 | Object Store + 索引,可按时间、级别等查询 |
| 导出 | 读取全部或按条件查询后,用 Blob + <a download> 触发下载 |
2.2 核心能力示例
- 初始化:
indexedDB.open(dbName, version),在 onupgradeneeded 中创建 Object Store(如 logs)及索引(如 timestamp、level)。
- 写入:每条日志为一条记录(含 timestamp、level、message、data、url、userAgent 等),通过
transaction + objectStore.add() 写入。
- 数量控制:维护最大条数(如 10000 条),超出时按
timestamp 索引删除最旧记录(trimLogs)。
- 导出:
getAll() 或按索引查询后,JSON.stringify 为 JSON,或拼接为 TXT,用 Blob + URL.createObjectURL + <a download> 触发下载。
- 清空:
objectStore.clear()。
日志写入时仍可同时 console[level](...) 便于开发调试。
2.3 代码实现
三、方案二:localStorage + 文件下载
localStorage 为同步、键值存储,容量约 5–10MB,实现简单,适合轻量、日志量不大的场景。
3.1 特点
| 特点 | 说明 |
|---|
| 容量 | 约 5–10MB,需控制总体大小 |
| 同步 | 读写同步,大量写可能卡顿 |
| 实现 | 单 key 存 JSON 数组,每次追加后 setItem;超大小则从头部删旧条 |
3.2 核心逻辑
- 单 key(如
sdk_logs)存储一个 JSON 数组,每行一条日志(timestamp、level、message、data)。
- 每次
log 时 getItem → JSON.parse → push → 若 JSON.stringify(logs).length > maxSize 则 shift() 再 setItem。
- 导出:
getItem 解析后,转为 JSON 或 CSV,同样用 Blob + <a download> 下载。
- 清空:
removeItem(key)。
3.3 代码实现
完整 SimpleLocalLogger 类见 附录 A.2。
四、方案三:Web Worker + IndexedDB
将 IndexedDB 的读写放到 Web Worker,避免高频日志时阻塞主线程,适合高频打点场景。
4.1 思路
- 主线程通过
postMessage 向 Worker 发送:init、log、export、clear 等。
- Worker 内打开 IndexedDB,在 Worker 中执行
add、getAll、clear;导出时把结果通过 postMessage 回传主线程,主线程再触发下载。
- 主线程的
log(level, message, data) 仅做 worker.postMessage({ type: 'log', payload: { level, message, data } }) 和可选 console 输出。
4.2 注意点
- Worker 内无法直接操作 DOM,下载需在主线程用 Blob/URL/createElement('a') 完成。
- 需处理 Worker 未就绪时的队列或降级(如先压入内存队列,init 后再写入 IndexedDB)。
4.3 代码实现
Worker 文件 logger-worker.js 及主线程使用示例见 附录 A.3。
五、方案四:集成到页面 UI
在页面内提供日志面板 + 导出按钮,便于测试与用户反馈问题时一键导出。
5.1 做法
- 固定位置(如底部或侧边)放一个可折叠的日志面板,内部用列表展示最近 N 条(如 100 条),按 level 着色(error/warn/info/debug)。
- 面板顶部按钮:导出 JSON、导出 TXT、清空、关闭;底部或角落放「查看日志」按钮切换面板显示。
- 日志写入仍走 IndexedDB(或 localStorage),每次写入后刷新面板内容(如
getAllLogs().then(logs => { ... slice(-100) ... })),实现「持久化 + 实时预览 + 导出」。
5.2 与 SDK 结合
- 若 SDK 注入到宿主页,面板 DOM 与样式需与宿主隔离(如 iframe 或 Shadow DOM),避免污染宿主样式。
- WebView 内可由原生提供「保存到设备」接口,前端只负责生成 JSON/TXT 并调用该接口(如
window.AndroidBridge.saveLogFile(content))。
5.3 代码实现
日志面板 HTML、UILogger 类及 WebView 原生桥导出见 附录 A.4。
六、业界常见做法
| 产品/类型 | 常见方案 | 特点 |
|---|
| Sentry | 内存 + 可选上报 | 优先内存,崩溃时尝试落盘或上报 |
| Bugsnag | IndexedDB + SessionStorage | 多级存储,自动清理策略 |
| ARMS 等 | localStorage + 上报队列 | 离线缓存,联网后上报 |
| APM 类 | IndexedDB | 大数据量、结构化、按条件查询 |
共同点:本地先存、按策略裁剪、支持导出或上报;不传后端时仅做「本地存储 + 导出」即可。
七、兼容性与限制
7.1 平台兼容性(localStorage / IndexedDB)
两者均为 W3C 标准 API,在以下环境中普遍支持:
| 平台/环境 | localStorage | IndexedDB |
|---|
| Chromium PC(Chrome/Edge) | 支持 | 支持 |
| Android Chrome | 支持 | 支持 |
| Android WebView | 支持 | 支持 |
| iOS Safari | 支持 | 支持 |
| iOS WebView(WKWebView) | 支持 | 支持 |
7.2 容量与行为限制
| 项目 | 说明 |
|---|
| localStorage | 约 5–10MB(因浏览器而异),超限会抛错,需做 try/catch 与裁剪 |
| IndexedDB | 通常 50MB+,部分浏览器支持 navigator.storage.estimate() 查询配额与用量 |
| iOS | 在存储紧张、用户清除站点数据、隐私模式等情况下可能清除或限制存储;建议重要日志支持「导出后带走」 |
| 老版本 Android WebView | 个别版本对 IndexedDB 支持不完整,可做能力检测并降级到 localStorage 或使用 localForage |
7.3 配额检测示例
async function checkStorageQuota() {
if (navigator.storage?.estimate) {
const { usage, quota } = await navigator.storage.estimate();
console.log('已用:', usage, '配额:', quota);
return { usage, quota };
}
return null;
}
八、跨平台与稳健方案
8.1 能力检测与降级
- 先检测
typeof indexedDB !== 'undefined',可用则用 IndexedDB;不可用则降级为 localStorage(单 key 存 JSON 数组,控制大小)。
- 导出与清空接口统一,内部根据当前使用的存储实现不同逻辑。
8.2 代码实现
CrossPlatformLogger(能力检测 + 降级)见 附录 A.5;RobustLogger(localForage)见 附录 A.6。
localForage 在底层自动选择 IndexedDB → WebSQL → localStorage,接口为 Promise,兼容性由库处理。
8.3 最佳实践建议
| 建议 | 说明 |
|---|
| 生产 | 使用 IndexedDB(或 localForage),设置最大条数/时间范围,避免占满存储 |
| 开发 | 同时输出到 console 与本地存储,便于即时查看与事后导出 |
| WebView | 若有原生桥,可提供「保存日志到设备」接口,由前端组好 JSON/TXT 后调用 |
| 隐私 | 敏感信息脱敏后再写入本地 |
| 监控 | 可定期用 navigator.storage.estimate() 查看使用量,必要时清理或提示用户导出 |
小结与速查
代码实现索引
| 方案 | 附录 | 类/文件 | 说明 |
|---|
| IndexedDB | A.1 | LocalLogger | 推荐主方案,含 init/log/trimLogs/getAllLogs/exportToJSON/exportToTXT/clearLogs |
| localStorage | A.2 | SimpleLocalLogger | 轻量方案,含 log/getLogs/exportToJSON/exportToCSV/clear |
| Worker + IDB | A.3 | logger-worker.js + 主线程示例 | Worker 内 init/addLog/getAllLogs/clearLogs,主线程 postMessage + 导出下载 |
| UI 面板 | A.4 | HTML 面板 + UILogger | 继承 LocalLogger,updateUI/getLevelColor;可选 AndroidBridge 导出 |
| 跨平台降级 | A.5 | CrossPlatformLogger | 检测 IndexedDB,不可用则用 localStorage,统一 exportToJSON/clearLogs |
| localForage | A.6 | RobustLogger | 依赖 localforage,自动选 IndexedDB/WebSQL/localStorage |
方案对比
| 方案 | 容量 | 复杂度 | 适用场景 |
|---|
| IndexedDB | 大(50MB+) | 中 | 推荐,大量结构化日志、需按条件查 |
| localStorage | 小(约 5–10MB) | 低 | 轻量、条数少、实现简单 |
| Worker + IndexedDB | 同 IndexedDB | 较高 | 高频打点、不阻塞主线程 |
| localForage | 同底层 | 低 | 希望自动降级、接口统一 |
要点归纳
- 不传后端、仅本地:用浏览器存储(IndexedDB/localStorage)持久化,用 Blob +
<a download> 或原生桥导出。
- 推荐主方案:IndexedDB + 条数/大小限制 + 导出 JSON/TXT;兼容性要求高时可用 localForage。
- 兼容性:PC Chromium、Android Chrome/WebView、iOS Safari/WebView 均支持;注意 iOS 清理策略与老版本 WebView 降级。
附录:完整代码实现
A.1 LocalLogger(IndexedDB)
class LocalLogger {
constructor(dbName = 'SDKLogs', storeName = 'logs') {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
this.maxLogs = 10000;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('level', 'level', { unique: false });
}
};
});
}
async log(level, message, data = {}) {
const logEntry = {
timestamp: Date.now(),
level,
message,
data,
url: typeof window !== 'undefined' ? window.location.href : '',
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : ''
};
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise((resolve, reject) => {
const request = store.add(logEntry);
request.onsuccess = resolve;
request.onerror = () => reject(request.error);
});
await this.trimLogs();
console[level](`[${level.toUpperCase()}] ${message}`, data);
}
async trimLogs() {
const countRequest = this.db.transaction([this.storeName], 'readonly').objectStore(this.storeName).count();
const count = await new Promise((resolve) => {
countRequest.onsuccess = () => resolve(countRequest.result);
});
if (count <= this.maxLogs) return;
const deleteCount = count - this.maxLogs;
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const index = store.index('timestamp');
const range = IDBKeyRange.lowerBound(0);
let deleted = 0;
return new Promise((resolve) => {
index.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && deleted < deleteCount) {
store.delete(cursor.primaryKey);
deleted++;
cursor.continue();
} else {
resolve();
}
};
});
}
async getAllLogs() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async exportToJSON(filename = `sdk-logs-${Date.now()}.json`) {
const logs = await this.getAllLogs();
this.downloadFile(JSON.stringify(logs, null, 2), filename, 'application/json');
}
async exportToTXT(filename = `sdk-logs-${Date.now()}.txt`) {
const logs = await this.getAllLogs();
const text = logs.map(log => `[${new Date(log.timestamp).toISOString()}] [${log.level.toUpperCase()}] ${log.message}${JSON.stringify(log.data)}`).join('\n');
this.downloadFile(text, filename, 'text/plain');
}
downloadFile(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async clearLogs() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.clear();
request.onsuccess = resolve;
request.onerror = () => reject(request.error);
});
}
}
A.2 SimpleLocalLogger(localStorage)
class SimpleLocalLogger {
constructor(key = 'sdk_logs') {
this.key = key;
this.maxSize = 4 * 1024 * 1024;
}
log(level, message, data = {}) {
const logs = this.getLogs();
logs.push({ timestamp: Date.now(), level, message, data });
while (JSON.stringify(logs).length > this.maxSize && logs.length > 0) logs.shift();
localStorage.setItem(this.key, JSON.stringify(logs));
console[level](`[${level.toUpperCase()}] ${message}`, data);
}
getLogs() {
try {
return JSON.parse(localStorage.getItem(this.key)) || [];
} catch {
return [];
}
}
exportToJSON() {
const blob = new Blob([JSON.stringify(this.getLogs(), null, 2)], { type: 'application/json' });
this.downloadBlob(blob, `sdk-logs-${Date.now()}.json`);
}
exportToCSV() {
const logs = this.getLogs();
const headers = ['Timestamp', 'Level', 'Message', 'Data'];
const rows = logs.map(log => [
new Date(log.timestamp).toISOString(),
log.level,
`"${(log.message || '').replace(/"/g, '""')}"`,
`"${JSON.stringify(log.data || {}).replace(/"/g, '""')}"`
]);
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
this.downloadBlob(new Blob([csv], { type: 'text/csv' }), `sdk-logs-${Date.now()}.csv`);
}
downloadBlob(blob, filename) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
clear() {
localStorage.removeItem(this.key);
}
}
A.3 Worker + IndexedDB
let db = null;
self.onmessage = async (e) => {
const { type, payload } = e.data;
switch (type) {
case 'init':
await initDB(payload?.dbName || 'SDKLogs');
self.postMessage({ type: 'initialized' });
break;
case 'log':
await addLog(payload?.level, payload?.message, payload?.data ?? {});
break;
case 'export':
const logs = await getAllLogs();
self.postMessage({ type: 'exported', logs });
break;
case 'clear':
await clearLogs();
self.postMessage({ type: 'cleared' });
break;
}
};
function initDB(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
db = request.result;
resolve();
};
request.onupgradeneeded = (e) => {
if (!e.target.result.objectStoreNames.contains('logs')) {
e.target.result.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
}
};
});
}
function addLog(level, message, data) {
return new Promise((resolve, reject) => {
const tx = db.transaction(['logs'], 'readwrite');
tx.objectStore('logs').add({ timestamp: Date.now(), level, message, data });
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
function getAllLogs() {
return new Promise((resolve, reject) => {
const request = db.transaction(['logs'], 'readonly').objectStore('logs').getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function clearLogs() {
return new Promise((resolve, reject) => {
const request = db.transaction(['logs'], 'readwrite').objectStore('logs').clear();
request.onsuccess = resolve;
request.onerror = () => reject(request.error);
});
}
const worker = new Worker('logger-worker.js');
worker.postMessage({ type: 'init', payload: { dbName: 'SDKLogs' } });
worker.onmessage = (e) => {
if (e.data.type === 'exported') {
const blob = new Blob([JSON.stringify(e.data.logs, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `sdk-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
}
};
function log(level, message, data = {}) {
worker.postMessage({ type: 'log', payload: { level, message, data } });
console[level](`[${level.toUpperCase()}] ${message}`, data);
}
function exportLogs() {
worker.postMessage({ type: 'export' });
}
function clearLogs() {
worker.postMessage({ type: 'clear' });
}
A.4 日志面板 + UILogger
<div id="log-panel" style="display:none;position:fixed;bottom:0;left:0;right:0;height:300px;background:#1e1e1e;color:#fff;z-index:9999;">
<div style="padding:8px;background:#333;display:flex;justify-content:space-between;align-items:center;">
<span>📋 SDK Logs</span>
<div>
<button onclick="logger.exportToJSON()" style="margin-right:8px;">导出 JSON</button>
<button onclick="logger.exportToTXT()" style="margin-right:8px;">导出 TXT</button>
<button onclick="logger.clearLogs()" style="margin-right:8px;">清空</button>
<button onclick="document.getElementById('log-panel').style.display='none'">关闭</button>
</div>
</div>
<div id="log-content" style="height:calc(100% - 40px);overflow-y:auto;padding:10px;font-family:monospace;font-size:12px;"></div>
</div>
<button onclick="document.getElementById('log-panel').style.display='block'" style="position:fixed;bottom:10px;right:10px;z-index:9998;">📜 查看日志</button>
UILogger(继承 LocalLogger):
class UILogger extends LocalLogger {
updateUI() {
this.getAllLogs().then(logs => {
const el = document.getElementById('log-content');
if (!el) return;
el.innerHTML = logs.slice(-100).map(log => `
<div style="color:${this.getLevelColor(log.level)}">
[${new Date(log.timestamp).toLocaleTimeString()}] [${log.level.toUpperCase()}] ${log.message}
</div>
`).join('');
el.scrollTop = el.scrollHeight;
});
}
getLevelColor(level) {
const colors = {
error: '#ff6b6b',
warn: '#ffd93d',
info: '#6bcb77',
debug: '#4d96ff'
};
return colors[level] || '#fff';
}
async log(level, message, data = {}) {
await super.log(level, message, data);
this.updateUI();
}
}
if (typeof window.AndroidBridge !== 'undefined' && window.AndroidBridge.saveLogFile) {
logger.exportToJSON = async function () {
const logs = await logger.getAllLogs();
window.AndroidBridge.saveLogFile(JSON.stringify(logs, null, 2));
};
}
A.5 CrossPlatformLogger
class CrossPlatformLogger {
constructor() {
this.useIndexedDB = typeof indexedDB !== 'undefined';
this.db = null;
this.dbName = 'SDKLogs';
this.storeName = 'logs';
this.maxLogs = 10000;
}
async init() {
if (!this.useIndexedDB) return this;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this);
};
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
async log(level, message, data = {}) {
const entry = { timestamp: Date.now(), level, message, data };
if (this.useIndexedDB && this.db) {
const tx = this.db.transaction([this.storeName], 'readwrite');
tx.objectStore(this.storeName).add(entry);
await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
const count = await new Promise(r => {
const req = this.db.transaction([this.storeName], 'readonly').objectStore(this.storeName).count();
req.onsuccess = () => r(req.result);
});
if (count > this.maxLogs) {
const delTx = this.db.transaction([this.storeName], 'readwrite');
const store = delTx.objectStore(this.storeName);
const idx = store.index('timestamp');
const cursorReq = idx.openCursor(IDBKeyRange.lowerBound(0));
let n = 0;
cursorReq.onsuccess = (ev) => {
const cur = ev.target.result;
if (cur && n < count - this.maxLogs) {
store.delete(cur.primaryKey);
n++;
cur.continue();
}
};
}
} else {
try {
const logs = JSON.parse(localStorage.getItem('sdk_logs') || '[]');
logs.push(entry);
if (JSON.stringify(logs).length > 4 * 1024 * 1024) logs.shift();
localStorage.setItem('sdk_logs', JSON.stringify(logs));
} catch (e) {
console.warn('localStorage full or unavailable', e);
}
}
console[level](`[${level.toUpperCase()}] ${message}`, data);
}
async getAllLogs() {
if (this.useIndexedDB && this.db) {
return new Promise((resolve, reject) => {
const req = this.db.transaction([this.storeName], 'readonly').objectStore(this.storeName).getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
try {
return JSON.parse(localStorage.getItem('sdk_logs') || '[]');
} catch {
return [];
}
}
downloadFile(content, filename, type) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([content], { type }));
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
async exportToJSON() {
const logs = await this.getAllLogs();
this.downloadFile(JSON.stringify(logs, null, 2), `sdk-logs-${Date.now()}.json`, 'application/json');
}
async clearLogs() {
if (this.useIndexedDB && this.db) {
await new Promise((resolve, reject) => {
const req = this.db.transaction([this.storeName], 'readwrite').objectStore(this.storeName).clear();
req.onsuccess = resolve;
req.onerror = () => reject(req.error);
});
} else {
localStorage.removeItem('sdk_logs');
}
}
}
A.6 RobustLogger(localForage)
import localforage from 'localforage';
const LOG_KEY = 'logs';
const MAX_LOGS = 10000;
localforage.config({
name: 'SDKLogs',
storeName: 'logs'
});
class RobustLogger {
async log(level, message, data = {}) {
const logs = (await localforage.getItem(LOG_KEY)) || [];
logs.push({ timestamp: Date.now(), level, message, data });
if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS);
await localforage.setItem(LOG_KEY, logs);
console[level](`[${level.toUpperCase()}] ${message}`, data);
}
async getAllLogs() {
return (await localforage.getItem(LOG_KEY)) || [];
}
async exportToJSON() {
const logs = await this.getAllLogs();
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `sdk-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
}
async clearLogs() {
await localforage.setItem(LOG_KEY, []);
}
}
延伸阅读
- W3C:IndexedDB、Storage 标准与 Quota 相关接口。
- localForage:文档与配置项(driver 优先级、size 等)。
- Sentry/Bugsnag:前端 SDK 的本地缓存与上报策略说明。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online