HOOOS

Service Worker加持,让你的天气预报PWA在离线状态下依然坚挺!

0 7 墨迹天气预报员 Service WorkerPWA离线应用
Apple

嘿,各位空中飞人们,经常出差是不是最怕落地后没信号,想查个天气都抓瞎?今天咱们就来聊聊怎么用 Service Worker 这玩意儿,给你的天气预报 PWA(Progressive Web App)装上离线“外挂”,保证没网也能心里有数!

啥是Service Worker?它凭啥这么牛?

简单来说,Service Worker 就是一个跑在你浏览器背后的“小弟”,它能拦截你的网络请求,然后决定是从缓存里拿数据,还是去网络上请求最新的。这就厉害了,相当于给你的 PWA 提前准备好了“备胎”,没网的时候直接用“备胎”顶上,体验瞬间提升 N 个档次!

想象一下这个场景:

你刚下飞机,打开天气预报 PWA,就算没 Wi-Fi,也能看到昨天查过的北京天气,心里顿时踏实了不少,知道今天该穿啥。这就是 Service Worker 的功劳!

咱们的目标:打造一个靠谱的离线天气预报 PWA

  • 离线可用: 这是最基本的要求,没网也能看最近浏览过的城市天气。
  • 自动更新: 一旦网络恢复,Service Worker 就能自动去更新数据,保证你看到的是最新的天气情况。
  • 用户提示: 离线状态下,要明确告诉用户“现在是离线模式”,避免用户误以为数据是实时的。
  • 数据过期处理: 天气数据有时效性,要考虑数据过期的问题,给用户一个合理的提示。

撸起袖子,开干!Service Worker实战教程

1. 注册你的 Service Worker

首先,在你的主 JavaScript 文件里,加上这段代码:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(function(registration) {
      console.log('Service Worker 注册成功,作用域:', registration.scope);
    })
    .catch(function(error) {
      console.log('Service Worker 注册失败:', error);
    });
}

这段代码会检查浏览器是否支持 Service Worker,如果支持,就注册你的 service-worker.js 文件。这个文件就是 Service Worker 的核心代码。

2. 编写 Service Worker 脚本(service-worker.js)

这个文件是重头戏,咱们一步一步来:

2.1 定义缓存名称和要缓存的资源

const CACHE_NAME = 'weather-app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/style.css',
  '/script.js',
  '/images/icon.png',
  // 这里可以添加你 PWA 需要缓存的其他资源,比如字体文件、图片等等
];
  • CACHE_NAME:缓存的名称,每次更新缓存时,可以修改这个名称,浏览器会自动更新缓存。
  • urlsToCache:一个数组,包含了你需要缓存的静态资源,比如 HTML、CSS、JavaScript、图片等等。

2.2 监听 install 事件

当 Service Worker 注册成功后,会触发 install 事件。在这个事件里,咱们可以预先缓存一些资源:

self.addEventListener('install', function(event) {
  // 延迟 install 事件,直到缓存完成
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('已打开缓存');
        return cache.addAll(urlsToCache);
      })
  );
});

这段代码会打开一个名为 weather-app-cache-v1 的缓存,然后把 urlsToCache 里的所有资源都放进去。这样,你的 PWA 在第一次加载时,就会把这些资源缓存起来,下次再访问时,就可以直接从缓存里读取,速度飞快!

2.3 监听 fetch 事件

当你的 PWA 发起网络请求时,会触发 fetch 事件。在这个事件里,咱们可以决定是从缓存里拿数据,还是去网络上请求最新的:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // 缓存命中
        if (response) {
          return response;
        }

        // 缓存未命中,去网络请求
        return fetch(event.request).then(
          function(response) {
            // 检查是否收到了有效的响应
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // 克隆一份响应,因为 response body 是 stream,只能读取一次
            var responseToCache = response.clone();

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

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

这段代码首先会尝试从缓存里查找对应的资源,如果找到了,就直接返回缓存里的数据。如果没找到,就去网络上请求,然后把请求到的数据也放进缓存里,下次再访问时,就可以直接从缓存里读取了。

注意:

  • 这段代码只缓存了 GET 请求,因为 POST 请求通常是用来提交数据的,不适合缓存。
  • 在缓存网络请求的响应之前,要先克隆一份,因为 response body 是 stream,只能读取一次。

2.4 监听 activate 事件

当 Service Worker 更新时,会触发 activate 事件。在这个事件里,咱们可以清理旧的缓存:

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

这段代码会获取所有的缓存名称,然后删除不在 cacheWhitelist 里的缓存。cacheWhitelist 包含了你想要保留的缓存名称,比如当前的缓存名称 CACHE_NAME。这样,你就可以在更新 Service Worker 时,自动清理旧的缓存,避免缓存污染。

3. 获取天气数据并缓存

3.1 修改 fetch 事件的处理逻辑

为了实现离线查看天气的功能,我们需要修改 fetch 事件的处理逻辑,只缓存天气 API 的请求结果。

假设你的天气 API 的 URL 是 https://api.example.com/weather?city=北京,你可以这样修改 fetch 事件的处理逻辑:

self.addEventListener('fetch', function(event) {
  // 只处理天气 API 的请求
  if (event.request.url.startsWith('https://api.example.com/weather')) {
    event.respondWith(
      caches.match(event.request)
        .then(function(response) {
          // 缓存命中
          if (response) {
            return response;
          }

          // 缓存未命中,去网络请求
          return fetch(event.request).then(
            function(response) {
              // 检查是否收到了有效的响应
              if(!response || response.status !== 200 || response.type !== 'basic') {
                return response;
              }

              // 克隆一份响应
              var responseToCache = response.clone();

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

              return response;
            }
          );
        })
    );
  } else {
    // 其他请求,直接去网络请求
    event.respondWith(fetch(event.request));
  }
});

3.2 在前端代码中,把天气数据存储到 localStorage

为了在离线状态下显示天气信息,我们需要把最近浏览过的城市天气数据存储到 localStorage 里。当 Service Worker 拦截到天气 API 的请求时,可以先从 localStorage 里读取数据,如果 localStorage 里没有数据,再去网络上请求。

// 获取天气数据
function getWeather(city) {
  fetch(`https://api.example.com/weather?city=${city}`)
    .then(response => response.json())
    .then(data => {
      // 把天气数据存储到 localStorage
      localStorage.setItem(city, JSON.stringify(data));
      // 显示天气数据
      displayWeather(data);
    });
}

// 显示天气数据
function displayWeather(data) {
  // ...
}

3.3 修改 fetch 事件的处理逻辑,从 localStorage 读取数据

self.addEventListener('fetch', function(event) {
  // 只处理天气 API 的请求
  if (event.request.url.startsWith('https://api.example.com/weather')) {
    event.respondWith(
      caches.match(event.request)
        .then(function(response) {
          // 缓存命中
          if (response) {
            return response;
          }

          // 缓存未命中,从 localStorage 读取数据
          const city = new URL(event.request.url).searchParams.get('city');
          const weatherData = localStorage.getItem(city);

          if (weatherData) {
            // 把 localStorage 里的数据转换成 Response 对象
            const response = new Response(weatherData, {
              headers: {
                'Content-Type': 'application/json'
              }
            });
            return response;
          }

          // localStorage 里也没有数据,去网络请求
          return fetch(event.request).then(
            function(response) {
              // 检查是否收到了有效的响应
              if(!response || response.status !== 200 || response.type !== 'basic') {
                return response;
              }

              // 克隆一份响应
              var responseToCache = response.clone();

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

              return response;
            }
          );
        })
    );
  } else {
    // 其他请求,直接去网络请求
    event.respondWith(fetch(event.request));
  }
});

4. 用户体验优化:离线提示和数据过期处理

4.1 离线提示

当用户在离线状态下访问你的 PWA 时,你需要明确地告诉用户“现在是离线模式”,避免用户误以为数据是实时的。你可以在页面上添加一个提示信息,或者使用一个全局的 JavaScript 变量来表示当前是否处于离线状态。

// 监听 online 和 offline 事件
window.addEventListener('online',  updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);

function updateOnlineStatus() {
  if (navigator.onLine) {
    // Online
    console.log('Online');
    // 移除离线提示
    document.getElementById('offline-message').style.display = 'none';
  } else {
    // Offline
    console.log('Offline');
    // 显示离线提示
    document.getElementById('offline-message').style.display = 'block';
  }
}

4.2 数据过期处理

天气数据有时效性,你需要考虑数据过期的问题,给用户一个合理的提示。你可以在 localStorage 里存储天气数据时,也存储一个时间戳,表示数据的过期时间。当 Service Worker 从 localStorage 里读取数据时,可以检查数据是否过期,如果过期了,就显示一个提示信息,告诉用户数据可能不是最新的。

// 获取天气数据
function getWeather(city) {
  fetch(`https://api.example.com/weather?city=${city}`)
    .then(response => response.json())
    .then(data => {
      // 存储数据和时间戳
      const weatherData = {
        data: data,
        timestamp: Date.now()
      };
      localStorage.setItem(city, JSON.stringify(weatherData));
      // 显示天气数据
      displayWeather(data);
    });
}

// 修改 fetch 事件的处理逻辑,检查数据是否过期
self.addEventListener('fetch', function(event) {
  // 只处理天气 API 的请求
  if (event.request.url.startsWith('https://api.example.com/weather')) {
    event.respondWith(
      caches.match(event.request)
        .then(function(response) {
          // 缓存命中
          if (response) {
            return response;
          }

          // 缓存未命中,从 localStorage 读取数据
          const city = new URL(event.request.url).searchParams.get('city');
          const weatherDataString = localStorage.getItem(city);

          if (weatherDataString) {
            const weatherData = JSON.parse(weatherDataString);
            // 检查数据是否过期 (假设过期时间为 1 小时)
            if (Date.now() - weatherData.timestamp < 60 * 60 * 1000) {
              // 数据未过期,返回数据
              const response = new Response(JSON.stringify(weatherData.data), {
                headers: {
                  'Content-Type': 'application/json'
                }
              });
              return response;
            } else {
              // 数据已过期,显示提示信息
              return new Response(JSON.stringify({
                error: '天气数据已过期,请联网更新'
              }), {
                headers: {
                  'Content-Type': 'application/json'
                }
              });
            }
          }

          // localStorage 里也没有数据,去网络请求
          return fetch(event.request).then(
            function(response) {
              // 检查是否收到了有效的响应
              if(!response || response.status !== 200 || response.type !== 'basic') {
                return response;
              }

              // 克隆一份响应
              var responseToCache = response.clone();

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

              return response;
            }
          );
        })
    );
  } else {
    // 其他请求,直接去网络请求
    event.respondWith(fetch(event.request));
  }
});

总结

通过 Service Worker,我们可以轻松地给天气预报 PWA 加上离线功能,让用户在没网的情况下也能查看最近浏览过的城市天气。同时,我们还可以通过离线提示和数据过期处理,进一步提升用户体验。赶紧动手试试吧,让你的 PWA 更加强大!

温馨提示:

  • Service Worker 的调试比较麻烦,可以使用 Chrome 的 DevTools 来进行调试。
  • Service Worker 的更新需要一些时间,可能需要手动刷新页面才能看到最新的效果。

希望这篇文章能帮助你打造一个靠谱的离线天气预报 PWA,祝你旅途愉快!

点评评价

captcha
健康