咳咳,大家好!欢迎来到今天的“让你的 Vuex/Pinia 状态永不掉线”讲座。我是你们的老朋友,老码农小马,今天咱们就来聊聊如何让 Vuex/Pinia 的状态像口香糖一样粘在你的浏览器里,即便断网也能访问,而且还能像打了鸡血一样持久化。
咱们今天的主题就是围绕状态持久化和离线访问,主要会涉及到 IndexedDB 和 Service Worker 这两个大杀器,当然,还会穿插一些小技巧和注意事项,保证大家听完之后都能满载而归。
第一部分:状态持久化的重要性
首先,咱们得明白,为啥要费这么大劲搞状态持久化?想象一下,你辛辛苦苦填了一张表单,结果刷新一下页面,数据全没了,是不是想砸电脑?这就是状态丢失的痛!
- 提升用户体验: 用户体验至上!持久化状态可以避免用户重复操作,比如记住用户的登录状态、购物车内容、偏好设置等等。
- 离线访问支持: 在网络不稳定或者离线状态下,用户仍然可以访问应用的部分功能,比如浏览已缓存的文章、查看购物车商品等等。
- 数据恢复能力: 防止因意外刷新、关闭页面导致的数据丢失,提升应用的健壮性。
第二部分:IndexedDB:浏览器端的本地数据库
IndexedDB 是浏览器提供的一个强大的本地数据库,它允许我们在浏览器端存储大量结构化数据,并且支持事务、索引等高级特性。 咱们先来看看如何将 Vuex/Pinia 的状态存储到 IndexedDB 中。
2.1 IndexedDB 的基本概念
- 数据库 (Database): 存储数据的容器,一个域名对应一个数据库。
- 对象仓库 (Object Store): 类似于关系型数据库中的表,用于存储特定类型的数据。
- 索引 (Index): 用于加速数据检索。
- 事务 (Transaction): 用于保证数据操作的原子性。
2.2 使用 indexedDB
API 的基本流程
- 打开数据库: 使用
indexedDB.open()
方法打开数据库,如果数据库不存在,则会创建数据库。 - 创建对象仓库: 在
onupgradeneeded
事件中创建对象仓库,并定义索引。 - 开启事务: 使用
db.transaction()
方法开启事务,指定事务的模式(只读或读写)。 - 获取对象仓库: 通过事务获取对象仓库。
- 执行操作: 使用对象仓库的
add()
、put()
、get()
、delete()
等方法执行数据操作。 - 关闭数据库: 使用
db.close()
方法关闭数据库。
2.3 封装 IndexedDB 操作
为了方便使用,我们可以将 IndexedDB 的操作封装成一个模块。
// indexedDB.js
const DB_NAME = 'my-app-db';
const STORE_NAME = 'app-state';
const DB_VERSION = 1;
let db = null;
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error('Failed to open database:', event);
reject(event);
};
request.onsuccess = (event) => {
db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 如果对象仓库不存在,则创建它
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'key' }); // keyPath 设置为 key
}
};
});
}
async function getData(key) {
if (!db) {
await openDB();
}
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const objectStore = transaction.objectStore(STORE_NAME);
const request = objectStore.get(key);
request.onerror = (event) => {
console.error('Failed to get data:', event);
reject(event);
};
request.onsuccess = (event) => {
resolve(request.result ? request.result.value : null); // 返回 value 属性
};
});
}
async function saveData(key, value) {
if (!db) {
await openDB();
}
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const objectStore = transaction.objectStore(STORE_NAME);
const request = objectStore.put({ key: key, value: value }); // 存储 key 和 value
request.onerror = (event) => {
console.error('Failed to save data:', event);
reject(event);
};
request.onsuccess = (event) => {
resolve();
};
});
}
export { openDB, getData, saveData };
2.4 结合 Vuex/Pinia 使用 IndexedDB
接下来,咱们看看如何在 Vuex/Pinia 中使用这个封装好的 IndexedDB 模块。
Vuex 示例:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { openDB, getData, saveData } from '../indexedDB';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
user: null
},
mutations: {
setCount(state, count) {
state.count = count
},
setUser(state, user) {
state.user = user
}
},
actions: {
async initStore({ commit }) {
await openDB(); // 确保数据库已打开
const count = await getData('count') || 0;
const user = await getData('user') || null;
commit('setCount', count);
commit('setUser', user);
},
async updateCount({ commit }, count) {
commit('setCount', count);
await saveData('count', count);
},
async updateUser({ commit }, user) {
commit('setUser', user);
await saveData('user', user);
}
},
modules: {}
})
Pinia 示例:
// stores/main.js
import { defineStore } from 'pinia'
import { openDB, getData, saveData } from '../indexedDB';
export const useMainStore = defineStore('main', {
state: () => ({
count: 0,
user: null
}),
actions: {
async initStore() {
await openDB();
this.count = await getData('count') || 0;
this.user = await getData('user') || null;
},
async updateCount(count) {
this.count = count;
await saveData('count', count);
},
async updateUser(user) {
this.user = user;
await saveData('user', user);
}
}
})
2.5 注意事项
- 错误处理: IndexedDB 的操作是异步的,需要进行充分的错误处理。
- 数据序列化: IndexedDB 只能存储特定类型的数据,对于复杂对象,可能需要进行序列化和反序列化,比如使用
JSON.stringify()
和JSON.parse()
。 - 版本管理: 当数据库结构发生变化时,需要更新数据库版本,并在
onupgradeneeded
事件中进行数据迁移。 - 容量限制: 浏览器对 IndexedDB 的容量有限制,需要注意避免存储过大的数据。
第三部分:Service Worker:离线访问的守护神
Service Worker 是一个运行在浏览器后台的脚本,它可以拦截网络请求,并根据预定义的策略返回缓存或网络数据,从而实现离线访问。 咱们来一起看看如何使用 Service Worker 来缓存 Vuex/Pinia 的状态,并实现离线访问。
3.1 Service Worker 的基本概念
- 注册: 在网页中注册 Service Worker。
- 安装 (Install): Service Worker 安装时,会缓存静态资源。
- 激活 (Activate): Service Worker 激活后,会接管页面的网络请求。
- 拦截请求 (Fetch): Service Worker 可以拦截页面的网络请求,并根据缓存策略返回数据。
3.2 Service Worker 的生命周期
Service Worker 的生命周期包括以下几个阶段:
- 注册: 网页调用
navigator.serviceWorker.register()
方法注册 Service Worker。 - 下载: 浏览器下载 Service Worker 脚本。
- 安装: 浏览器执行 Service Worker 脚本的
install
事件,通常用于缓存静态资源。 - 等待: Service Worker 进入等待状态,直到所有相关的页面都关闭。
- 激活: Service Worker 激活后,会接管页面的网络请求。
- 运行: Service Worker 持续运行,直到被卸载或更新。
3.3 创建 Service Worker 脚本
创建一个名为 service-worker.js
的文件,用于编写 Service Worker 逻辑。
// service-worker.js
const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/main.js', // 你的应用主入口 js
'/style.css' //你的应用样式
// 其他静态资源
];
self.addEventListener('install', (event) => {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
(response) => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
3.4 注册 Service Worker
在你的 Vue 应用的主入口文件(例如 main.js
)中注册 Service Worker。
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then((registration) => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch((err) => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
3.5 动态缓存 API 请求
除了缓存静态资源,我们还可以使用 Service Worker 动态缓存 API 请求,以便在离线状态下访问 API 数据。
修改 service-worker.js
文件,添加 API 请求的缓存逻辑。
// service-worker.js
// ... (之前的代码)
self.addEventListener('fetch', (event) => {
// 检查是否是 API 请求
if (event.request.url.startsWith('/api/')) {
event.respondWith(
caches.match(event.request).then((response) => {
// 缓存命中 - 返回缓存的响应
if (response) {
return response;
}
// 没有缓存 - 发起网络请求
return fetch(event.request).then((response) => {
// 检查是否收到了有效的响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应,因为我们需要将其提供给浏览器和缓存
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
} else {
// 处理静态资源
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
(response) => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
}
});
3.6 结合 IndexedDB 和 Service Worker
我们可以将 IndexedDB 和 Service Worker 结合起来使用,实现更强大的离线访问功能。例如,可以使用 Service Worker 拦截 API 请求,如果网络可用,则从网络获取数据并存储到 IndexedDB 中;如果网络不可用,则从 IndexedDB 中读取数据。
3.7 注意事项
- 作用域: Service Worker 的作用域由注册时指定的路径决定,只有在该作用域内的页面才能被 Service Worker 控制。
- 更新: 当 Service Worker 文件发生变化时,浏览器会自动下载并安装新的 Service Worker,但需要等待所有相关的页面都关闭后才能激活。
- 调试: 可以使用 Chrome DevTools 的 "Application" 面板来调试 Service Worker。
- HTTPS: Service Worker 只能在 HTTPS 环境下使用,或者在 localhost 环境下使用。
第四部分:一些高级技巧
- 使用
workbox
库:workbox
是 Google 提供的一套 Service Worker 工具库,可以简化 Service Worker 的开发,提供各种缓存策略和路由规则。 - 增量更新: 只缓存发生变化的数据,减少缓存的大小。
- 后台同步: 使用
Background Sync API
在网络恢复后自动同步数据。
第五部分:总结
今天咱们一起学习了如何使用 IndexedDB 和 Service Worker 来实现 Vuex/Pinia 状态的持久化和离线访问。 希望大家能够将这些技术应用到自己的项目中,提升用户体验,打造更加健壮的应用。
最后,记住,技术是为人类服务的,不要为了技术而技术,要根据实际需求选择合适的方案。 祝大家编程愉快!下次再见!