在 Vue 项目中,如何设计和实现一个通用的 PWA 离线缓存策略,支持离线访问和消息推送?

各位老铁,晚上好!我是今天的主讲人,很高兴能和大家一起聊聊Vue项目中的PWA离线缓存策略。今天咱们不搞那些虚头巴脑的,直接上干货,争取让大家听完就能上手,让你的Vue项目也拥有“断网不断电”的超能力!

PWA:让你的Vue应用更上一层楼

首先,咱们先简单回顾一下什么是PWA。PWA (Progressive Web App) 是一种使用 Web 技术构建,但拥有原生 App 体验的 Web 应用。它具有以下特点:

  • 可靠性 (Reliable): 即使在网络状况不佳或离线状态下也能快速加载。
  • 快速 (Fast): 响应迅速,提供流畅的用户体验。
  • 吸引力 (Engaging): 具有类似原生 App 的交互体验,例如添加到主屏幕、推送通知等。

其中,离线缓存是PWA的核心特性之一。想象一下,用户在地铁里打开你的Vue应用,即使信号再差,也能流畅浏览之前访问过的内容,是不是很酷?

离线缓存策略:选择比努力更重要

实现离线缓存,最关键的就是选择合适的缓存策略。不同的策略适用于不同的场景,选对了事半功倍,选错了可能适得其反。

咱们常用的缓存策略主要有以下几种:

缓存策略 描述 适用场景 优点 缺点
Cache-first 优先从缓存中获取资源,如果缓存中没有,则从网络获取,并将获取到的资源添加到缓存中。 静态资源(例如图片、CSS、JS文件),一旦缓存后不需要频繁更新的资源。 速度快,离线可用。 如果资源更新了,用户可能无法立即获取到最新的版本。
Network-first 优先从网络获取资源,如果网络请求失败,则从缓存中获取。 需要实时更新的数据,例如新闻资讯、股票行情等。 保证用户始终获取到最新的数据。 如果网络不稳定,用户可能需要等待较长时间才能获取到资源。
Cache-only 只从缓存中获取资源,如果缓存中没有,则返回错误。 预先知道所有资源都已缓存的情况,例如离线游戏。 速度快,完全离线可用。 如果缓存中没有资源,则无法获取。
Network-only 只从网络获取资源,不使用缓存。 永远不需要缓存的资源,例如用户行为统计接口。 保证用户始终获取到最新的数据。 如果网络不稳定,则无法获取资源。
Stale-while-revalidate 先从缓存中获取资源,同时在后台更新缓存。当下次请求相同资源时,会返回缓存中的旧版本,并同时更新缓存。 对实时性要求不高,但希望尽快显示数据的资源,例如用户头像、文章列表等。 速度快,且可以保证最终一致性。 用户可能会看到旧版本的数据,直到缓存更新完成。

选择哪种策略,需要根据你的应用场景和数据特点来决定。没有一种策略是万能的,需要灵活运用。

Service Worker:离线缓存的幕后英雄

有了缓存策略,接下来就要靠Service Worker来落地实现了。Service Worker 是一个运行在浏览器后台的脚本,它可以拦截网络请求,并根据缓存策略来决定是返回缓存中的资源,还是从网络获取资源。

注册 Service Worker

首先,在你的Vue项目的入口文件(例如 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 文件,来实现具体的缓存逻辑。

const CACHE_NAME = 'my-vue-app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/static/css/app.css',
  '/static/js/app.js',
  '/static/js/chunk-vendors.js',
  '/img/logo.png' // 你的logo
];

// 安装 Service Worker
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;
        }

        // Not in cache - return fetch request
        return fetch(event.request).then(
          function(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 independent copies.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

// 激活 Service Worker
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);
          }
        })
      );
    })
  );
});

这段代码做了以下几件事情:

  1. 定义缓存名称和需要缓存的URL列表: CACHE_NAME 定义了缓存的名称,urlsToCache 定义了需要预先缓存的资源列表。
  2. 安装 Service Worker:install 事件中,我们将 urlsToCache 中的资源添加到缓存中。
  3. 拦截网络请求:fetch 事件中,我们首先尝试从缓存中获取资源,如果缓存中没有,则从网络获取,并将获取到的资源添加到缓存中。
  4. 激活 Service Worker:activate 事件中,我们清理旧版本的缓存。

这段代码实现了一个简单的 Cache-first 策略。当用户访问 urlsToCache 中的资源时,会优先从缓存中获取,如果缓存中没有,则从网络获取,并将获取到的资源添加到缓存中。

更加复杂的缓存策略

上面的例子只是一个简单的 Cache-first 策略,实际项目中可能需要更复杂的策略。例如,我们可以使用 workbox 来简化 Service Worker 的开发。

workbox 是 Google 提供的 PWA 工具库,它提供了各种缓存策略和工具,可以帮助我们更轻松地实现离线缓存。

首先,安装 workbox-webpack-plugin

npm install workbox-webpack-plugin --save-dev

然后,在 vue.config.js 文件中配置 workbox-webpack-plugin

const { GenerateSW } = require('workbox-webpack-plugin');

module.exports = {
  // ...
  configureWebpack: {
    plugins: [
      new GenerateSW({
        clientsClaim: true,
        skipWaiting: true,
        runtimeCaching: [
          {
            urlPattern: /^https://fonts.(?:googleapis|gstatic).com/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'google-fonts-cache',
              expiration: {
                maxEntries: 10,
                maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
              },
              cacheableResponse: {
                statuses: [0, 200],
              },
            },
          },
          {
            urlPattern: /.(?:png|jpg|jpeg|gif|svg|webp)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'images-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
              },
              cacheableResponse: {
                statuses: [0, 200],
              },
            },
          },
          {
            urlPattern: /.(?:js|css)$/,
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'static-resources',
              expiration: {
                maxEntries: 60,
                maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
              },
              cacheableResponse: {
                statuses: [0, 200],
              },
            },
          },
        ],
      })
    ]
  }
  // ...
}

这段代码会使用 workbox-webpack-plugin 自动生成 service-worker.js 文件,并配置了三种缓存策略:

  • CacheFirst:用于缓存 Google Fonts 和图片资源。
  • StaleWhileRevalidate:用于缓存 JS 和 CSS 资源。

你可以根据自己的需求配置不同的缓存策略。

消息推送:与用户保持连接

除了离线缓存,PWA 还可以实现消息推送功能,即使应用不在前台,也能向用户发送通知。

获取用户授权

首先,我们需要获取用户的授权才能发送消息推送。

function requestPushPermission() {
  return new Promise(function(resolve, reject) {
    const permissionResult = Notification.requestPermission(function(result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  })
  .then(function(permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error('用户拒绝了推送授权!');
    }
  });
}

这段代码会向用户请求推送授权,如果用户同意,则返回 granted,否则返回 denieddefault

获取推送订阅

获取用户授权后,我们需要获取用户的推送订阅信息,才能向用户发送消息推送。

function subscribeUser() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(function(registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          '你的 VAPID 公钥'
        )
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function(pushSubscription) {
      console.log('Received Push Subscription: ', JSON.stringify(pushSubscription));
      // 将推送订阅信息发送到服务器
      sendSubscriptionToServer(pushSubscription);
      return pushSubscription;
    });
  }
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

这段代码会获取用户的推送订阅信息,并将其发送到服务器。其中,applicationServerKey 是 VAPID 公钥,用于验证推送请求的合法性。

服务器端发送消息推送

获取到用户的推送订阅信息后,服务器端就可以向用户发送消息推送了。

服务器端需要使用 Web Push 协议来发送消息推送。常用的 Web Push 库有 web-push (Node.js) 和 pywebpush (Python)。

以 Node.js 为例,使用 web-push 库发送消息推送:

const webpush = require('web-push');

// VAPID keys should only be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();

webpush.setVapidDetails(
  'mailto:[email protected]',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

// 使用推送订阅信息发送消息推送
const pushSubscription = {
  endpoint: '用户的推送订阅 endpoint',
  keys: {
    auth: '用户的推送订阅 auth',
    p256dh: '用户的推送订阅 p256dh'
  }
};

const payload = JSON.stringify({
  title: 'Hello PWA!',
  body: 'This is a test notification.',
  icon: '/img/logo.png'
});

webpush.sendNotification(pushSubscription, payload)
  .then(result => console.log(result))
  .catch(error => console.error(error));

这段代码会向指定的推送订阅发送消息推送。其中,payload 是消息推送的内容,可以包含标题、正文、图标等信息。

Service Worker 处理消息推送

当服务器端发送消息推送后,Service Worker 会接收到推送消息,并显示通知。

self.addEventListener('push', function(event) {
  console.log('[Service Worker] Push Received.');
  console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

  const notificationTitle = 'PWA Demo';
  const notificationOptions = {
    body: event.data.text(),
    icon: '/img/logo.png',
    badge: '/img/badge.png'
  };

  event.waitUntil(self.registration.showNotification(notificationTitle,
    notificationOptions));
});

这段代码会在 Service Worker 接收到推送消息时,显示一个通知。

总结

今天我们聊了Vue项目中的PWA离线缓存策略和消息推送功能。希望大家能够掌握这些知识,让你的Vue应用更上一层楼!

记住,PWA 不是一蹴而就的,需要不断地学习和实践。希望大家能够多尝试、多思考,打造出更好的 PWA 应用!

好了,今天的讲座就到这里,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注