各位观众,晚上好!今天咱们来聊聊Vue应用怎么才能做到“断网也能浪”,也就是离线缓存这事儿。目标就是让你的用户在地铁里、深山老林里也能刷你的应用,起码能看到上次刷到的东西,而不是一个可怜的“网络错误”页面。
我们要用到的工具呢,就是Service Worker
、IndexedDB
和 localStorage
这哥仨。别怕,听名字唬人,其实掌握了套路,也没那么难。
一、Service Worker:幕后英雄
首先,Service Worker
是个啥?你可以把它想象成一个运行在浏览器后台的“小弟”,它能拦截网络请求,然后决定是去网络拿数据,还是从缓存里拿。关键是,它能在你的应用关闭后依然运行!这才是离线缓存的精髓所在。
-
注册 Service Worker
首先,在你的 Vue 项目的
public
目录下创建一个service-worker.js
文件(名字随意,只要你喜欢)。 然后,在你的main.js
里注册它:// 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); }); }); }
这段代码的意思就是,如果浏览器支持
Service Worker
,那么就在页面加载完成后注册service-worker.js
。 -
Service Worker 的生命周期
Service Worker
有几个重要的生命周期事件:install
、activate
、fetch
。-
install: 这是
Service Worker
第一次安装的时候触发的。通常我们在这里做一些初始化工作,比如缓存一些静态资源。 -
activate: 这是
Service Worker
激活的时候触发的。通常我们在这里清理旧的缓存。 -
fetch: 这是最重要的一个事件,每次浏览器发起网络请求的时候都会触发。我们可以在这里拦截请求,然后决定是走网络还是走缓存。
-
-
缓存静态资源
在
service-worker.js
里,我们先缓存一些静态资源,比如 CSS、JS、图片等。// service-worker.js const cacheName = 'my-vue-app-v1'; const staticAssets = [ '/', '/index.html', '/css/app.css', '/js/app.js', '/img/logo.png' // 假设你有个logo ]; self.addEventListener('install', e => { e.waitUntil( caches.open(cacheName) .then(cache => { console.log('Caching static assets'); return cache.addAll(staticAssets); }) ); }); self.addEventListener('activate', e => { e.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.filter(cacheName => { // 清理旧的缓存 return cacheName !== 'my-vue-app-v1'; }).map(cacheName => { return caches.delete(cacheName); }) ); }) ); });
这段代码的意思是,在
install
事件里,我们创建了一个名为my-vue-app-v1
的缓存,然后把staticAssets
里的资源都放进去。在activate
事件里,我们清理了所有旧的缓存。 -
拦截网络请求
现在,我们可以拦截网络请求,然后决定是走网络还是走缓存。
// service-worker.js self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request) .then(response => { // 如果缓存里有,就返回缓存里的 if (response) { return response; } // 如果缓存里没有,就去网络拿 return fetch(e.request); } ) ); });
这段代码的意思是,每次浏览器发起网络请求,我们都先去缓存里找,如果找到了就返回缓存里的,如果没找到就去网络拿。
二、IndexedDB:数据大管家
Service Worker
负责缓存静态资源,但是动态数据怎么办?比如用户发布的文章、评论等等。这时候就要用到 IndexedDB
了。IndexedDB
是一个浏览器提供的 NoSQL 数据库,可以在客户端存储大量结构化数据。
-
打开数据库
首先,我们需要打开数据库。
// db.js (单独创建一个文件) const DB_NAME = 'my-vue-app-db'; const DB_VERSION = 1; // Remember to increment version when schema changes! const OBJECT_STORE_NAME = 'posts'; let db = null; function openDatabase() { return new Promise((resolve, reject) => { if (db) { resolve(db); return; } const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = (event) => { console.error("Database error: " + event.target.errorCode); reject(event); }; request.onsuccess = (event) => { db = event.target.result; console.log("Database opened successfully"); resolve(db); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create an object store const objectStore = db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id', autoIncrement: true }); // Define what data items the object store will contain objectStore.createIndex('title', 'title', { unique: false }); objectStore.createIndex('body', 'body', { unique: false }); objectStore.createIndex('createdAt', 'createdAt', { unique: false }); console.log("Object store created"); }; }); } export { openDatabase, OBJECT_STORE_NAME };
这段代码的意思是,我们打开一个名为
my-vue-app-db
的数据库,版本号是 1。如果数据库不存在,就创建一个名为posts
的对象仓库,用来存储文章数据。 -
存储数据
现在,我们可以往数据库里存储数据了。
// db.js import { openDatabase, OBJECT_STORE_NAME } from './db.js'; async function addPost(post) { const db = await openDatabase(); return new Promise((resolve, reject) => { const transaction = db.transaction([OBJECT_STORE_NAME], 'readwrite'); const objectStore = transaction.objectStore(OBJECT_STORE_NAME); const request = objectStore.add(post); request.onsuccess = () => { console.log('Post added to the database'); resolve(request.result); // Resolve with the generated ID }; request.onerror = () => { console.error('Error adding post: ', request.error); reject(request.error); }; }); } export { openDatabase, OBJECT_STORE_NAME, addPost };
这段代码的意思是,我们创建了一个事务,然后往
posts
对象仓库里添加一条数据。 -
读取数据
当然,我们也要能从数据库里读取数据。
// db.js import { openDatabase, OBJECT_STORE_NAME } from './db.js'; async function getAllPosts() { const db = await openDatabase(); return new Promise((resolve, reject) => { const transaction = db.transaction([OBJECT_STORE_NAME], 'readonly'); const objectStore = transaction.objectStore(OBJECT_STORE_NAME); const request = objectStore.getAll(); request.onsuccess = () => { console.log('All posts retrieved from the database'); resolve(request.result); }; request.onerror = () => { console.error('Error getting all posts: ', request.error); reject(request.error); }; }); } export { openDatabase, OBJECT_STORE_NAME, addPost, getAllPosts };
这段代码的意思是,我们创建了一个事务,然后从
posts
对象仓库里读取所有数据。 -
在 Vue 组件中使用 IndexedDB
现在,我们可以在 Vue 组件中使用
IndexedDB
了。// MyComponent.vue <template> <div> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script> import { getAllPosts, addPost } from './db.js'; export default { data() { return { posts: [] }; }, async mounted() { this.posts = await getAllPosts(); // 模拟添加数据 setTimeout(async () => { const newPost = { title: '这是新文章', body: '这是新文章的内容', createdAt: new Date() }; const id = await addPost(newPost); newPost.id = id; // Assign the ID to the new post this.posts.push(newPost); }, 2000); } }; </script>
这段代码的意思是,在组件加载完成后,我们从
IndexedDB
里读取所有文章,然后显示在页面上。 我们还模拟了添加数据,2秒后添加一个新文章。 -
Service Worker 同步 IndexedDB
最关键的一步,就是让
Service Worker
在网络恢复的时候,把IndexedDB
里的数据同步到服务器。 这里我们采用 Background Sync API.- 注册 Background Sync
首先,在你的 Vue 组件里,注册一个 Background Sync。
// MyComponent.vue <template> <div> <button @click="syncData">Sync Data</button> </div> </template> <script> export default { methods: { syncData() { if ('serviceWorker' in navigator && 'SyncManager' in window) { navigator.serviceWorker.ready.then(registration => { registration.sync.register('sync-posts') .then(() => console.log('Background sync registered!')) .catch(err => console.error('Background sync registration failed: ', err)); }); } else { console.log('Background sync not supported.'); } } } }; </script>
这段代码的意思是,如果浏览器支持
Service Worker
和SyncManager
,那么就注册一个名为sync-posts
的 Background Sync。- 在 Service Worker 里处理 Background Sync
然后,在
service-worker.js
里,处理 Background Sync 事件。// service-worker.js self.addEventListener('sync', event => { if (event.tag === 'sync-posts') { event.waitUntil(syncPosts()); } }); async function syncPosts() { console.log("Syncing posts..."); // TODO: Implement the logic to read data from IndexedDB and send it to the server // This is a placeholder, replace with your actual syncing logic. // 1. Read posts from IndexedDB that need to be synced // 2. Send them to the server // 3. If successful, remove them from IndexedDB // 4. If failed, leave them in IndexedDB for the next sync attempt // Example (replace with your actual code): try { const postsToSync = await getAllPostsFromIndexedDB(); // Replace with your IndexedDB retrieval logic console.log("Posts to Sync:", postsToSync); for (const post of postsToSync) { const response = await fetch('/api/posts', { // Replace with your API endpoint method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(post) }); if (response.ok) { // Remove post from IndexedDB if sync was successful await deletePostFromIndexedDB(post.id); // Replace with your IndexedDB deletion logic console.log(`Post with id ${post.id} synced successfully and deleted from DB.`); } else { console.error(`Failed to sync post with id ${post.id}.`); throw new Error('Sync failed'); // This will retry the sync } } console.log("All posts synced successfully!"); } catch (error) { console.error("Error during sync:", error); // The sync event will be retried later } }
这段代码的意思是,当
sync-posts
事件触发时,我们调用syncPosts
函数,把IndexedDB
里的数据同步到服务器。 注意,这里需要替换成你自己的同步逻辑,包括从 IndexedDB 读取数据,发送到服务器,以及删除同步成功的数据。
三、localStorage:轻量级数据缓存
localStorage
也是一个浏览器提供的存储方案,但是它只能存储字符串,而且容量比较小。所以,我们通常用它来存储一些轻量级的数据,比如用户的配置信息、主题设置等等。
-
存储数据
localStorage.setItem('theme', 'dark');
-
读取数据
const theme = localStorage.getItem('theme');
-
删除数据
localStorage.removeItem('theme');
四、组合使用:最佳实践
现在,我们已经了解了 Service Worker
、IndexedDB
和 localStorage
的基本用法。那么,怎么把它们组合起来使用呢?
技术 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Service Worker | 缓存静态资源、拦截网络请求、后台数据同步 | 离线访问、提升性能、后台同步 | 复杂,需要处理生命周期、兼容性问题 |
IndexedDB | 存储大量结构化数据,比如文章、评论等 | 存储容量大、支持索引、事务 | API 复杂、异步操作 |
localStorage | 存储少量配置信息,比如主题、语言等 | API 简单、同步操作 | 存储容量小、只能存储字符串 |
最佳实践:
- Service Worker 缓存静态资源:使用
Service Worker
缓存 CSS、JS、图片等静态资源,让应用可以离线访问。 - IndexedDB 存储动态数据:使用
IndexedDB
存储文章、评论等动态数据,让用户可以离线浏览。 - localStorage 存储配置信息:使用
localStorage
存储用户的配置信息,比如主题、语言等,让用户可以自定义应用。 - Background Sync 同步数据:使用 Background Sync API,在网络恢复的时候,把
IndexedDB
里的数据同步到服务器。
五、注意事项
- 缓存策略:选择合适的缓存策略,比如 Cache First、Network First 等。
- 缓存更新:定期更新缓存,避免用户看到过时的内容。
- 错误处理:处理各种错误情况,比如网络错误、数据库错误等。
- 用户体验:给用户提供友好的提示,比如“正在离线访问”、“正在同步数据”等。
总结
今天我们一起学习了如何使用 Service Worker
、IndexedDB
和 localStorage
来实现 Vue 应用的离线缓存。虽然有点复杂,但是只要掌握了套路,就能让你的应用在任何情况下都能给用户提供良好的体验。 希望大家都能做出“断网也能浪”的 Vue 应用!
今天的讲座就到这里,谢谢大家!