前端数据库缓存
前端缓存
相信各位老司机都知道前端的缓存有哪些,不外乎就是cookie,localStorage和sessionStorage,但是还有一种缓存,其功能强大与数据库不逞多让(夸张了),这就是新!一!代!缓存------IndexedDB 具体位置
下面所有的代码地址:gitee地址
功能及用法
功能
具体功能你可以把他理解成一个存储在前端的数据库,没错,就是数据库,存储的方式是以键值对的方式进行存储,其上线很大很大,但是一般也跟浏览器有关系,主要用于离线缓存数据的获取
用法
indexedDB用法是比较麻烦的,主要是方法名字长不好记,而且使用比较麻烦,那这里直接上代码
创建数据库
const createDatabase = (dbName: string,dbVersion: number,names: string[],keyPath: string,deleteData?: string[],
) => {// 如果浏览器不支持 IndexedDB,返回一个空对象if (!window.indexedDB) {console.error('浏览器不支持 IndexedDB');return Promise.reject(new Error('浏览器不支持 IndexedDB'));}return new Promise((resolve, reject) => {const request = indexedDB.open(dbName, dbVersion);request.onerror = (event) => {console.error('数据库打开失败', event);reject(event);};request.onsuccess = (event) => {const db = (event.target as IDBOpenDBRequest).result;db.onversionchange = function () {db.close(); // 关闭数据库连接alert('Database is outdated, please reload the page.');};resolve(db);};request.onupgradeneeded = async (event) => {console.log('数据库升级', event);const db = (event.target as IDBOpenDBRequest).result;console.log(db, 'db');const version = event.oldVersion;// 在版本变更时创建对象存储for (const name of names) {// 如果表ying存在,就不创建了if (db.objectStoreNames.contains(name)) {console.log(`表 ${name} 已存在`);continue;}const objectStore = db.createObjectStore(name, {keyPath,});if (name === 'playlistCreatedAndSongs') {console.log(123);// 在 存储上创建索引,以便按 createId查询objectStore.createIndex('createId', 'createId', { unique: false });// 创建一个歌曲idobjectStore.createIndex('songId', 'songId', { unique: false });}}if (deleteData && deleteData.length > 0) {for (const name of deleteData) {if (db.objectStoreNames.contains(name)) {db.deleteObjectStore(name);console.log(`表 ${name} 已删除`);}}}resolve({ db, version });};});
};
创建表
const createObjectStore = (db: IDBDatabase,storeName: string,autoIncrement = true,keyPath?: string,
) => {return new Promise((resolve, reject) => {if (db.objectStoreNames.contains(storeName)) {console.log('表已存在');reject('表已存在');return;} else {// 如果您在创建对象存储时决定不使用 keyPath,您可以将 autoIncrement 设置为 true。这将允许 IndexedDB 自动为每个新记录生成一个唯一的主键const objectStore = db.createObjectStore(storeName, { keyPath, autoIncrement });objectStore.transaction.oncomplete = () => {console.log('表创建成功');resolve(db);};}});
};
给表添加数据
const addData = (db: IDBDatabase, storeName: string, data: any) => {return new Promise((resolve, reject) => {const transaction = db.transaction(storeName, 'readwrite');const objectStore = transaction.objectStore(storeName);const request = objectStore.add(data);request.onsuccess = () => {console.log('数据添加成功');resolve(request.result);};request.onerror = (event) => {console.error('数据添加失败', event);reject(event);};});
};
表删除数据
const deleteData = (db: IDBDatabase, storeName: string, key: IDBValidKey) => {return new Promise((resolve, reject) => {const transaction = db.transaction(storeName, 'readwrite');const objectStore = transaction.objectStore(storeName);const request = objectStore.delete(key);request.onsuccess = () => {console.log('数据删除成功');resolve(request.result);};request.onerror = (event) => {console.error('数据删除失败', event);reject(event);};});
};
更新表数据
// 更新数据
const updateData = (db: IDBDatabase, storeName: string, data: any) => {return new Promise((resolve, reject) => {const transaction = db.transaction(storeName, 'readwrite');const objectStore = transaction.objectStore(storeName);const request = objectStore.put(data);request.onsuccess = () => {console.log('数据更新成功');resolve(request.result);};request.onerror = (event) => {console.error('数据更新失败', event);reject(event);};});
};
好了,基本就是这些,结束了。。。那是不可能的,这玩意看的一头雾水,没事,我稍微说一下流程,再给一些案例大家立刻就懂了
第一:创建数据库
固定格式(下面代码是部分代码)const request = indexedDB.open(dbName, dbVersion);
,dbName为数据库名称,dbVersion为版本,注意:当
- 数据库不存在的时候,即dbName不存在,会自动创建,否则就是连接到这个数据库,dbName的dbVersion(版本)不能低于上一个版本,比如有一个teacher数据库,第一次创建的时候版本为1,如果你要更新这个数据库,版本必须大于1,否则出错
- 如果版本发生了变化,比如一开始开发的时候,用户已经有了数据库,但是后续更新之后数据库变成2,通过onversionchange回调函数告诉用户需要更新
db.onversionchange = function () {db.close(); // 关闭数据库连接alert('Database is outdated, please reload the page.');};
案例:
地址:gitee地址
<script setup lang="ts">
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
import { useDark, useToggle } from '@vueuse/core';
import { createDatabase } from '@/utils/indexDB';
import { useInit } from './store';
// 引入图片
import avatar from '@/assets/image/avatar.webp';
const isDark = useDark({storageKey: null, // 禁用持久化
});
const toggleDark = useToggle(isDark);
const dialogVisible = ref(false);
const locale = zhCn;
const init = useInit();
// 通知
const message = '欢迎来到欧阳天的音乐播放器,祝您使用愉快!';
const notification = () => {if (!('Notification' in window)) {console.log('浏览器不支持通知');return;}if (Notification.permission === 'granted') {new Notification('通知', {body: message,icon: avatar,});} else if (Notification.permission !== 'denied') {Notification.requestPermission().then((permission) => {if (permission === 'granted') {new Notification('通知', {body: message,icon: avatar,});}});}
};
const know = () => {localStorage.setItem('know', '1');dialogVisible.value = false;
};
const dbName = 'Music';
const dbVersion = 1;
onMounted(async () => {new PerformanceObserver((list) => {list.getEntries().forEach((entry) => {console.log('Layout shift:', entry);});}).observe({ type: 'layout-shift', buffered: true });window.$wujie?.bus.$on('toggle', function (message: any) {console.log(message, 'message');toggleDark();});// recentlyPlayed:最近播放,playlistCreatedtry {const res: any = await createDatabase(dbName,dbVersion,['like', 'recentlyPlayed', 'playlistCreated', 'songs', 'playlistCreatedAndSongs'],'id',['collect'],);init.setIndexDB(res.db ? res.db : res);// console.log(res, 'res');// if (res.db) {// res.open(dbName, dbVersion);// }// 创建连接// const connect = await createObjectStore(res, 'connect');// 创建喜欢表// const like = await createObjectStore(res, 'like');} catch (error) {console.log(error);}notification();if (localStorage.getItem('know') === '1') {dialogVisible.value = false;} else {dialogVisible.value = true;}
});
</script><template><el-config-provider :locale="locale"><el-dialogv-model="dialogVisible"title="提示"width="50%"style="max-width: 600px"append-to-body:z-index="1000"><p class="content">该音乐播放使用无界框架搭建的微服务,除开最上方的几个榜单和榜单歌曲通过第三方接口获取,其他所有数据均保存在浏览器的indexDB中,所以当更换浏览器之后数据会丢失,请知晓</p><template #footer><span class="dialog-footer"><el-button type="primary" @click="know"> 我已知晓,不再提示 </el-button><el-button @click="dialogVisible = false"> 关闭 </el-button></span></template></el-dialog><router-view></router-view></el-config-provider>
</template><style scoped lang="scss">
.content {font-family: 'self-font';line-height: 1.5;
}
.dialog-footer {font-family: 'self-font';
}
</style>
第二步:创建表
也是固定写法 下面代码是部分代码
const objectStore = db.createObjectStore(storeName, { keyPath, autoIncrement });objectStore.transaction.oncomplete = () => {console.log('表创建成功');resolve(db);};}
注意:
- 表的创建必须在数据库的版本更新的时候或者第一次创建的时候,即onupgradeneeded 回调函数,该回调函数在创建和更新版本的时候都会触发,必须在这里进行创建表
request.onupgradeneeded = async (event) => {for (const name of names) {// 如果表ying存在,就不创建了if (db.objectStoreNames.contains(name)) {console.log(`表 ${name} 已存在`);continue;}const objectStore = db.createObjectStore(name, {keyPath,});}}
新增数据和删除
没什么特别需要注意的点,主要是
const transaction = db.transaction(storeName, 'readwrite');
readwrite表示具有读和写的操作
案例:gitee
<template><el-skeleton :rows="5" v-if="loading" /><div class="right-list" v-if="!loading"><RecycleScroller :items="list" key-field="id" class="scroller" :item-size="30"><template #before><music-message :item="first"></music-message><div class="music-list-header"><li>序号</li><li>歌曲</li><li>作者</li><li>喜欢</li><li>时长</li></div></template><template #default="props"><!-- <music-message :item="first" v-if="props.item.type === 'one'"></music-message> --><li class="song-item" :class="{ active: currentIndexs === props.item.id }"><span>{{ props.index + 1 }}</span><div @click="play(props.item, props.index)" class="music"><div class="music-left"><imgsrc="@/assets/image/img_loading.gif"alt=""v-lazy:data-src="props.item.al.picUrl"v-if="(currentWidth as number) > 630"/><span :title="props.item.name">{{ props.item.name }}</span></div><div class="opation"><el-icon title="收藏" @click.stop="collectFun(props.item)"><Collection/></el-icon><el-icontitle="移出"v-if="item.from === 'my'"@click.stop="deleteFun(props.item)"><Delete/></el-icon></div></div><span :title="handleData(props.item.ar)"> {{ handleData(props.item.ar) }}</span><span class="image"><imgsrc="../assets/image/like.svg"alt=""v-if="!props.item.like"@click="likes('like', props.item)"/><imgsrc="../assets/image/likePointer.svg"alt=""v-if="props.item.like"@click="likes('nolike', props.item)"/></span><span> {{ millisecondsToMinutesAndSeconds(props.item.dt) }}</span></li></template></RecycleScroller><collect-index :collectItem="currentItem" ref="collect"></collect-index><delete-index:currentSelectItem="currentItem"ref="delete":item@delete="deleteSuccess"></delete-index></div>
</template>
<script setup lang="ts">
import { getSongList } from '@/utils/music';
import { useInit } from '@/store';
import eventBus from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
import cloneDeep from 'lodash-es/cloneDeep';
import { v4 as uuidv4 } from 'uuid';
// import { ElMessage, ElMessageBox } from 'element-plus';
// import 'element-plus/theme-chalk/el-message.css';
// const currentComponent = shallowRef<any>(null);
const init = useInit();
const { item = {}, isplay = false } = defineProps<{item: IList;isplay: boolean;
}>();
import { addData, deleteData, getData as getDataIndexDb, dbGet } from '@/utils/indexDB';
const { indexDB } = init;
const { currentMusic, currentIndex, currentWidth } = storeToRefs(init);
const first = ref<{ [key: string]: any }>({});
const list = ref<IList[]>([]);
const currentIndexs = ref(-1);
const loading = ref(true);
const axiosList: any[] = [];
const collectRef = useTemplateRef('collect');
const deleteRef = useTemplateRef('delete');
const currentItem = ref<IList>({});
// 点击收藏
// / 加载组件
// const loadComponent = () => {
// currentComponent.value = defineAsyncComponent(
// () => import('@/components/CollectIndex.vue'),
// );
// };
// 移出成功
const deleteSuccess = () => {// 将数据移出const index = list.value.findIndex((item: IList) => item.id === currentItem.value.id);if (index !== -1) {// list.value.splice(index, 1);// 重新赋值的方式更新list.value = [...list.value.slice(0, index), ...list.value.slice(index + 1)];}
};
const collectFun = (item: IList) => {currentItem.value = item;if (collectRef.value) {(collectRef.value as any).dialogVisible = true;}
};
// 点击删除
const deleteFun = (item: IList) => {currentItem.value = item;if (deleteRef.value) {(deleteRef.value as any).dialogVisible = true;}
};
const handleLike = async (lists: IList[]) => {lists.forEach((item: IList) => {item.like = false;});try {const resIndexDb = await getDataIndexDb(indexDB, 'like', 'id', true);(resIndexDb as IList[]).forEach((itemA: IList) => {// 在 arrayB 中查找具有相同 id 的对象const itemB = lists.find((item: IList) => item.id === itemA.id);// 如果找到了,则将 itemB 的 like 属性设置为 trueif (itemB) {itemB.like = true;}});} catch (error) {console.log(error);}list.value = lists;// 记录列表init.setMusicList(lists);first.value = lists[0]?.al;initCurrentIndex();
};
const initCurrentIndex = () => {// 切换的时候需要重新设置init的currentIndexconst index = list.value.findIndex((item: IList) => item.id === currentMusic.value.id);if (index !== -1) {init.setCurrentIndex(index);} else {init.setCurrentIndex(-1);}
};
const handle = async () => {// 点击的不用通过接口获取的数据if (item.type === 'like') {// 我喜欢的 音乐try {const resIndexDb = await getDataIndexDb(indexDB, 'like', 'id', true);(resIndexDb as IList[]).forEach((itemA: IList) => {itemA.like = true;});list.value = resIndexDb as IList[];// 记录列表init.setMusicList(resIndexDb as IList[]);first.value = (resIndexDb as IList[])[0]?.al;initCurrentIndex();} catch (error) {console.log(error);}return;}if (item.type === 'recentlyPlayed') {// 最近播放try {const resIndexDb = await getDataIndexDb(indexDB, 'recentlyPlayed', 'id', true);console.log(resIndexDb, 'resIndexDb');try {await handleLike(resIndexDb as IList[]);} catch (error) {console.log(error);}} catch (error) {console.log(error);}}if (item.type === 'playlistCreated') {// 我创建的歌单try {const result = await dbGet(init.indexDB, item.id);const { songs } = result as any;list.value = songs as IList[];init.setMusicList(songs as IList[]);first.value = (songs as IList[])[0]?.al;if (isplay) {play((songs as IList[])[0], 0);}initCurrentIndex();} catch (error) {console.log(error);}}
};// 点击喜欢
const likes = async (islike: 'like' | 'nolike', items: IList) => {if (islike === 'like') {try {// console.log(cloneDeep(items));const res = await addData(indexDB, 'like', cloneDeep(items));console.log(res, 'res');items.like = true;} catch (error) {console.log(error);// 如果添加失败,将状态设置为未喜欢items.like = false;}} else {// 查找并删除数据try {await deleteData(indexDB, 'like', items.id);// console.log(res, 'res');items.like = false;} catch (error) {console.log(error);}// items.like = false;}
};</script>
总结
上述就是比较简单的indexedDB的用法,当然肯定不止这么简单,在gitee项目中也涉及到表的关联(这个是真复杂呀),大家有兴趣可以看一下,具体位置gitee
项目截图