背景:为啥会有它?#
localStorage 用起来顺手,但就像“只有锤子,看啥都像钉子”:
- 同步 API,主线程一卡一卡的,存点大对象明显拖慢交互;
- 容量捉襟见肘,序列化/反序列化全靠你自己;
- 二进制存储?绕一大圈,麻烦。
IndexedDB 硬实力很强,但门槛也不低:事务、游标、版本升级……要是你只想“快点把本地缓存这摊活干完”,学习成本就有点“压榨脑力”。
localForage 的目标很朴素:用 localStorage 一样的脑回路,享受 IndexedDB 的性能与容量。它会优先用 IndexedDB,兜不住再退回 WebSQL(历史原因,现在基本用不上)/localStorage。你用的始终是一套 Promise 风格的 API,存啥都行(对象、数组、Blob、ArrayBuffer 都 OK)。
API 概览:就这几个,够吃一年#
-
初始化与配置
localforage.config({ name, storeName, version, driver })
localforage.setDriver([localforage.INDEXEDDB, localforage.LOCALSTORAGE])
localforage.createInstance({...})
多命名空间/多库
-
KV 操作(Promise)
setItem(key, value)
:存任意 JS 值(可序列化的)getItem(key)
:取值(取不到就是null
)removeItem(key)
:删clear()
:清空当前实例keys()
:拿到所有 keylength()
:有多少条iterate((value, key, index) => {...})
:遍历所有数据
-
驱动常量
localforage.INDEXEDDB
localforage.WEBSQL
(历史遗留)localforage.LOCALSTORAGE
Tips:
- 默认会创建一个名叫
localforage
的数据库和keyvaluepairs
的 store;自己配name/storeName
更稳。 - 失败会抛异常,记得
try/catch
,尤其是配额满了的时候。
最小示例:两分钟就能跑#
安装
npm i localforage# 或 pnpm add localforage / yarn add localforage
初始化 + 基本读写
import localforage from 'localforage'
// 建议:自己定义一个实例,别用默认的,方便隔离const db = localforage.createInstance({ name: 'my-app', storeName: 'kv', // 只能小写字母、数字、下划线})
async function demo() { await db.setItem('user', { id: 1, name: 'Ava' }) const u = await db.getItem<{ id: number; name: string }>('user') console.log('user:', u)
await db.setItem('token', 'abc123') console.log('keys:', await db.keys()) console.log('length:', await db.length())
await db.removeItem('token') // await db.clear() // 清空当前实例}demo()
逐步扩展:把常见需求一锅端#
1) 明确优先级:优先 IndexedDB,不行就回退#
await db.setDriver([ localforage.INDEXEDDB, localforage.LOCALSTORAGE,])console.log('driver:', db.driver()) // 实际使用的驱动
大多数现代浏览器都会用上 IndexedDB;遇到“无痕模式/受限环境”,会自动降级到 localStorage。不折腾,省心。
2) 多实例隔离:一人一锅,互不串味#
export const cacheAuth = localforage.createInstance({ name: 'my-app', storeName: 'auth', // 登录态})
export const cacheBiz = localforage.createInstance({ name: 'my-app', storeName: 'biz', // 业务缓存})
好处是清理和迁移都更有“颗粒度”,不会一刀切全删。
3) 存二进制:图片、文件说来就来#
async function saveAvatar(blob: Blob) { await cacheBiz.setItem('avatar', blob) // 直接丢 Blob}async function loadAvatar(): Promise<Blob | null> { return cacheBiz.getItem<Blob>('avatar')}
localForage 内部处理好了序列化,你不必自己转 Base64 折腾。
4) 做个 TTL:到点就失效(它没内置,手搓一个)#
type Box<T> = { value: T; expiresAt?: number }
async function setWithTTL<T>(key: string, value: T, ms?: number) { const box: Box<T> = { value } if (ms) box.expiresAt = Date.now() + ms await cacheBiz.setItem(key, box)}
async function getWithTTL<T>(key: string): Promise<T | null> { const box = await cacheBiz.getItem<Box<T>>(key) if (!box) return null if (box.expiresAt && Date.now() > box.expiresAt) { await cacheBiz.removeItem(key) return null } return box.value}
简单粗暴但好用,常见的“列表缓存 10 分钟”就靠它。
5) 批量与遍历:该快的时候别磨叽#
// 批量await Promise.all([ db.setItem('a', 1), db.setItem('b', 2), db.setItem('c', 3),])
// 遍历await db.iterate((value, key, i) => { console.log(i, key, value)})
6) React/Vue 里用:状态同步一把梭#
React 例子(首屏从本地读,网络回来再刷新):
import { useEffect, useState } from 'react'import localforage from 'localforage'
const postsDB = localforage.createInstance({ name: 'my-app', storeName: 'posts' })
export function usePosts() { const [posts, setPosts] = useState<any[] | null>(null) useEffect(() => { let cancelled = false ;(async () => { const cached = await postsDB.getItem<any[]>('list') if (!cancelled && cached) setPosts(cached) const fresh = await fetch('/api/posts').then(r => r.json()) if (!cancelled) { setPosts(fresh) postsDB.setItem('list', fresh) } })() return () => { cancelled = true } }, []) return posts}
体验就是“先有,再新”,丝滑。
7) 版本迁移:轻量做法就够了#
场景:你之前用 kv
存了用户数据,现在想分成 auth
和 profile
。
async function migrate() { const old = localforage.createInstance({ name: 'my-app', storeName: 'kv' }) const auth = localforage.createInstance({ name: 'my-app', storeName: 'auth' }) const profile = localforage.createInstance({ name: 'my-app', storeName: 'profile' })
await old.iterate(async (value, key) => { if (key.startsWith('token')) await auth.setItem(key, value) if (key.startsWith('user:')) await profile.setItem(key, value) }) // 观察一段时间后再 clear,别立刻“忍痛割爱”}
复杂到“表级事务”的诉求,建议直接上 Dexie 之类的 ORM 风格库。
小结与常见坑#
- 该用它的场景
- 需要更大容量/二进制存储/异步不阻塞;
- 不想深入操作 IndexedDB,但要稳定可用。
- 不该硬上的场景
- 强事务、复杂查询、批量原子性要求很高的离线业务(上 Dexie 或更重的方案)。
常见坑清单
- 驱动与环境
- 私密/无痕模式的 Safari 可能禁用 IndexedDB,实际会回退到 localStorage;注意容量和性能预期。
- WebSQL 已是历史(几乎用不上),别指望它救场。
- 存储上限与错误
- 配额不固定,平台差异大;捕获
QuotaExceededError
,出现就走降级方案(例如裁剪缓存、只存关键字段)。
- 配额不固定,平台差异大;捕获
- 类型与序列化
undefined
会被存成null
;不适合存函数、循环引用对象。- 大对象频繁 set/get,序列化成本会“发胖”,可拆分/分块。
- 性能姿势
- 复用实例,别每次都
createInstance
; - 批量用
Promise.all
,遍历用iterate
,别手写keys + getItem
的“过家家”。
- 复用实例,别每次都
- 路径与命名
storeName
只能用小写字母、数字、下划线;不同实例名/库名会生成不同的 object store,别撞车。
- SSR/非浏览器环境
- 需要
window
/浏览器环境;框架里记得只在客户端执行(比如加onMounted
/useEffect
)。
- 需要
一句话:localForage 就是一把顺手的小扳手,日常修修补补够用又省心;真要大修大改,别犹豫,换上专业工具。