各位观众老爷们,大家好! 欢迎来到今天的“Vue 应用离线缓存奇妙之旅”讲座。我是你们的老朋友,人称Bug终结者,今天就来跟大家聊聊如何打造一个能断网也能用的 Vue 应用。
咱们的目标是:让用户即使在信号不好的地方,或者干脆没网的时候,也能愉快地使用我们的 Vue 应用,就像手机里已经下载好的单机游戏一样。
要实现这个目标,我们需要一套组合拳,把 Service Worker、IndexedDB 和 localStorage 这三位大咖请出来,让他们各司其职,发挥最大的作用。
第一回合:Service Worker——拦截请求,缓存资源
Service Worker 是一个运行在浏览器后台的脚本,它可以拦截网络请求,并决定是直接从缓存中返回资源,还是发起网络请求。它就像一个尽职尽责的门卫,守卫着我们的应用。
-
注册 Service Worker:
首先,在你的
main.js
(或者应用的入口文件) 里注册 Service Worker。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); }); }); }
这段代码会检查浏览器是否支持 Service Worker,如果支持,就在页面加载完成后注册
service-worker.js
这个文件。 -
编写 Service Worker 脚本 (service-worker.js):
接下来,我们需要编写
service-worker.js
文件,这是 Service Worker 的核心。const cacheName = 'my-vue-app-cache-v1'; // 缓存的名字,每次更新都要改 const staticAssets = [ '/', // 根目录 '/index.html', '/js/app.js', // 编译后的 JavaScript 文件 '/css/style.css', // 样式文件 '/img/logo.png', // 图片资源 // ... 其他静态资源 ]; // 安装 Service Worker self.addEventListener('install', event => { event.waitUntil( caches.open(cacheName) .then(cache => { console.log('Opened cache'); return cache.addAll(staticAssets); // 将静态资源添加到缓存 }) ); }); // 激活 Service Worker self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.filter(name => name !== cacheName) // 移除旧的缓存 .map(name => caches.delete(name)) ); }) ); }); // 拦截请求 self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) // 尝试从缓存中获取资源 .then(response => { if (response) { return response; // 如果缓存中有,直接返回 } return fetch(event.request); // 如果缓存中没有,发起网络请求 }) ); });
这段代码做了几件事:
- 定义缓存名称和静态资源列表:
cacheName
用来区分不同版本的缓存,staticAssets
包含了需要缓存的静态资源。 - 安装事件: 在 Service Worker 安装时,将
staticAssets
中的资源添加到缓存中。 - 激活事件: 在 Service Worker 激活时,清理旧版本的缓存。
- 拦截请求事件: 拦截所有网络请求,先尝试从缓存中获取资源,如果缓存中没有,再发起网络请求。
注意事项:
- 每次更新静态资源后,都要修改
cacheName
,这样浏览器才会更新缓存。 staticAssets
列表要包含应用所需的所有静态资源,否则会导致应用无法正常运行。- Service Worker 只能拦截与它位于同一域名或子域名的请求。
- 定义缓存名称和静态资源列表:
第二回合:IndexedDB——存储结构化数据
Service Worker 擅长缓存静态资源,但对于动态数据,比如用户的个人信息、文章内容等等,就需要 IndexedDB 来帮忙了。IndexedDB 是一个浏览器端的 NoSQL 数据库,可以存储大量的结构化数据。
-
封装 IndexedDB 操作:
为了方便使用,我们可以封装一个 IndexedDB 操作类。
class IndexedDBHelper { constructor(dbName, version, objectStoreName) { this.dbName = dbName; this.version = version; this.objectStoreName = objectStoreName; this.db = null; } openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); request.onerror = event => { reject('Failed to open database: ' + event.target.errorCode); }; request.onsuccess = event => { this.db = event.target.result; resolve(this.db); }; request.onupgradeneeded = event => { const db = event.target.result; db.createObjectStore(this.objectStoreName, { keyPath: 'id', autoIncrement: true }); // 创建对象仓库 }; }); } addData(data) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.objectStoreName], 'readwrite'); const objectStore = transaction.objectStore(this.objectStoreName); const request = objectStore.add(data); request.onsuccess = () => { resolve('Data added successfully'); }; request.onerror = () => { reject('Failed to add data: ' + transaction.error); }; }); } getData(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.objectStoreName], 'readonly'); const objectStore = transaction.objectStore(this.objectStoreName); const request = objectStore.get(id); request.onsuccess = event => { resolve(event.target.result); }; request.onerror = () => { reject('Failed to get data: ' + transaction.error); }; }); } getAllData() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.objectStoreName], 'readonly'); const objectStore = transaction.objectStore(this.objectStoreName); const request = objectStore.getAll(); request.onsuccess = event => { resolve(event.target.result); }; request.onerror = () => { reject('Failed to get all data: ' + transaction.error); }; }); } updateData(id, data) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.objectStoreName], 'readwrite'); const objectStore = transaction.objectStore(this.objectStoreName); const request = objectStore.get(id); request.onsuccess = event => { const existingData = event.target.result; if (!existingData) { reject('Data not found'); return; } const updatedData = { ...existingData, ...data, id: id }; // 合并数据 const updateRequest = objectStore.put(updatedData); updateRequest.onsuccess = () => { resolve('Data updated successfully'); }; updateRequest.onerror = () => { reject('Failed to update data: ' + transaction.error); }; }; request.onerror = () => { reject('Failed to get data: ' + transaction.error); }; }); } deleteData(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.objectStoreName], 'readwrite'); const objectStore = transaction.objectStore(this.objectStoreName); const request = objectStore.delete(id); request.onsuccess = () => { resolve('Data deleted successfully'); }; request.onerror = () => { reject('Failed to delete data: ' + transaction.error); }; }); } }
这个类提供了打开数据库、添加数据、获取数据、更新数据和删除数据的方法。
-
在 Vue 组件中使用 IndexedDB:
在 Vue 组件中,我们可以使用这个类来操作 IndexedDB。
<template> <div> <button @click="saveData">保存数据</button> <button @click="loadData">加载数据</button> <p>{{ data }}</p> </div> </template> <script> import IndexedDBHelper from './indexeddb-helper'; export default { data() { return { data: '', dbHelper: null, }; }, async mounted() { this.dbHelper = new IndexedDBHelper('my-vue-app-db', 1, 'my-data'); // 数据库名,版本号,对象仓库名 await this.dbHelper.openDatabase(); }, methods: { async saveData() { const dataToSave = { name: '张三', age: 30, }; await this.dbHelper.addData(dataToSave); console.log('数据已保存'); }, async loadData() { const data = await this.dbHelper.getAllData(); this.data = JSON.stringify(data); console.log('数据已加载'); }, }, }; </script>
这段代码演示了如何使用 IndexedDBHelper 来保存和加载数据。
第三回合:localStorage——存储少量简单数据
localStorage 只能存储少量简单的数据,但它的优点是简单易用,可以用来存储一些配置信息,比如用户的登录状态、主题颜色等等。
-
使用 localStorage:
// 保存数据 localStorage.setItem('theme', 'dark'); // 获取数据 const theme = localStorage.getItem('theme'); // 移除数据 localStorage.removeItem('theme'); // 清空所有数据 localStorage.clear();
localStorage 的 API 非常简单,一看就懂。
终极组合:三剑客协同作战
现在,我们有了 Service Worker、IndexedDB 和 localStorage 这三位大咖,接下来就是让他们协同作战,发挥最大的威力。
-
Service Worker 负责缓存静态资源: 前面已经讲过,Service Worker 负责缓存 HTML、CSS、JavaScript、图片等静态资源,确保应用在离线状态下也能正常加载。
-
IndexedDB 负责存储动态数据: 当应用需要存储动态数据时,比如用户的个人信息、文章内容等等,就使用 IndexedDB。
- 在线时: 从服务器获取数据,并将数据存储到 IndexedDB 中。
- 离线时: 直接从 IndexedDB 中读取数据。
- 数据同步: 当应用重新上线时,将 IndexedDB 中的数据同步到服务器。
-
localStorage 负责存储配置信息: 使用 localStorage 存储用户的登录状态、主题颜色等配置信息,确保用户在离线状态下也能保持个性化设置。
一个更复杂点的例子:离线评论功能
假设我们有一个博客应用,需要支持离线评论功能。
-
数据结构:
// 评论数据结构 { id: 1, // 评论 ID articleId: 123, // 文章 ID author: '张三', // 评论者姓名 content: '评论内容', // 评论内容 timestamp: 1678886400000, // 评论时间戳 isSynced: true, // 是否已同步到服务器 }
-
IndexedDB 存储:
使用 IndexedDB 存储评论数据。
// 添加评论 async addComment(comment) { await this.dbHelper.addData(comment); } // 获取文章的评论 async getCommentsByArticleId(articleId) { const allComments = await this.dbHelper.getAllData(); return allComments.filter(comment => comment.articleId === articleId); } // 获取未同步的评论 async getUnsyncedComments() { const allComments = await this.dbHelper.getAllData(); return allComments.filter(comment => !comment.isSynced); } // 更新评论的同步状态 async updateCommentSyncStatus(id, isSynced) { await this.dbHelper.updateData(id, { isSynced: isSynced }); }
-
评论提交逻辑:
<template> <div> <textarea v-model="commentContent"></textarea> <button @click="submitComment">提交评论</button> </div> </template> <script> import IndexedDBHelper from './indexeddb-helper'; export default { props: { articleId: { type: Number, required: true, }, }, data() { return { commentContent: '', dbHelper: null, }; }, async mounted() { this.dbHelper = new IndexedDBHelper('my-blog-db', 1, 'comments'); await this.dbHelper.openDatabase(); this.loadComments(); }, methods: { async submitComment() { const comment = { articleId: this.articleId, author: '游客', // 或者从 localStorage 获取登录用户的姓名 content: this.commentContent, timestamp: Date.now(), isSynced: false, // 默认未同步 }; await this.dbHelper.addData(comment); this.commentContent = ''; this.loadComments(); this.syncComments(); // 尝试同步评论 }, async loadComments() { // 从 IndexedDB 加载评论 const comments = await this.dbHelper.getCommentsByArticleId(this.articleId); this.$emit('comments-loaded', comments); // 触发事件,将评论传递给父组件 }, async syncComments() { // 检查网络状态 if (!navigator.onLine) { console.log('离线状态,评论将在上线后同步'); return; } // 获取未同步的评论 const unsyncedComments = await this.dbHelper.getUnsyncedComments(); // 同步评论到服务器 for (const comment of unsyncedComments) { try { await this.postCommentToServer(comment); // 假设有一个 postCommentToServer 方法用于将评论发送到服务器 await this.dbHelper.updateCommentSyncStatus(comment.id, true); // 更新同步状态 console.log(`评论 ${comment.id} 同步成功`); } catch (error) { console.error(`评论 ${comment.id} 同步失败:`, error); } } this.loadComments(); // 重新加载评论 }, }, }; </script>
这段代码实现了离线评论的提交和同步功能。
- 提交评论: 将评论保存到 IndexedDB 中,并尝试同步到服务器。
- 加载评论: 从 IndexedDB 中加载评论。
- 同步评论: 检查网络状态,如果在线,则将未同步的评论同步到服务器,并更新 IndexedDB 中的同步状态。
-
Service Worker 的配合:
Service Worker 负责拦截评论提交的请求,如果离线,则将请求缓存起来,等待上线后再发送。
self.addEventListener('fetch', event => { if (event.request.url.includes('/api/comments') && event.request.method === 'POST') { // 拦截评论提交请求 event.respondWith( fetch(event.request.clone()) // 尝试发起网络请求 .catch(() => { // 如果网络请求失败,说明离线,将请求缓存起来 return savePostMessage(event.request.clone()); // 假设有一个 savePostMessage 方法用于缓存 POST 请求 }) ); } else { // 其他请求,按照之前的逻辑处理 event.respondWith( caches.match(event.request) .then(response => { return response || fetch(event.request); }) ); } }); // 监听 Service Worker 的 sync 事件 self.addEventListener('sync', event => { if (event.tag === 'sync-comments') { event.waitUntil(syncComments()); // 假设有一个 syncComments 方法用于同步评论 } });
这段代码演示了如何使用 Service Worker 拦截评论提交请求,并在离线状态下将请求缓存起来。
表格总结:三剑客的职责分工
技术 | 职责 | 优点 | 缺点 |
---|---|---|---|
Service Worker | 缓存静态资源、拦截网络请求、后台同步 | 离线访问、提高性能、推送通知 | 学习曲线较陡峭、调试困难、生命周期复杂 |
IndexedDB | 存储大量的结构化数据 | 容量大、支持事务、支持索引 | API 复杂、异步操作 |
localStorage | 存储少量的简单数据(配置信息) | API 简单易用、同步操作 | 容量小、不适合存储敏感数据、容易被 XSS 攻击 |
总结:
通过 Service Worker、IndexedDB 和 localStorage 的组合使用,我们可以打造一个功能强大、体验优秀的离线 Vue 应用。当然,离线缓存策略的设计需要根据具体的应用场景进行调整,没有一成不变的方案。
希望今天的讲座能对大家有所帮助。记住,Bug 终结者永远是你们的朋友! 下次再见!