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

各位观众老爷,大家好!我是你们的老朋友,今天给大家带来一场关于Vue项目PWA离线缓存策略的“相声”——哦不,是讲座!保证让大家听得懂、学得会、用得上,就算你以前是“离线状态”,听完也能“在线起飞”!

讲座主题:Vue项目PWA离线缓存策略设计与实现,外加消息推送小技巧

咱们的目标是:让你的Vue项目在没有网络的情况下也能“苟延残喘”,甚至还能推送消息“骚扰”用户!

第一部分:PWA是什么?为什么我们需要它?(500字)

先来点基础知识,别嫌烦,磨刀不误砍柴工嘛。

PWA,全称 Progressive Web App,翻译过来就是“渐进式Web应用”。 听起来高大上,其实就是让你的网站拥有Native App的部分特性,比如:

  • 离线访问: 没有网络也能用,想想都刺激!
  • 添加到主屏幕: 像App一样有个图标,用户体验棒棒哒!
  • 消息推送: 可以给用户发消息,提高用户粘性。

为什么我们需要PWA?

想象一下这个场景:用户在地铁上刷你的网站,突然信号没了!如果是传统网站,用户只能看到一个“无法连接到互联网”的提示,体验瞬间降到冰点。但是,如果你的网站是PWA,用户仍然可以访问之前浏览过的页面,甚至可以填写表单、浏览商品列表等等。简直是用户体验的救星!

再比如,你想给用户发送一些促销信息,但是用户不经常打开你的网站。有了PWA的消息推送功能,就可以直接把消息推送到用户的手机上,简直是营销利器!

PWA的核心技术:Service Worker

Service Worker是PWA的灵魂,它是一个运行在浏览器后台的脚本,可以拦截网络请求、缓存资源、推送消息等等。 简单来说,它就像一个“代理”,帮我们处理网络请求,让我们可以在离线状态下也能访问网站。

第二部分:Vue项目PWA改造:手把手教你实现离线缓存(2000字)

接下来,咱们进入实战环节,一步一步把你的Vue项目改造成PWA。

1. 安装@vue/cli-plugin-pwa插件

如果你是用Vue CLI创建的项目,安装PWA插件非常简单:

vue add pwa

这条命令会自动帮你安装必要的依赖、生成manifest.json文件和registerServiceWorker.js文件,简直是懒人福音!

2. manifest.json文件配置

manifest.json文件定义了PWA的元数据,比如应用名称、图标、主题颜色等等。 打开public/manifest.json文件,修改成你自己的配置:

{
  "name": "我的PWA应用",
  "short_name": "PWA",
  "icons": [
    {
      "src": "./img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#4DBA87",
  "background_color": "#000000"
}
  • name:应用名称,会显示在添加到主屏幕时的图标下方。
  • short_name:应用简称,在空间有限的情况下使用。
  • icons:应用图标,建议提供不同尺寸的图标。
  • start_url:应用启动时的URL。
  • display:应用显示模式,standalone表示以独立应用的形式运行。
  • theme_color:应用主题颜色。
  • background_color:应用背景颜色。

3. registerServiceWorker.js文件

registerServiceWorker.js文件负责注册Service Worker。 打开src/registerServiceWorker.js文件,你会看到类似这样的代码:

import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready () {
      console.log(
        'App is being served from cache by a service worker.n' +
        'For more info, visit https://goo.gl/AFskqB'
      )
    },
    registered () {
      console.log('Service worker has been registered.')
    },
    cached () {
      console.log('Content has been cached for offline use.')
    },
    updatefound () {
      console.log('New content is downloading.')
    },
    updated () {
      console.log('New content is available; please refresh.')
    },
    offline () {
      console.log('No internet connection found. App is running in offline mode.')
    },
    error (error) {
      console.error('Error during service worker registration:', error)
    }
  })
}

这段代码会在生产环境下注册Service Worker,并监听Service Worker的各种事件,比如注册成功、缓存完成、更新等等。

4. 自定义Service Worker (重点!)

默认情况下,@vue/cli-plugin-pwa会帮你生成一个简单的Service Worker,它可以缓存静态资源,比如JS、CSS、图片等等。 但是,如果你想实现更复杂的离线缓存策略,就需要自定义Service Worker了。

在项目的根目录下创建一个public/service-worker.js文件(如果已经存在,就直接修改它)。 下面是一个简单的自定义Service Worker示例:

const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/css/app.css',
  '/js/app.js',
  '/img/logo.png'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // IMPORTANT: Clone the request. This is needed because the request
        // is a stream and streams can only be consumed once.
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // IMPORTANT: Clone the response. This is needed to save the response
            // for later use.
            var responseToCache = response.clone();

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

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

self.addEventListener('activate', function(event) {

  var cacheWhitelist = [CACHE_NAME];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
  • CACHE_NAME:缓存名称,每次更新Service Worker时,建议更新缓存名称。
  • urlsToCache:需要缓存的URL列表,可以根据你的需求修改。
  • install事件:Service Worker安装时触发,用于缓存静态资源。
  • fetch事件:每次发起网络请求时触发,用于拦截请求并从缓存中返回响应。
  • activate事件:Service Worker激活时触发,用于清理旧的缓存。

5. 缓存策略选择

不同的缓存策略适用于不同的场景。 这里介绍几种常见的缓存策略:

缓存策略 描述 适用场景
Cache First 优先从缓存中获取,如果缓存中没有,则从网络获取,并缓存到缓存中。 静态资源、不经常更新的资源
Network First 优先从网络获取,如果网络请求失败,则从缓存中获取。 动态数据、需要实时更新的资源
Cache Only 只从缓存中获取,如果缓存中没有,则返回错误。 离线应用、不需要网络连接的资源
Network Only 只从网络获取,不使用缓存。 重要数据、不允许使用缓存的资源
Stale-While-Revalidate 先从缓存中获取,同时发起网络请求更新缓存。 对实时性要求不高,但需要快速响应的资源

6. 动态缓存

除了缓存静态资源,我们还可以缓存动态资源,比如API请求的结果。 可以使用Cache API来实现动态缓存。

self.addEventListener('fetch', function(event) {
  if (event.request.url.startsWith('/api/')) {
    event.respondWith(
      caches.open('api-cache')
        .then(function(cache) {
          return fetch(event.request)
            .then(function(response) {
              cache.put(event.request.url, response.clone());
              return response;
            });
        })
    );
  } else {
    // ... 其他资源的缓存逻辑
  }
});

第三部分:消息推送:让你的PWA“骚扰”用户(1000字)

有了离线缓存,你的PWA已经很棒了,但是还可以更棒! 消息推送可以让你主动给用户发送消息,提高用户粘性。

1. 获取用户授权

在使用消息推送之前,必须先获取用户的授权。

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('Permission not granted.');
    }
  });
}

requestPushPermission()
  .then(() => {
    console.log('用户已授权');
  })
  .catch(error => {
    console.error('获取授权失败:', error);
  });

2. 获取Subscription

获取用户授权后,需要获取用户的Subscription,Subscription包含了用户的推送信息,比如endpoint和keys。

function subscribeUser() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(function(registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          '你的VAPID Public Key' // 替换成你自己的VAPID Public Key
        )
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function(pushSubscription) {
      console.log('Received Push Subscription: ', JSON.stringify(pushSubscription));
      // TODO: Send pushSubscription to your server
    });
  }
}

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;
}

subscribeUser();
  • applicationServerKey:VAPID Public Key,用于验证推送请求的合法性。 需要生成一对VAPID Key,Public Key用于客户端,Private Key用于服务器端。 可以使用web-push库生成VAPID Key。

3. 服务器端推送消息

获取到用户的Subscription后,就可以在服务器端使用web-push库推送消息了。

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

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

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

const pushSubscription = {
  endpoint: '用户的endpoint', // 替换成用户的endpoint
  keys: {
    auth: '用户的auth', // 替换成用户的auth
    p256dh: '用户的p256dh' // 替换成用户的p256dh
  }
};

const payload = JSON.stringify({
  title: '我的PWA应用',
  body: '您有一条新消息!',
  icon: '/img/logo.png'
});

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

4. Service Worker 接收消息

在Service Worker中监听push事件,接收服务器端推送的消息。

self.addEventListener('push', function(event) {
  const data = event.data.json();
  const options = {
    body: data.body,
    icon: data.icon,
    badge: '/img/badge.png'
  };

  event.waitUntil(self.registration.showNotification(data.title, options));
});

self.addEventListener('notificationclick', function(event) {
  event.notification.close();

  event.waitUntil(
    clients.openWindow('https://www.example.com') // 点击消息后打开的URL
  );
});

第四部分:常见问题及注意事项(500字)

  • 缓存更新问题: 当你的网站内容更新时,需要更新Service Worker的版本,才能让浏览器重新缓存资源。 可以通过修改CACHE_NAME来实现。
  • 调试Service Worker: 可以使用Chrome DevTools的Application面板来调试Service Worker。
  • HTTPS要求: Service Worker只能在HTTPS环境下运行。
  • 兼容性问题: 不同的浏览器对PWA的支持程度不同,需要进行兼容性测试。
  • VAPID Key 安全: VAPID Private Key 必须保存在服务器端,不能泄露给客户端。
  • 用户体验: 不要滥用消息推送,以免打扰用户。

总结

今天的讲座就到这里了。 相信大家已经对Vue项目PWA离线缓存策略和消息推送有了更深入的了解。 记住,PWA不是一蹴而就的,需要不断学习和实践,才能真正掌握它。 希望大家都能把自己的Vue项目改造成强大的PWA,给用户带来更好的体验!

感谢大家的观看,下次再见!

发表回复

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